mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 12:19:06 +00:00
Permanent room urls
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
139
server/reflector/db/rooms.py
Normal file
139
server/reflector/db/rooms.py
Normal file
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
107
server/reflector/views/rooms.py
Normal file
107
server/reflector/views/rooms.py
Normal file
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
·
|
||||
<Link
|
||||
href="/rooms"
|
||||
as={NextLink}
|
||||
className="hover:underline focus-within:underline underline-offset-2 decoration-[.5px] font-light px-2"
|
||||
prefetch={false}
|
||||
>
|
||||
Rooms
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
·
|
||||
<About buttonText="About" />
|
||||
{privacy ? (
|
||||
|
||||
33
www/app/[domain]/rooms/[roomName]/page.tsx
Normal file
33
www/app/[domain]/rooms/[roomName]/page.tsx
Normal file
@@ -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<HTMLElement>(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 && (
|
||||
<whereby-embed
|
||||
ref={wherebyRef}
|
||||
room={roomUrl}
|
||||
style={{ width: "100%", height: "98%" }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
171
www/app/[domain]/rooms/page.tsx
Normal file
171
www/app/[domain]/rooms/page.tsx
Normal file
@@ -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<number>(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 (
|
||||
<Flex flexDir="column" align="center" justify="center" h="100%">
|
||||
<Spinner size="xl" />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container maxW={"container.lg"}>
|
||||
<Flex
|
||||
flexDir="row"
|
||||
justify="flex-end"
|
||||
align="center"
|
||||
flexWrap={"wrap-reverse"}
|
||||
mb={2}
|
||||
>
|
||||
<Heading>Rooms</Heading>
|
||||
<Spacer />
|
||||
<Button colorScheme="blue" onClick={onOpen}>
|
||||
Add Room
|
||||
</Button>
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Add Room</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<FormControl>
|
||||
<FormLabel>Room name</FormLabel>
|
||||
<Input
|
||||
placeholder="room-name"
|
||||
value={roomName}
|
||||
onChange={handleRoomNameChange}
|
||||
/>
|
||||
<FormHelperText>Please enter room name</FormHelperText>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant="ghost" mr={3} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button colorScheme="blue" onClick={handleAddRoom}>
|
||||
Add
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</Flex>
|
||||
|
||||
<VStack>
|
||||
{response?.items && response.items.length > 0 ? (
|
||||
response.items.map((room) => (
|
||||
<Card w={"full"}>
|
||||
<CardBody>
|
||||
<Flex align={"center"}>
|
||||
<Heading size="md">
|
||||
<Link
|
||||
// as={NextLink}
|
||||
href={`/rooms/${room.name}`}
|
||||
noOfLines={2}
|
||||
>
|
||||
{room.name}
|
||||
</Link>
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<Menu closeOnSelect={true}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
icon={<FaEllipsisVertical />}
|
||||
aria-label="actions"
|
||||
/>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
onClick={() => handleDeleteRoom(room.id)}
|
||||
icon={<FaTrash color={"red.500"} />}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<Flex flexDir="column" align="center" justify="center" h="100%">
|
||||
<Text>No rooms found</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</VStack>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
47
www/app/[domain]/rooms/useRoomList.tsx
Normal file
47
www/app/[domain]/rooms/useRoomList.tsx
Normal file
@@ -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<Page_Room_ | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setErrorState] = useState<Error | null>(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;
|
||||
70
www/app/[domain]/rooms/useRoomMeeting.tsx
Normal file
70
www/app/[domain]/rooms/useRoomMeeting.tsx
Normal file
@@ -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<GetMeeting | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setErrorState] = useState<Error | null>(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;
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<V1MeetingCreateResponse> {
|
||||
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<V1RoomsListResponse> {
|
||||
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<V1RoomsCreateResponse> {
|
||||
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<V1RoomsDeleteResponse> {
|
||||
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<V1RoomsCreateMeetingResponse> {
|
||||
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.
|
||||
|
||||
@@ -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<Room>;
|
||||
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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user