mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-21 12:49:06 +00:00
173 lines
5.7 KiB
Python
173 lines
5.7 KiB
Python
from datetime import timedelta
|
||
from urllib.parse import urlparse
|
||
|
||
import httpx
|
||
|
||
from reflector.db.rooms import rooms_controller
|
||
from reflector.db.transcripts import Transcript, transcripts_controller
|
||
from reflector.settings import settings
|
||
|
||
|
||
class InvalidMessageError(Exception):
|
||
pass
|
||
|
||
|
||
async def get_zulip_topics(stream_id: int) -> list[dict]:
|
||
try:
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.get(
|
||
f"https://{settings.ZULIP_REALM}/api/v1/users/me/{stream_id}/topics",
|
||
auth=(settings.ZULIP_BOT_EMAIL, settings.ZULIP_API_KEY),
|
||
)
|
||
|
||
response.raise_for_status()
|
||
|
||
return response.json().get("topics", [])
|
||
except httpx.RequestError as error:
|
||
raise Exception(f"Failed to get topics: {error}")
|
||
|
||
|
||
async def get_zulip_streams() -> list[dict]:
|
||
try:
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.get(
|
||
f"https://{settings.ZULIP_REALM}/api/v1/streams",
|
||
auth=(settings.ZULIP_BOT_EMAIL, settings.ZULIP_API_KEY),
|
||
)
|
||
|
||
response.raise_for_status()
|
||
|
||
return response.json().get("streams", [])
|
||
except httpx.RequestError as error:
|
||
raise Exception(f"Failed to get streams: {error}")
|
||
|
||
|
||
async def send_message_to_zulip(stream: str, topic: str, content: str):
|
||
try:
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.post(
|
||
f"https://{settings.ZULIP_REALM}/api/v1/messages",
|
||
data={
|
||
"type": "stream",
|
||
"to": stream,
|
||
"topic": topic,
|
||
"content": content,
|
||
},
|
||
auth=(settings.ZULIP_BOT_EMAIL, settings.ZULIP_API_KEY),
|
||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||
)
|
||
|
||
response.raise_for_status()
|
||
|
||
return response.json()
|
||
except httpx.RequestError as error:
|
||
raise Exception(f"Failed to send message to Zulip: {error}")
|
||
|
||
|
||
async def update_zulip_message(message_id: int, stream: str, topic: str, content: str):
|
||
try:
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.patch(
|
||
f"https://{settings.ZULIP_REALM}/api/v1/messages/{message_id}",
|
||
data={
|
||
"topic": topic,
|
||
"content": content,
|
||
},
|
||
auth=(settings.ZULIP_BOT_EMAIL, settings.ZULIP_API_KEY),
|
||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||
)
|
||
|
||
if (
|
||
response.status_code == 400
|
||
and response.json()["msg"] == "Invalid message(s)"
|
||
):
|
||
raise InvalidMessageError(f"There is no message with id: {message_id}")
|
||
|
||
response.raise_for_status()
|
||
|
||
return response.json()
|
||
except httpx.RequestError as error:
|
||
raise Exception(f"Failed to update Zulip message: {error}")
|
||
|
||
|
||
def get_zulip_message(transcript: Transcript, include_topics: bool):
|
||
transcript_url = f"{settings.UI_BASE_URL}/transcripts/{transcript.id}"
|
||
|
||
header_text = f"# Reflector – {transcript.title or 'Unnamed recording'}\n\n"
|
||
header_text += f"**Date**: <time:{transcript.created_at.isoformat()}>\n"
|
||
header_text += f"**Link**: [{extract_domain(transcript_url)}]({transcript_url})\n"
|
||
header_text += f"**Duration**: {format_time_ms(transcript.duration)}\n\n"
|
||
|
||
topic_text = ""
|
||
|
||
if include_topics and transcript.topics:
|
||
topic_text = "```spoiler Topics\n"
|
||
for topic in transcript.topics:
|
||
topic_text += f"1. [{format_time(topic.timestamp)}] {topic.title}\n"
|
||
topic_text += "```\n\n"
|
||
|
||
summary = "```spoiler Summary\n"
|
||
summary += transcript.long_summary or "No summary available"
|
||
summary += "\n```\n\n"
|
||
|
||
message = header_text + summary + topic_text + "-----\n"
|
||
return message
|
||
|
||
|
||
async def post_transcript_notification(transcript: Transcript) -> int | None:
|
||
"""Post or update transcript notification in Zulip.
|
||
|
||
Uses transcript.room_id directly (Hatchet flow).
|
||
Celery's pipeline_post_to_zulip uses recording→meeting→room path instead.
|
||
DUPLICATION NOTE: This function will stay when we use Celery no more, and Celery one will be removed.
|
||
"""
|
||
if not transcript.room_id:
|
||
return None
|
||
|
||
room = await rooms_controller.get_by_id(transcript.room_id)
|
||
if not room or not room.zulip_stream or not room.zulip_auto_post:
|
||
return None
|
||
|
||
message = get_zulip_message(transcript=transcript, include_topics=True)
|
||
message_updated = False
|
||
|
||
if transcript.zulip_message_id:
|
||
try:
|
||
await update_zulip_message(
|
||
transcript.zulip_message_id,
|
||
room.zulip_stream,
|
||
room.zulip_topic,
|
||
message,
|
||
)
|
||
message_updated = True
|
||
except Exception:
|
||
pass
|
||
|
||
if not message_updated:
|
||
response = await send_message_to_zulip(
|
||
room.zulip_stream, room.zulip_topic, message
|
||
)
|
||
message_id = response.get("id")
|
||
if message_id:
|
||
await transcripts_controller.update(
|
||
transcript, {"zulip_message_id": message_id}
|
||
)
|
||
return message_id
|
||
|
||
return transcript.zulip_message_id
|
||
|
||
|
||
def extract_domain(url: str) -> str:
|
||
return urlparse(url).netloc
|
||
|
||
|
||
def format_time_ms(milliseconds: float) -> str:
|
||
return format_time(milliseconds // 1000)
|
||
|
||
|
||
def format_time(seconds: float) -> str:
|
||
td = timedelta(seconds=seconds)
|
||
time = str(td - timedelta(microseconds=td.microseconds))
|
||
|
||
return time[2:] if time.startswith("0:") else time
|