From 901de8c009e20457cb5902518ba95c54b36ab926 Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Fri, 6 Sep 2024 17:26:32 +0200 Subject: [PATCH] Replace streams json --- server/reflector/app.py | 2 + server/reflector/views/zulip.py | 46 +++++++ server/reflector/zulip.py | 28 +++++ .../transcripts/[transcriptId]/shareModal.tsx | 115 ++++++++++-------- www/app/api/schemas.gen.ts | 28 +++++ www/app/api/services.gen.ts | 39 ++++++ www/app/api/types.gen.ts | 42 +++++++ 7 files changed, 247 insertions(+), 53 deletions(-) create mode 100644 server/reflector/views/zulip.py diff --git a/server/reflector/app.py b/server/reflector/app.py index cebe3b36..fc9fbdd9 100644 --- a/server/reflector/app.py +++ b/server/reflector/app.py @@ -24,6 +24,7 @@ from reflector.views.transcripts_upload import router as transcripts_upload_rout from reflector.views.transcripts_webrtc import router as transcripts_webrtc_router from reflector.views.transcripts_websocket import router as transcripts_websocket_router from reflector.views.user import router as user_router +from reflector.views.zulip import router as zulip_router try: import sentry_sdk @@ -79,6 +80,7 @@ app.include_router(transcripts_websocket_router, prefix="/v1") app.include_router(transcripts_webrtc_router, prefix="/v1") app.include_router(transcripts_process_router, prefix="/v1") app.include_router(user_router, prefix="/v1") +app.include_router(zulip_router, prefix="/v1") add_pagination(app) # prepare celery diff --git a/server/reflector/views/zulip.py b/server/reflector/views/zulip.py new file mode 100644 index 00000000..451e94e0 --- /dev/null +++ b/server/reflector/views/zulip.py @@ -0,0 +1,46 @@ +from typing import Annotated, Optional + +import reflector.auth as auth +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from reflector.zulip import get_zulip_streams, get_zulip_topics + +router = APIRouter() + + +class Stream(BaseModel): + stream_id: int + name: str + + +class Topic(BaseModel): + name: str + + +@router.get("/zulip/streams") +async def zulip_get_streams( + user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], +) -> list[Stream]: + """ + Get all Zulip streams. + """ + if not user: + raise HTTPException(status_code=403, detail="Authentication required") + + streams = get_zulip_streams() + return streams + + +@router.get("/zulip/streams/{stream_id}/topics") +async def zulip_get_topics( + stream_id: int, + user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], +) -> list[Topic]: + """ + Get all topics for a specific Zulip stream. + """ + if not user: + raise HTTPException(status_code=403, detail="Authentication required") + + topics = get_zulip_topics(stream_id) + return topics diff --git a/server/reflector/zulip.py b/server/reflector/zulip.py index ee35fe13..7f6b85ba 100644 --- a/server/reflector/zulip.py +++ b/server/reflector/zulip.py @@ -10,6 +10,34 @@ class InvalidMessageError(Exception): pass +def get_zulip_topics(stream_id: int) -> list[dict]: + try: + response = requests.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 requests.RequestException as error: + raise Exception(f"Failed to get topics: {error}") + + +def get_zulip_streams() -> list[dict]: + try: + response = requests.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 requests.RequestException as error: + raise Exception(f"Failed to get streams: {error}") + + def send_message_to_zulip(stream: str, topic: str, content: str): try: response = requests.post( diff --git a/www/app/(app)/transcripts/[transcriptId]/shareModal.tsx b/www/app/(app)/transcripts/[transcriptId]/shareModal.tsx index c79165ee..2e8e5ece 100644 --- a/www/app/(app)/transcripts/[transcriptId]/shareModal.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/shareModal.tsx @@ -1,11 +1,11 @@ -import React, { useContext, useState, useEffect, useCallback } from "react"; +import React, { useContext, useState, useEffect } from "react"; import SelectSearch from "react-select-search"; import { GetTranscript, GetTranscriptTopic } from "../../../api"; import "react-select-search/style.css"; import { DomainContext } from "../../../domainContext"; import useApi from "../../../lib/useApi"; -type ShareModal = { +type ShareModalProps = { show: boolean; setShow: (show: boolean) => void; transcript: GetTranscript | null; @@ -13,9 +13,12 @@ type ShareModal = { }; interface Stream { - id: number; + stream_id: number; + name: string; +} + +interface Topic { name: string; - topics: string[]; } interface SelectSearchOption { @@ -23,41 +26,54 @@ interface SelectSearchOption { value: string; } -const ShareModal = (props: ShareModal) => { +const ShareModal = (props: ShareModalProps) => { const [stream, setStream] = useState(undefined); const [topic, setTopic] = useState(undefined); const [includeTopics, setIncludeTopics] = useState(false); const [isLoading, setIsLoading] = useState(true); const [streams, setStreams] = useState([]); - const { zulip_streams } = useContext(DomainContext); + const [topics, setTopics] = useState([]); const api = useApi(); useEffect(() => { - fetch(zulip_streams + "/streams.json") - .then((response) => { - if (!response.ok) { - throw new Error("Network response was not ok"); - } - return response.json(); - }) - .then((data) => { - data = data.sort((a: Stream, b: Stream) => - a.name.localeCompare(b.name), - ); - setStreams(data); + const fetchZulipStreams = async () => { + if (!api) return; + + try { + const response = await api.v1ZulipGetStreams(); + setStreams(response); setIsLoading(false); - // data now contains the JavaScript object decoded from JSON - }) - .catch((error) => { - console.error("There was a problem with your fetch operation:", error); - }); - }, []); + } catch (error) { + console.error("Error fetching Zulip streams:", error); + } + }; + + fetchZulipStreams(); + }, [!api]); + + useEffect(() => { + const fetchZulipTopics = async () => { + if (!api || !stream) return; + try { + const selectedStream = streams.find((s) => s.name === stream); + if (selectedStream) { + const response = await api.v1ZulipGetTopics({ + streamId: selectedStream.stream_id, + }); + setTopics(response); + } + } catch (error) { + console.error("Error fetching Zulip topics:", error); + } + }; + + fetchZulipTopics(); + }, [stream, streams, api]); const handleSendToZulip = async () => { - if (!props.transcript) return; + if (!api || !props.transcript) return; if (stream && topic) { - if (!api) return; try { await api.v1TranscriptPostToZulip({ transcriptId: props.transcript.id, @@ -75,13 +91,15 @@ const ShareModal = (props: ShareModal) => { return
Loading...
; } - let streamOptions: SelectSearchOption[] = []; - if (streams) { - streams.forEach((stream) => { - const value = stream.name; - streamOptions.push({ name: value, value: value }); - }); - } + const streamOptions: SelectSearchOption[] = streams.map((stream) => ({ + name: stream.name, + value: stream.name, + })); + + const topicOptions: SelectSearchOption[] = topics.map((topic) => ({ + name: topic.name, + value: topic.name, + })); return (
@@ -111,7 +129,7 @@ const ShareModal = (props: ShareModal) => { options={streamOptions} value={stream} onChange={(val) => { - setTopic(undefined); + setTopic(undefined); // Reset topic when stream changes setStream(val.toString()); }} placeholder="Pick a stream" @@ -119,25 +137,16 @@ const ShareModal = (props: ShareModal) => {
{stream && ( - <> -
- # - s.name == stream) - ?.topics.sort((a: string, b: string) => - a.localeCompare(b), - ) - .map((t) => ({ name: t, value: t })) || [] - } - value={topic} - onChange={(val) => setTopic(val.toString())} - placeholder="Pick a topic" - /> -
- +
+ # + setTopic(val.toString())} + placeholder="Pick a topic" + /> +
)}