From 55697e670decdf01ae2d8b8d61eff50204e691e7 Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Fri, 16 Aug 2024 22:26:00 +0200 Subject: [PATCH] Permanent room urls --- .../versions/1340c04426b8_add_meeting.py | 16 +- server/reflector/app.py | 2 + server/reflector/db/__init__.py | 1 + server/reflector/db/meetings.py | 22 ++- server/reflector/db/rooms.py | 139 ++++++++++++++ server/reflector/views/meetings.py | 30 ++- server/reflector/views/rooms.py | 107 +++++++++++ server/reflector/views/transcripts.py | 4 +- server/reflector/worker/process.py | 9 + www/app/[domain]/layout.tsx | 17 +- www/app/[domain]/rooms/[roomName]/page.tsx | 33 ++++ www/app/[domain]/rooms/page.tsx | 171 ++++++++++++++++++ www/app/[domain]/rooms/useRoomList.tsx | 47 +++++ www/app/[domain]/rooms/useRoomMeeting.tsx | 70 +++++++ .../[transcriptId]/meeting/page.tsx | 12 -- www/app/api/schemas.gen.ts | 93 ++++++++++ www/app/api/services.gen.ts | 121 +++++++++++++ www/app/api/types.gen.ts | 129 +++++++++++++ www/config-template.ts | 1 + www/middleware.ts | 8 +- 20 files changed, 1001 insertions(+), 31 deletions(-) create mode 100644 server/reflector/db/rooms.py create mode 100644 server/reflector/views/rooms.py create mode 100644 www/app/[domain]/rooms/[roomName]/page.tsx create mode 100644 www/app/[domain]/rooms/page.tsx create mode 100644 www/app/[domain]/rooms/useRoomList.tsx create mode 100644 www/app/[domain]/rooms/useRoomMeeting.tsx diff --git a/server/migrations/versions/1340c04426b8_add_meeting.py b/server/migrations/versions/1340c04426b8_add_meeting.py index 564bf377..272acaae 100644 --- a/server/migrations/versions/1340c04426b8_add_meeting.py +++ b/server/migrations/versions/1340c04426b8_add_meeting.py @@ -5,26 +5,22 @@ Revises: b9348748bbbc Create Date: 2024-07-31 16:41:29.415218 """ + from typing import Sequence, Union -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. -revision: str = '1340c04426b8' -down_revision: Union[str, None] = 'b9348748bbbc' +revision: str = "1340c04426b8" +down_revision: Union[str, None] = "b9348748bbbc" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### + op.add_column("transcript", sa.Column("meeting_id", sa.String(), nullable=True)) def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### + op.drop_column("transcript", "meeting_id") diff --git a/server/reflector/app.py b/server/reflector/app.py index a8a46c31..84ed2ea8 100644 --- a/server/reflector/app.py +++ b/server/reflector/app.py @@ -12,6 +12,7 @@ from reflector.logger import logger from reflector.metrics import metrics_init from reflector.settings import settings from reflector.views.meetings import router as meetings_router +from reflector.views.rooms import router as rooms_router from reflector.views.rtc_offer import router as rtc_offer_router from reflector.views.transcripts import router as transcripts_router from reflector.views.transcripts_audio import router as transcripts_audio_router @@ -70,6 +71,7 @@ metrics_init(app, instrumentator) # register views app.include_router(rtc_offer_router) app.include_router(meetings_router, prefix="/v1") +app.include_router(rooms_router, prefix="/v1") app.include_router(transcripts_router, prefix="/v1") app.include_router(transcripts_audio_router, prefix="/v1") app.include_router(transcripts_participants_router, prefix="/v1") diff --git a/server/reflector/db/__init__.py b/server/reflector/db/__init__.py index 387bf58b..3378d0c0 100644 --- a/server/reflector/db/__init__.py +++ b/server/reflector/db/__init__.py @@ -8,6 +8,7 @@ metadata = sqlalchemy.MetaData() # import models import reflector.db.meetings # noqa +import reflector.db.rooms # noqa import reflector.db.transcripts # noqa engine = sqlalchemy.create_engine( diff --git a/server/reflector/db/meetings.py b/server/reflector/db/meetings.py index cd32a63c..ad0c6397 100644 --- a/server/reflector/db/meetings.py +++ b/server/reflector/db/meetings.py @@ -16,6 +16,7 @@ meetings = sqlalchemy.Table( sqlalchemy.Column("start_date", sqlalchemy.DateTime), sqlalchemy.Column("end_date", sqlalchemy.DateTime), sqlalchemy.Column("user_id", sqlalchemy.String), + sqlalchemy.Column("room_id", sqlalchemy.String), ) @@ -28,10 +29,11 @@ class Meeting(BaseModel): start_date: datetime end_date: datetime user_id: str + room_id: str | None = None class MeetingController: - async def add( + async def create( self, id: str, room_name: str, @@ -41,9 +43,10 @@ class MeetingController: start_date: datetime, end_date: datetime, user_id: str, + room_id: str = None, ): """ - Add a new meeting + Create a new meeting """ meeting = Meeting( id=id, @@ -54,6 +57,7 @@ class MeetingController: start_date=start_date, end_date=end_date, user_id=user_id, + room_id=room_id, ) query = meetings.insert().values(**meeting.model_dump()) await database.execute(query) @@ -73,6 +77,20 @@ class MeetingController: return Meeting(**result) + async def get_latest(self, room_id: str) -> Meeting: + """ + Get latest meeting for a room. + """ + start_date = getattr(meetings.c, "start_date").desc() + query = ( + meetings.select().where(meetings.c.room_id == room_id).order_by(start_date) + ) + result = await database.fetch_one(query) + if not result: + return None + + return Meeting(**result) + async def get_by_id_for_http(self, meeting_id: str, user_id: str | None) -> Meeting: """ Get a meeting by ID for HTTP request. diff --git a/server/reflector/db/rooms.py b/server/reflector/db/rooms.py new file mode 100644 index 00000000..bd5386e8 --- /dev/null +++ b/server/reflector/db/rooms.py @@ -0,0 +1,139 @@ +from datetime import datetime + +import sqlalchemy +from fastapi import HTTPException +from pydantic import BaseModel, ConfigDict, Field +from reflector.db import database, metadata +from reflector.db.transcripts import generate_uuid4 +from sqlalchemy.sql import false + +rooms = sqlalchemy.Table( + "room", + metadata, + sqlalchemy.Column("id", sqlalchemy.String, primary_key=True), + sqlalchemy.Column("name", sqlalchemy.String, nullable=False), + sqlalchemy.Column("user_id", sqlalchemy.String, nullable=False), + sqlalchemy.Column("created_at", sqlalchemy.DateTime, nullable=False), + sqlalchemy.Column( + "zulip_auto_post", sqlalchemy.Boolean, nullable=False, server_default=false() + ), + sqlalchemy.Column("zulip_stream", sqlalchemy.String), + sqlalchemy.Column("zulip_topic", sqlalchemy.String), +) + + +class Room(BaseModel): + id: str = Field(default_factory=generate_uuid4) + name: str + user_id: str + created_at: datetime = Field(default_factory=datetime.utcnow) + zulip_auto_post: bool = False + zulip_stream: str = "" + zulip_topic: str = "" + + +class RoomController: + async def get_all( + self, + user_id: str | None = None, + order_by: str | None = None, + return_query: bool = False, + ) -> list[Room]: + """ + Get all rooms + + If `user_id` is specified, only return rooms that belong to the user. + Otherwise, return all rooms. + + Parameters: + - `order_by`: field to order by, e.g. "-created_at" + """ + query = rooms.select() + if user_id is not None: + query = query.where(rooms.c.user_id == user_id) + + if order_by is not None: + field = getattr(rooms.c, order_by[1:]) + if order_by.startswith("-"): + field = field.desc() + query = query.order_by(field) + + if return_query: + return query + + results = await database.fetch_all(query) + return results + + async def add( + self, + name: str, + user_id: str, + ): + """ + Add a new room + """ + room = Room( + name=name, + user_id=user_id, + ) + query = rooms.insert().values(**room.model_dump()) + await database.execute(query) + return room + + async def get_by_id(self, room_id: str, **kwargs) -> Room | None: + """ + Get a room by id + """ + query = rooms.select().where(rooms.c.id == room_id) + if "user_id" in kwargs: + query = query.where(rooms.c.user_id == kwargs["user_id"]) + result = await database.fetch_one(query) + if not result: + return None + return Room(**result) + + async def get_by_name(self, room_name: str, **kwargs) -> Room | None: + """ + Get a room by name + """ + query = rooms.select().where(rooms.c.name == room_name) + if "user_id" in kwargs: + query = query.where(rooms.c.user_id == kwargs["user_id"]) + result = await database.fetch_one(query) + if not result: + return None + return Room(**result) + + async def get_by_id_for_http(self, meeting_id: str, user_id: str | None) -> Room: + """ + Get a room by ID for HTTP request. + + If not found, it will raise a 404 error. + """ + query = rooms.select().where(rooms.c.id == meeting_id) + result = await database.fetch_one(query) + if not result: + raise HTTPException(status_code=404, detail="Room not found") + + room = Room(**result) + + return room + + async def remove_by_id( + self, + room_id: str, + user_id: str | None = None, + ) -> None: + """ + Remove a room by id + """ + room = await self.get_by_id(room_id, user_id=user_id) + if not room: + return + if user_id is not None and room.user_id != user_id: + return + query = rooms.delete().where(rooms.c.id == room_id) + await database.execute(query) + + +rooms_controller = RoomController() diff --git a/server/reflector/views/meetings.py b/server/reflector/views/meetings.py index 229c8466..3ebbb249 100644 --- a/server/reflector/views/meetings.py +++ b/server/reflector/views/meetings.py @@ -1,10 +1,11 @@ -from datetime import datetime +from datetime import datetime, timedelta, timezone from typing import Annotated, Optional import reflector.auth as auth from fastapi import APIRouter, Depends from pydantic import BaseModel from reflector.db.meetings import meetings_controller +from reflector.whereby import create_meeting router = APIRouter() @@ -26,3 +27,30 @@ async def meeting_get( ): user_id = user["sub"] if user else None return await meetings_controller.get_by_id_for_http(meeting_id, user_id=user_id) + + +@router.post("/meetings/", response_model=GetMeeting) +async def meeting_create( + room_id: str, + user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], +): + user_id = user["sub"] if user else None + meeting = await meetings_controller.get_latest(room_id) + if meeting is None: + start_date = datetime.now(timezone.utc) + end_date = start_date + timedelta(minutes=1) + meeting = await create_meeting("", start_date=start_date, end_date=end_date) + + meeting = await meetings_controller.add( + id=meeting["meetingId"], + room_name=meeting["roomName"], + room_url=meeting["roomUrl"], + host_room_url=meeting["hostRoomUrl"], + viewer_room_url=meeting["viewerRoomUrl"], + start_date=datetime.fromisoformat(meeting["startDate"]), + end_date=datetime.fromisoformat(meeting["endDate"]), + user_id=user_id, + room_id=room_id, + ) + + return await meetings_controller.get_by_id_for_http(meeting.id, user_id=user_id) diff --git a/server/reflector/views/rooms.py b/server/reflector/views/rooms.py new file mode 100644 index 00000000..aeb9ff3f --- /dev/null +++ b/server/reflector/views/rooms.py @@ -0,0 +1,107 @@ +from datetime import datetime, timedelta, timezone +from http.client import HTTPException +from typing import Annotated, Optional + +import reflector.auth as auth +from fastapi import APIRouter, Depends +from fastapi_pagination import Page +from fastapi_pagination.ext.databases import paginate +from pydantic import BaseModel, Field +from reflector.db import database +from reflector.db.meetings import meetings_controller +from reflector.db.rooms import rooms_controller +from reflector.settings import settings +from reflector.views.meetings import GetMeeting +from reflector.whereby import create_meeting + +router = APIRouter() + + +class Room(BaseModel): + id: str + name: str + user_id: str + created_at: datetime + + +class CreateRoom(BaseModel): + name: str + + +class DeletionStatus(BaseModel): + status: str + + +@router.get("/rooms", response_model=Page[Room]) +async def rooms_list( + user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], +) -> list[Room]: + user_id = user["sub"] if user else None + + if not user and not settings.PUBLIC_MODE: + raise HTTPException(status_code=401, detail="Not authenticated") + + user_id = user["sub"] if user else None + return await paginate( + database, + await rooms_controller.get_all( + user_id=user_id, order_by="-created_at", return_query=True + ), + ) + + +@router.post("/rooms", response_model=Room) +async def rooms_create( + room: CreateRoom, + user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], +): + user_id = user["sub"] if user else None + + return await rooms_controller.add( + name=room.name, + user_id=user_id, + ) + + +@router.delete("/rooms/{room_id}", response_model=DeletionStatus) +async def rooms_delete( + room_id: str, + user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], +): + user_id = user["sub"] if user else None + room = await rooms_controller.get_by_id(room_id, user_id=user_id) + if not room: + raise HTTPException(status_code=404, detail="Room not found") + await rooms_controller.remove_by_id(room.id, user_id=user_id) + return DeletionStatus(status="ok") + + +@router.post("/rooms/{room_name}/meeting", response_model=GetMeeting) +async def rooms_create_meeting( + room_name: str, + user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], +): + user_id = user["sub"] if user else None + room = await rooms_controller.get_by_name(room_name) + if not room: + raise HTTPException(status_code=404, detail="Room not found") + + meeting = await meetings_controller.get_latest(room_id=room.id) + if meeting is None: + start_date = datetime.now(timezone.utc) + end_date = start_date + timedelta(minutes=1) + meeting = await create_meeting("", start_date=start_date, end_date=end_date) + + meeting = await meetings_controller.create( + id=meeting["meetingId"], + room_name=meeting["roomName"], + room_url=meeting["roomUrl"], + host_room_url=meeting["hostRoomUrl"], + viewer_room_url=meeting["viewerRoomUrl"], + start_date=datetime.fromisoformat(meeting["startDate"]), + end_date=datetime.fromisoformat(meeting["endDate"]), + user_id=user_id, + room_id=room.id, + ) + + return meeting diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py index bde06087..e7d373bc 100644 --- a/server/reflector/views/transcripts.py +++ b/server/reflector/views/transcripts.py @@ -121,7 +121,7 @@ async def transcripts_create_meeting( end_date = start_date + timedelta(minutes=1) meeting = await create_meeting("", start_date=start_date, end_date=end_date) - meeting = await meetings_controller.add( + meeting = await meetings_controller.create( id=meeting["meetingId"], room_name=meeting["roomName"], room_url=meeting["roomUrl"], @@ -133,7 +133,7 @@ async def transcripts_create_meeting( ) return await transcripts_controller.add( - info.name, + "", source_language=info.source_language, target_language=info.target_language, user_id=user_id, diff --git a/server/reflector/worker/process.py b/server/reflector/worker/process.py index b988a10f..7541b6cd 100644 --- a/server/reflector/worker/process.py +++ b/server/reflector/worker/process.py @@ -65,6 +65,15 @@ async def process_recording(bucket_name: str, object_key: str): room_name = f"/{object_key[:36]}" meeting = await meetings_controller.get_by_room_name(room_name) transcript = await transcripts_controller.get_by_meeting_id(meeting.id) + if transcript is None: + transcript = await transcripts_controller.add( + "", + source_language="en", + target_language="en", + user_id=meeting.user_id, + meeting_id=meeting.id, + share_mode="public", + ) _, extension = os.path.splitext(object_key) upload_filename = transcript.data_path / f"upload{extension}" diff --git a/www/app/[domain]/layout.tsx b/www/app/[domain]/layout.tsx index 95d0f544..f4145d32 100644 --- a/www/app/[domain]/layout.tsx +++ b/www/app/[domain]/layout.tsx @@ -76,7 +76,7 @@ type LayoutProps = { export default async function RootLayout({ children, params }: LayoutProps) { const config = await getConfig(params.domain); - const { requireLogin, privacy, browse } = config.features; + const { requireLogin, privacy, browse, rooms } = config.features; const hasAuthCookie = !!cookies().get(SESSION_COOKIE_NAME); return ( @@ -154,6 +154,21 @@ export default async function RootLayout({ children, params }: LayoutProps) { ) : ( <> )} + {rooms ? ( + <> +  ·  + + Rooms + + + ) : ( + <> + )}  ·  {privacy ? ( diff --git a/www/app/[domain]/rooms/[roomName]/page.tsx b/www/app/[domain]/rooms/[roomName]/page.tsx new file mode 100644 index 00000000..3d16e532 --- /dev/null +++ b/www/app/[domain]/rooms/[roomName]/page.tsx @@ -0,0 +1,33 @@ +"use client"; + +import "@whereby.com/browser-sdk/embed"; +import { useCallback, useEffect, useRef } from "react"; +import useRoomMeeting from "../../rooms/useRoomMeeting"; + +export type RoomDetails = { + params: { + roomName: string; + }; +}; + +export default function Room(details: RoomDetails) { + const wherebyRef = useRef(null); + const roomName = details.params.roomName; + const meeting = useRoomMeeting(roomName); + + const roomUrl = meeting?.response?.host_room_url + ? meeting?.response?.host_room_url + : meeting?.response?.room_url; + + return ( + <> + {roomUrl && ( + + )} + + ); +} diff --git a/www/app/[domain]/rooms/page.tsx b/www/app/[domain]/rooms/page.tsx new file mode 100644 index 00000000..dc6c61ba --- /dev/null +++ b/www/app/[domain]/rooms/page.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { + Box, + Button, + Card, + CardBody, + Flex, + FormControl, + FormHelperText, + FormLabel, + Grid, + Heading, + Input, + Link, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Spacer, + Spinner, + useDisclosure, + VStack, + Text, + Menu, + MenuButton, + MenuList, + MenuItem, + AlertDialog, + IconButton, +} from "@chakra-ui/react"; +import NextLink from "next"; +import React, { ReactNode, useState } from "react"; +import { Container } from "@chakra-ui/react"; +import { PlusSquareIcon } from "@chakra-ui/icons"; +import useApi from "../../lib/useApi"; +import useRoomList from "./useRoomList"; +import { FaEllipsisVertical, FaTrash } from "react-icons/fa6"; +import next from "next"; + +export default function RoomsList() { + const { isOpen, onOpen, onClose } = useDisclosure(); + const [roomName, setRoomName] = useState(""); + const api = useApi(); + const [page, setPage] = useState(1); + const { loading, response, refetch } = useRoomList(page); + + const handleAddRoom = async () => { + try { + const response = await api?.v1RoomsCreate({ + requestBody: { name: roomName }, + }); + setRoomName(""); + refetch(); + } catch (err) {} + onClose(); + }; + + const handleDeleteRoom = async (roomId: string) => { + try { + const response = await api?.v1RoomsDelete({ + roomId, + }); + refetch(); + } catch (err) {} + }; + + const handleRoomNameChange = (e) => { + setRoomName(e.target.value); + }; + + if (loading && !response) + return ( + + + + ); + + return ( + <> + + + Rooms + + + + + + Add Room + + + + Room name + + Please enter room name + + + + + + + + + + + + + + {response?.items && response.items.length > 0 ? ( + response.items.map((room) => ( + + + + + + {room.name} + + + + + } + aria-label="actions" + /> + + handleDeleteRoom(room.id)} + icon={} + > + Delete + + + + + + + )) + ) : ( + + No rooms found + + )} + + + + ); +} diff --git a/www/app/[domain]/rooms/useRoomList.tsx b/www/app/[domain]/rooms/useRoomList.tsx new file mode 100644 index 00000000..d0aad727 --- /dev/null +++ b/www/app/[domain]/rooms/useRoomList.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from "react"; +import { useError } from "../../(errors)/errorContext"; +import useApi from "../../lib/useApi"; +import { Page_Room_ } from "../../api"; + +type RoomList = { + response: Page_Room_ | null; + loading: boolean; + error: Error | null; + refetch: () => void; +}; + +//always protected +const useRoomList = (page: number): RoomList => { + const [response, setResponse] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setErrorState] = useState(null); + const { setError } = useError(); + const api = useApi(); + const [refetchCount, setRefetchCount] = useState(0); + + const refetch = () => { + setLoading(true); + setRefetchCount(refetchCount + 1); + }; + + useEffect(() => { + if (!api) return; + setLoading(true); + api + .v1RoomsList({ page }) + .then((response) => { + setResponse(response); + setLoading(false); + }) + .catch((err) => { + setResponse(null); + setLoading(false); + setError(err); + setErrorState(err); + }); + }, [!api, page, refetchCount]); + + return { response, loading, error, refetch }; +}; + +export default useRoomList; diff --git a/www/app/[domain]/rooms/useRoomMeeting.tsx b/www/app/[domain]/rooms/useRoomMeeting.tsx new file mode 100644 index 00000000..1d625d25 --- /dev/null +++ b/www/app/[domain]/rooms/useRoomMeeting.tsx @@ -0,0 +1,70 @@ +import { useEffect, useState } from "react"; +import { useError } from "../../(errors)/errorContext"; +import { GetMeeting } from "../../api"; +import { shouldShowError } from "../../lib/errorUtils"; +import useApi from "../../lib/useApi"; + +type ErrorMeeting = { + error: Error; + loading: false; + response: null; + reload: () => void; +}; + +type LoadingMeeting = { + response: null; + loading: true; + error: false; + reload: () => void; +}; + +type SuccessMeeting = { + response: GetMeeting; + loading: false; + error: null; + reload: () => void; +}; + +const useRoomMeeting = ( + roomName: string | null | undefined, +): ErrorMeeting | LoadingMeeting | SuccessMeeting => { + const [response, setResponse] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setErrorState] = useState(null); + const [reload, setReload] = useState(0); + const { setError } = useError(); + const api = useApi(); + const reloadHandler = () => setReload((prev) => prev + 1); + + useEffect(() => { + if (!roomName || !api) return; + + if (!response) { + setLoading(true); + } + + api + .v1RoomsCreateMeeting({ roomName }) + .then((result) => { + setResponse(result); + setLoading(false); + console.debug("Meeting Loaded:", result); + }) + .catch((error) => { + const shouldShowHuman = shouldShowError(error); + if (shouldShowHuman) { + setError(error, "There was an error loading the meeting"); + } else { + setError(error); + } + setErrorState(error); + }); + }, [roomName, !api, reload]); + + return { response, loading, error, reload: reloadHandler } as + | ErrorMeeting + | LoadingMeeting + | SuccessMeeting; +}; + +export default useRoomMeeting; diff --git a/www/app/[domain]/transcripts/[transcriptId]/meeting/page.tsx b/www/app/[domain]/transcripts/[transcriptId]/meeting/page.tsx index 2aaf7974..6dde62f1 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/meeting/page.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/meeting/page.tsx @@ -20,18 +20,6 @@ export default function TranscriptMeeting(details: TranscriptDetails) { ? meeting?.response?.host_room_url : meeting?.response?.room_url; - const handleLeave = useCallback((event) => { - console.log("LEFT", event); - }, []); - - useEffect(() => { - wherebyRef.current?.addEventListener("leave", handleLeave); - - return () => { - wherebyRef.current?.removeEventListener("leave", handleLeave); - }; - }, [handleLeave]); - return ( <> {roomUrl && ( diff --git a/www/app/api/schemas.gen.ts b/www/app/api/schemas.gen.ts index 49816f40..513ffbec 100644 --- a/www/app/api/schemas.gen.ts +++ b/www/app/api/schemas.gen.ts @@ -53,6 +53,18 @@ export const $CreateParticipant = { title: "CreateParticipant", } as const; +export const $CreateRoom = { + properties: { + name: { + type: "string", + title: "Name", + }, + }, + type: "object", + required: ["name"], + title: "CreateRoom", +} as const; + export const $CreateTranscript = { properties: { name: { @@ -529,6 +541,62 @@ export const $Page_GetTranscript_ = { title: "Page[GetTranscript]", } as const; +export const $Page_Room_ = { + properties: { + items: { + items: { + $ref: "#/components/schemas/Room", + }, + type: "array", + title: "Items", + }, + total: { + type: "integer", + minimum: 0, + title: "Total", + }, + page: { + anyOf: [ + { + type: "integer", + minimum: 1, + }, + { + type: "null", + }, + ], + title: "Page", + }, + size: { + anyOf: [ + { + type: "integer", + minimum: 1, + }, + { + type: "null", + }, + ], + title: "Size", + }, + pages: { + anyOf: [ + { + type: "integer", + minimum: 0, + }, + { + type: "null", + }, + ], + title: "Pages", + }, + }, + type: "object", + required: ["items", "total", "page", "size"], + title: "Page[Room]", +} as const; + export const $Participant = { properties: { id: { @@ -556,6 +624,31 @@ export const $Participant = { title: "Participant", } as const; +export const $Room = { + properties: { + id: { + type: "string", + title: "Id", + }, + name: { + type: "string", + title: "Name", + }, + user_id: { + type: "string", + title: "User Id", + }, + created_at: { + type: "string", + format: "date-time", + title: "Created At", + }, + }, + type: "object", + required: ["id", "name", "user_id", "created_at"], + title: "Room", +} as const; + export const $RtcOffer = { properties: { sdp: { diff --git a/www/app/api/services.gen.ts b/www/app/api/services.gen.ts index 76a57b80..91eeb7bd 100644 --- a/www/app/api/services.gen.ts +++ b/www/app/api/services.gen.ts @@ -6,6 +6,16 @@ import type { MetricsResponse, V1MeetingGetData, V1MeetingGetResponse, + V1MeetingCreateData, + V1MeetingCreateResponse, + V1RoomsListData, + V1RoomsListResponse, + V1RoomsCreateData, + V1RoomsCreateResponse, + V1RoomsDeleteData, + V1RoomsDeleteResponse, + V1RoomsCreateMeetingData, + V1RoomsCreateMeetingResponse, V1TranscriptsListData, V1TranscriptsListResponse, V1TranscriptsCreateData, @@ -93,6 +103,117 @@ export class DefaultService { }); } + /** + * Meeting Create + * @param data The data for the request. + * @param data.roomId + * @returns GetMeeting Successful Response + * @throws ApiError + */ + public v1MeetingCreate( + data: V1MeetingCreateData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/meetings/", + query: { + room_id: data.roomId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms List + * @param data The data for the request. + * @param data.page Page number + * @param data.size Page size + * @returns Page_Room_ Successful Response + * @throws ApiError + */ + public v1RoomsList( + data: V1RoomsListData = {}, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/rooms", + query: { + page: data.page, + size: data.size, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms Create + * @param data The data for the request. + * @param data.requestBody + * @returns Room Successful Response + * @throws ApiError + */ + public v1RoomsCreate( + data: V1RoomsCreateData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/rooms", + body: data.requestBody, + mediaType: "application/json", + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms Delete + * @param data The data for the request. + * @param data.roomId + * @returns DeletionStatus Successful Response + * @throws ApiError + */ + public v1RoomsDelete( + data: V1RoomsDeleteData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "DELETE", + url: "/v1/rooms/{room_id}", + path: { + room_id: data.roomId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms Create Meeting + * @param data The data for the request. + * @param data.roomName + * @returns GetMeeting Successful Response + * @throws ApiError + */ + public v1RoomsCreateMeeting( + data: V1RoomsCreateMeetingData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/rooms/{room_name}/meeting", + path: { + room_name: data.roomName, + }, + errors: { + 422: "Validation Error", + }, + }); + } + /** * Transcripts List * @param data The data for the request. diff --git a/www/app/api/types.gen.ts b/www/app/api/types.gen.ts index a726745c..e0fd3f78 100644 --- a/www/app/api/types.gen.ts +++ b/www/app/api/types.gen.ts @@ -14,6 +14,10 @@ export type CreateParticipant = { name: string; }; +export type CreateRoom = { + name: string; +}; + export type CreateTranscript = { name: string; source_language?: string; @@ -103,12 +107,27 @@ export type Page_GetTranscript_ = { pages?: number | null; }; +export type Page_Room_ = { + items: Array; + total: number; + page: number | null; + size: number | null; + pages?: number | null; +}; + export type Participant = { id: string; speaker: number | null; name: string; }; +export type Room = { + id: string; + name: string; + user_id: string; + created_at: string; +}; + export type RtcOffer = { sdp: string; type: string; @@ -184,6 +203,43 @@ export type V1MeetingGetData = { export type V1MeetingGetResponse = GetMeeting; +export type V1MeetingCreateData = { + roomId: string; +}; + +export type V1MeetingCreateResponse = GetMeeting; + +export type V1RoomsListData = { + /** + * Page number + */ + page?: number; + /** + * Page size + */ + size?: number; +}; + +export type V1RoomsListResponse = Page_Room_; + +export type V1RoomsCreateData = { + requestBody: CreateRoom; +}; + +export type V1RoomsCreateResponse = Room; + +export type V1RoomsDeleteData = { + roomId: string; +}; + +export type V1RoomsDeleteResponse = DeletionStatus; + +export type V1RoomsCreateMeetingData = { + roomName: string; +}; + +export type V1RoomsCreateMeetingResponse = GetMeeting; + export type V1TranscriptsListData = { /** * Page number @@ -374,6 +430,79 @@ export type $OpenApiTs = { }; }; }; + "/v1/meetings/": { + post: { + req: V1MeetingCreateData; + res: { + /** + * Successful Response + */ + 200: GetMeeting; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/rooms": { + get: { + req: V1RoomsListData; + res: { + /** + * Successful Response + */ + 200: Page_Room_; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + post: { + req: V1RoomsCreateData; + res: { + /** + * Successful Response + */ + 200: Room; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/rooms/{room_id}": { + delete: { + req: V1RoomsDeleteData; + res: { + /** + * Successful Response + */ + 200: DeletionStatus; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/rooms/{room_name}/meeting": { + post: { + req: V1RoomsCreateMeetingData; + res: { + /** + * Successful Response + */ + 200: GetMeeting; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; "/v1/transcripts": { get: { req: V1TranscriptsListData; diff --git a/www/config-template.ts b/www/config-template.ts index 6f0cf194..e8d4c01c 100644 --- a/www/config-template.ts +++ b/www/config-template.ts @@ -4,6 +4,7 @@ export const localConfig = { privacy: true, browse: true, sendToZulip: true, + rooms: true, }, api_url: "http://127.0.0.1:1250", websocket_url: "ws://127.0.0.1:1250", diff --git a/www/middleware.ts b/www/middleware.ts index 4bf13ed0..59416982 100644 --- a/www/middleware.ts +++ b/www/middleware.ts @@ -14,8 +14,9 @@ export async function middleware(request: NextRequest) { ) { // Feature-flag protedted paths if ( - !config.features.browse && - request.nextUrl.pathname.startsWith("/browse") + (!config.features.browse && + request.nextUrl.pathname.startsWith("/browse")) || + (!config.features.rooms && request.nextUrl.pathname.startsWith("/rooms")) ) { return NextResponse.redirect(request.nextUrl.origin); } @@ -27,7 +28,8 @@ export async function middleware(request: NextRequest) { if ( request.nextUrl.pathname == "/" || request.nextUrl.pathname.startsWith("/transcripts") || - request.nextUrl.pathname.startsWith("/browse") + request.nextUrl.pathname.startsWith("/browse") || + request.nextUrl.pathname.startsWith("/rooms") ) { if (!fiefResponse.headers.get("x-middleware-rewrite")) { fiefResponse.headers.set(