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}
+
+
+
+
+
+
+
+ ))
+ ) : (
+
+ 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(