mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
Replace streams json
This commit is contained in:
@@ -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_webrtc import router as transcripts_webrtc_router
|
||||||
from reflector.views.transcripts_websocket import router as transcripts_websocket_router
|
from reflector.views.transcripts_websocket import router as transcripts_websocket_router
|
||||||
from reflector.views.user import router as user_router
|
from reflector.views.user import router as user_router
|
||||||
|
from reflector.views.zulip import router as zulip_router
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import sentry_sdk
|
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_webrtc_router, prefix="/v1")
|
||||||
app.include_router(transcripts_process_router, prefix="/v1")
|
app.include_router(transcripts_process_router, prefix="/v1")
|
||||||
app.include_router(user_router, prefix="/v1")
|
app.include_router(user_router, prefix="/v1")
|
||||||
|
app.include_router(zulip_router, prefix="/v1")
|
||||||
add_pagination(app)
|
add_pagination(app)
|
||||||
|
|
||||||
# prepare celery
|
# prepare celery
|
||||||
|
|||||||
46
server/reflector/views/zulip.py
Normal file
46
server/reflector/views/zulip.py
Normal file
@@ -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
|
||||||
@@ -10,6 +10,34 @@ class InvalidMessageError(Exception):
|
|||||||
pass
|
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):
|
def send_message_to_zulip(stream: str, topic: str, content: str):
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
|
|||||||
@@ -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 SelectSearch from "react-select-search";
|
||||||
import { GetTranscript, GetTranscriptTopic } from "../../../api";
|
import { GetTranscript, GetTranscriptTopic } from "../../../api";
|
||||||
import "react-select-search/style.css";
|
import "react-select-search/style.css";
|
||||||
import { DomainContext } from "../../../domainContext";
|
import { DomainContext } from "../../../domainContext";
|
||||||
import useApi from "../../../lib/useApi";
|
import useApi from "../../../lib/useApi";
|
||||||
|
|
||||||
type ShareModal = {
|
type ShareModalProps = {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
setShow: (show: boolean) => void;
|
setShow: (show: boolean) => void;
|
||||||
transcript: GetTranscript | null;
|
transcript: GetTranscript | null;
|
||||||
@@ -13,9 +13,12 @@ type ShareModal = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface Stream {
|
interface Stream {
|
||||||
id: number;
|
stream_id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Topic {
|
||||||
name: string;
|
name: string;
|
||||||
topics: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectSearchOption {
|
interface SelectSearchOption {
|
||||||
@@ -23,41 +26,54 @@ interface SelectSearchOption {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShareModal = (props: ShareModal) => {
|
const ShareModal = (props: ShareModalProps) => {
|
||||||
const [stream, setStream] = useState<string | undefined>(undefined);
|
const [stream, setStream] = useState<string | undefined>(undefined);
|
||||||
const [topic, setTopic] = useState<string | undefined>(undefined);
|
const [topic, setTopic] = useState<string | undefined>(undefined);
|
||||||
const [includeTopics, setIncludeTopics] = useState(false);
|
const [includeTopics, setIncludeTopics] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [streams, setStreams] = useState<Stream[]>([]);
|
const [streams, setStreams] = useState<Stream[]>([]);
|
||||||
const { zulip_streams } = useContext(DomainContext);
|
const [topics, setTopics] = useState<Topic[]>([]);
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(zulip_streams + "/streams.json")
|
const fetchZulipStreams = async () => {
|
||||||
.then((response) => {
|
if (!api) return;
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Network response was not ok");
|
try {
|
||||||
}
|
const response = await api.v1ZulipGetStreams();
|
||||||
return response.json();
|
setStreams(response);
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
data = data.sort((a: Stream, b: Stream) =>
|
|
||||||
a.name.localeCompare(b.name),
|
|
||||||
);
|
|
||||||
setStreams(data);
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
// data now contains the JavaScript object decoded from JSON
|
} catch (error) {
|
||||||
})
|
console.error("Error fetching Zulip streams:", error);
|
||||||
.catch((error) => {
|
}
|
||||||
console.error("There was a problem with your fetch operation:", 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 () => {
|
const handleSendToZulip = async () => {
|
||||||
if (!props.transcript) return;
|
if (!api || !props.transcript) return;
|
||||||
|
|
||||||
if (stream && topic) {
|
if (stream && topic) {
|
||||||
if (!api) return;
|
|
||||||
try {
|
try {
|
||||||
await api.v1TranscriptPostToZulip({
|
await api.v1TranscriptPostToZulip({
|
||||||
transcriptId: props.transcript.id,
|
transcriptId: props.transcript.id,
|
||||||
@@ -75,13 +91,15 @@ const ShareModal = (props: ShareModal) => {
|
|||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let streamOptions: SelectSearchOption[] = [];
|
const streamOptions: SelectSearchOption[] = streams.map((stream) => ({
|
||||||
if (streams) {
|
name: stream.name,
|
||||||
streams.forEach((stream) => {
|
value: stream.name,
|
||||||
const value = stream.name;
|
}));
|
||||||
streamOptions.push({ name: value, value: value });
|
|
||||||
});
|
const topicOptions: SelectSearchOption[] = topics.map((topic) => ({
|
||||||
}
|
name: topic.name,
|
||||||
|
value: topic.name,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute">
|
<div className="absolute">
|
||||||
@@ -111,7 +129,7 @@ const ShareModal = (props: ShareModal) => {
|
|||||||
options={streamOptions}
|
options={streamOptions}
|
||||||
value={stream}
|
value={stream}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setTopic(undefined);
|
setTopic(undefined); // Reset topic when stream changes
|
||||||
setStream(val.toString());
|
setStream(val.toString());
|
||||||
}}
|
}}
|
||||||
placeholder="Pick a stream"
|
placeholder="Pick a stream"
|
||||||
@@ -119,25 +137,16 @@ const ShareModal = (props: ShareModal) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{stream && (
|
{stream && (
|
||||||
<>
|
|
||||||
<div className="flex items-center mt-4">
|
<div className="flex items-center mt-4">
|
||||||
<span className="mr-2 invisible">#</span>
|
<span className="mr-2 invisible">#</span>
|
||||||
<SelectSearch
|
<SelectSearch
|
||||||
search={true}
|
search={true}
|
||||||
options={
|
options={topicOptions}
|
||||||
streams
|
|
||||||
.find((s) => s.name == stream)
|
|
||||||
?.topics.sort((a: string, b: string) =>
|
|
||||||
a.localeCompare(b),
|
|
||||||
)
|
|
||||||
.map((t) => ({ name: t, value: t })) || []
|
|
||||||
}
|
|
||||||
value={topic}
|
value={topic}
|
||||||
onChange={(val) => setTopic(val.toString())}
|
onChange={(val) => setTopic(val.toString())}
|
||||||
placeholder="Pick a topic"
|
placeholder="Pick a topic"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -828,6 +828,34 @@ export const $SpeakerWords = {
|
|||||||
title: "SpeakerWords",
|
title: "SpeakerWords",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const $Stream = {
|
||||||
|
properties: {
|
||||||
|
stream_id: {
|
||||||
|
type: "integer",
|
||||||
|
title: "Stream Id",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
title: "Name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: "object",
|
||||||
|
required: ["stream_id", "name"],
|
||||||
|
title: "Stream",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const $Topic = {
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
title: "Name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: "object",
|
||||||
|
required: ["name"],
|
||||||
|
title: "Topic",
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const $TranscriptParticipant = {
|
export const $TranscriptParticipant = {
|
||||||
properties: {
|
properties: {
|
||||||
id: {
|
id: {
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ import type {
|
|||||||
V1TranscriptProcessData,
|
V1TranscriptProcessData,
|
||||||
V1TranscriptProcessResponse,
|
V1TranscriptProcessResponse,
|
||||||
V1UserMeResponse,
|
V1UserMeResponse,
|
||||||
|
V1ZulipGetStreamsResponse,
|
||||||
|
V1ZulipGetTopicsData,
|
||||||
|
V1ZulipGetTopicsResponse,
|
||||||
} from "./types.gen";
|
} from "./types.gen";
|
||||||
|
|
||||||
export class DefaultService {
|
export class DefaultService {
|
||||||
@@ -762,4 +765,40 @@ export class DefaultService {
|
|||||||
url: "/v1/me",
|
url: "/v1/me",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zulip Get Streams
|
||||||
|
* Get all Zulip streams.
|
||||||
|
* @returns Stream Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public v1ZulipGetStreams(): CancelablePromise<V1ZulipGetStreamsResponse> {
|
||||||
|
return this.httpRequest.request({
|
||||||
|
method: "GET",
|
||||||
|
url: "/v1/zulip/streams",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zulip Get Topics
|
||||||
|
* Get all topics for a specific Zulip stream.
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.streamId
|
||||||
|
* @returns Topic Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public v1ZulipGetTopics(
|
||||||
|
data: V1ZulipGetTopicsData,
|
||||||
|
): CancelablePromise<V1ZulipGetTopicsResponse> {
|
||||||
|
return this.httpRequest.request({
|
||||||
|
method: "GET",
|
||||||
|
url: "/v1/zulip/streams/{stream_id}/topics",
|
||||||
|
path: {
|
||||||
|
stream_id: data.streamId,
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
422: "Validation Error",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,6 +168,15 @@ export type SpeakerWords = {
|
|||||||
words: Array<Word>;
|
words: Array<Word>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Stream = {
|
||||||
|
stream_id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Topic = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TranscriptParticipant = {
|
export type TranscriptParticipant = {
|
||||||
id?: string;
|
id?: string;
|
||||||
speaker: number | null;
|
speaker: number | null;
|
||||||
@@ -427,6 +436,14 @@ export type V1TranscriptProcessResponse = unknown;
|
|||||||
|
|
||||||
export type V1UserMeResponse = UserInfo | null;
|
export type V1UserMeResponse = UserInfo | null;
|
||||||
|
|
||||||
|
export type V1ZulipGetStreamsResponse = Array<Stream>;
|
||||||
|
|
||||||
|
export type V1ZulipGetTopicsData = {
|
||||||
|
streamId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type V1ZulipGetTopicsResponse = Array<Topic>;
|
||||||
|
|
||||||
export type $OpenApiTs = {
|
export type $OpenApiTs = {
|
||||||
"/metrics": {
|
"/metrics": {
|
||||||
get: {
|
get: {
|
||||||
@@ -850,4 +867,29 @@ export type $OpenApiTs = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
"/v1/zulip/streams": {
|
||||||
|
get: {
|
||||||
|
res: {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: Array<Stream>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"/v1/zulip/streams/{stream_id}/topics": {
|
||||||
|
get: {
|
||||||
|
req: V1ZulipGetTopicsData;
|
||||||
|
res: {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: Array<Topic>;
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HTTPValidationError;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user