llm-ready-data / app /services /webhook_socket_service.py
Soumik Bose
optimization 404
bd469c1
Raw
History Blame Contribute Delete
5.02 kB
from __future__ import annotations
import asyncio
import hashlib
import hmac
import time
import uuid
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from app.core.logger import get_logger
log = get_logger("webhook-socket")
@dataclass
class Channel:
channel_id: str
secret: Optional[str] = None
buffer_size: int = 0
created_at: float = field(default_factory=time.time)
message_count: int = 0
last_activity: float = field(default_factory=time.time)
subscribers: Dict[Any, asyncio.Queue] = field(default_factory=dict)
history: List[dict] = field(default_factory=list)
class ChannelManager:
def __init__(self, default_buffer: int = 0):
self.channels: Dict[str, Channel] = {}
self.default_buffer = default_buffer
self.total_messages = 0
def create_channel(
self,
channel_id: Optional[str] = None,
secret: Optional[str] = None,
buffer_size: Optional[int] = None,
) -> Channel:
ch_id = channel_id or uuid.uuid4().hex[:16]
buf = buffer_size if buffer_size is not None else self.default_buffer
ch = Channel(channel_id=ch_id, secret=secret, buffer_size=buf)
self.channels[ch_id] = ch
log.info("Channel created id=%s buffer=%d secret=%s", ch_id, buf, "yes" if secret else "no")
return ch
def get_channel(self, channel_id: str) -> Optional[Channel]:
return self.channels.get(channel_id)
def delete_channel(self, channel_id: str) -> bool:
if channel_id in self.channels:
for q in self.channels[channel_id].subscribers.values():
q.put_nowait({"event": "channel_deleted", "channel": channel_id})
del self.channels[channel_id]
log.info("Channel deleted id=%s", channel_id)
return True
return False
def subscribe(self, channel_id: str, ws: Any) -> Optional[asyncio.Queue]:
ch = self.channels.get(channel_id)
if not ch:
return None
q: asyncio.Queue = asyncio.Queue()
ch.subscribers[ws] = q
log.info(
"Subscriber joined channel=%s total_subs=%d",
channel_id, len(ch.subscribers),
)
for msg in ch.history:
q.put_nowait(msg)
return q
def unsubscribe(self, channel_id: str, ws: Any) -> None:
ch = self.channels.get(channel_id)
if ch and ws in ch.subscribers:
del ch.subscribers[ws]
log.info(
"Subscriber left channel=%s total_subs=%d",
channel_id, len(ch.subscribers),
)
async def publish(
self,
channel_id: str,
payload: Any,
headers: Optional[Dict[str, str]] = None,
) -> int:
ch = self.channels.get(channel_id)
if not ch:
return -1
message = {
"event": "message",
"channel": channel_id,
"timestamp": time.time(),
"id": uuid.uuid4().hex[:12],
"payload": payload,
"headers": headers or {},
}
ch.message_count += 1
ch.last_activity = time.time()
self.total_messages += 1
if ch.buffer_size > 0:
ch.history.append(message)
while len(ch.history) > ch.buffer_size:
ch.history.pop(0)
dead: List[Any] = []
sent = 0
for ws, q in list(ch.subscribers.items()):
if getattr(ws, "closed", False):
dead.append(ws)
continue
await q.put(message)
sent += 1
for ws in dead:
del ch.subscribers[ws]
log.info(
"Published channel=%s subs=%d msg_total=%d",
channel_id, sent, ch.message_count,
)
return sent
def stats(self) -> dict:
return {
"channels": len(self.channels),
"total_messages": self.total_messages,
"total_subscribers": sum(len(c.subscribers) for c in self.channels.values()),
"channels_detail": {
cid: {
"subscribers": len(ch.subscribers),
"messages": ch.message_count,
"buffered": len(ch.history),
"last_activity": ch.last_activity,
"has_secret": ch.secret is not None,
}
for cid, ch in self.channels.items()
},
}
def sign_payload(secret: str, raw_body: bytes) -> str:
return "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
def verify_signature(secret: str, raw_body: bytes, signature: str) -> bool:
expected = sign_payload(secret, raw_body)
return hmac.compare_digest(expected, signature)
_manager: Optional[ChannelManager] = None
def get_manager() -> ChannelManager:
global _manager
if _manager is None:
_manager = ChannelManager()
return _manager