Room config

This commit is contained in:
2024-09-03 11:15:32 +02:00
parent 42796d7d3f
commit 5c89a07996
9 changed files with 375 additions and 46 deletions

View File

@@ -0,0 +1,86 @@
"""Add room options
Revision ID: 62dea3db63a5
Revises: 1340c04426b8
Create Date: 2024-09-03 16:19:26.861027
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "62dea3db63a5"
down_revision: Union[str, None] = "1340c04426b8"
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! ###
op.add_column(
"meeting",
sa.Column(
"is_locked", sa.Boolean(), server_default=sa.text("0"), nullable=False
),
)
op.add_column(
"meeting",
sa.Column("room_mode", sa.String(), server_default="normal", nullable=False),
)
op.add_column(
"meeting",
sa.Column(
"recording_type", sa.String(), server_default="cloud", nullable=False
),
)
op.add_column(
"meeting",
sa.Column(
"recording_trigger",
sa.String(),
server_default="automatic-2nd-participant",
nullable=False,
),
)
op.add_column(
"room",
sa.Column(
"is_locked", sa.Boolean(), server_default=sa.text("0"), nullable=False
),
)
op.add_column(
"room",
sa.Column("room_mode", sa.String(), server_default="normal", nullable=False),
)
op.add_column(
"room",
sa.Column(
"recording_type", sa.String(), server_default="cloud", nullable=False
),
)
op.add_column(
"room",
sa.Column(
"recording_trigger",
sa.String(),
server_default="automatic-2nd-participant",
nullable=False,
),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("room", "recording_trigger")
op.drop_column("room", "recording_type")
op.drop_column("room", "room_mode")
op.drop_column("room", "is_locked")
op.drop_column("meeting", "recording_trigger")
op.drop_column("meeting", "recording_type")
op.drop_column("meeting", "room_mode")
op.drop_column("meeting", "is_locked")
# ### end Alembic commands ###

View File

@@ -1,9 +1,12 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Literal
import sqlalchemy import sqlalchemy
from fastapi import HTTPException from fastapi import HTTPException
from pydantic import BaseModel from pydantic import BaseModel
from reflector.db import database, metadata from reflector.db import database, metadata
from reflector.db.rooms import Room
from sqlalchemy.sql import false
meetings = sqlalchemy.Table( meetings = sqlalchemy.Table(
"meeting", "meeting",
@@ -17,6 +20,21 @@ meetings = sqlalchemy.Table(
sqlalchemy.Column("end_date", sqlalchemy.DateTime), sqlalchemy.Column("end_date", sqlalchemy.DateTime),
sqlalchemy.Column("user_id", sqlalchemy.String), sqlalchemy.Column("user_id", sqlalchemy.String),
sqlalchemy.Column("room_id", sqlalchemy.String), sqlalchemy.Column("room_id", sqlalchemy.String),
sqlalchemy.Column(
"is_locked", sqlalchemy.Boolean, nullable=False, server_default=false()
),
sqlalchemy.Column(
"room_mode", sqlalchemy.String, nullable=False, server_default="normal"
),
sqlalchemy.Column(
"recording_type", sqlalchemy.String, nullable=False, server_default="cloud"
),
sqlalchemy.Column(
"recording_trigger",
sqlalchemy.String,
nullable=False,
server_default="automatic-2nd-participant",
),
) )
@@ -30,6 +48,12 @@ class Meeting(BaseModel):
end_date: datetime end_date: datetime
user_id: str | None = None user_id: str | None = None
room_id: str | None = None room_id: str | None = None
is_locked: bool = False
room_mode: Literal["normal", "group"] = "normal"
recording_type: Literal["none", "local", "cloud"] = "cloud"
recording_trigger: Literal[
"none", "prompt", "automatic", "automatic-2nd-participant"
] = "automatic-2nd-participant"
class MeetingController: class MeetingController:
@@ -43,7 +67,7 @@ class MeetingController:
start_date: datetime, start_date: datetime,
end_date: datetime, end_date: datetime,
user_id: str, user_id: str,
room_id: str = None, room: Room,
): ):
""" """
Create a new meeting Create a new meeting
@@ -57,7 +81,11 @@ class MeetingController:
start_date=start_date, start_date=start_date,
end_date=end_date, end_date=end_date,
user_id=user_id, user_id=user_id,
room_id=room_id, room_id=room.id,
is_locked=room.is_locked,
room_mode=room.room_mode,
recording_type=room.recording_type,
recording_trigger=room.recording_trigger,
) )
query = meetings.insert().values(**meeting.model_dump()) query = meetings.insert().values(**meeting.model_dump())
await database.execute(query) await database.execute(query)
@@ -77,15 +105,23 @@ class MeetingController:
return Meeting(**result) return Meeting(**result)
async def get_latest(self, room_id: str) -> Meeting: async def get_latest(self, room: Room) -> Meeting:
""" """
Get latest meeting for a room. Get latest meeting for a room.
""" """
end_date = getattr(meetings.c, "end_date") end_date = getattr(meetings.c, "end_date")
query = ( query = (
meetings.select() meetings.select()
.where(meetings.c.room_id == room_id) .where(
.where(meetings.c.end_date > datetime.now(timezone.utc)) sqlalchemy.and_(
meetings.c.room_id == room.id,
meetings.c.is_locked == room.is_locked,
meetings.c.room_mode == room.room_mode,
meetings.c.recording_type == room.recording_type,
meetings.c.recording_trigger == room.recording_trigger,
meetings.c.end_date > datetime.now(timezone.utc),
)
)
.order_by(end_date.desc()) .order_by(end_date.desc())
) )
result = await database.fetch_one(query) result = await database.fetch_one(query)

View File

@@ -1,4 +1,5 @@
from datetime import datetime from datetime import datetime
from typing import Literal
import sqlalchemy import sqlalchemy
from fastapi import HTTPException from fastapi import HTTPException
@@ -19,6 +20,21 @@ rooms = sqlalchemy.Table(
), ),
sqlalchemy.Column("zulip_stream", sqlalchemy.String), sqlalchemy.Column("zulip_stream", sqlalchemy.String),
sqlalchemy.Column("zulip_topic", sqlalchemy.String), sqlalchemy.Column("zulip_topic", sqlalchemy.String),
sqlalchemy.Column(
"is_locked", sqlalchemy.Boolean, nullable=False, server_default=false()
),
sqlalchemy.Column(
"room_mode", sqlalchemy.String, nullable=False, server_default="normal"
),
sqlalchemy.Column(
"recording_type", sqlalchemy.String, nullable=False, server_default="cloud"
),
sqlalchemy.Column(
"recording_trigger",
sqlalchemy.String,
nullable=False,
server_default="automatic-2nd-participant",
),
) )
@@ -30,6 +46,12 @@ class Room(BaseModel):
zulip_auto_post: bool = False zulip_auto_post: bool = False
zulip_stream: str = "" zulip_stream: str = ""
zulip_topic: str = "" zulip_topic: str = ""
is_locked: bool = False
room_mode: Literal["normal", "group"] = "normal"
recording_type: Literal["none", "local", "cloud"] = "cloud"
recording_trigger: Literal[
"none", "prompt", "automatic", "automatic-2nd-participant"
] = "automatic-2nd-participant"
class RoomController: class RoomController:

View File

@@ -24,6 +24,10 @@ class Room(BaseModel):
zulip_auto_post: bool zulip_auto_post: bool
zulip_stream: str zulip_stream: str
zulip_topic: str zulip_topic: str
is_locked: bool
room_mode: str
recording_type: str
recording_trigger: str
class Meeting(BaseModel): class Meeting(BaseModel):
@@ -41,6 +45,10 @@ class CreateRoom(BaseModel):
zulip_auto_post: bool zulip_auto_post: bool
zulip_stream: str zulip_stream: str
zulip_topic: str zulip_topic: str
is_locked: bool
room_mode: str
recording_type: str
recording_trigger: str
class UpdateRoom(BaseModel): class UpdateRoom(BaseModel):
@@ -48,6 +56,10 @@ class UpdateRoom(BaseModel):
zulip_auto_post: bool zulip_auto_post: bool
zulip_stream: str zulip_stream: str
zulip_topic: str zulip_topic: str
is_locked: bool
room_mode: str
recording_type: str
recording_trigger: str
class DeletionStatus(BaseModel): class DeletionStatus(BaseModel):
@@ -126,11 +138,13 @@ async def rooms_create_meeting(
if not room: if not room:
raise HTTPException(status_code=404, detail="Room not found") raise HTTPException(status_code=404, detail="Room not found")
meeting = await meetings_controller.get_latest(room_id=room.id) meeting = await meetings_controller.get_latest(room=room)
if meeting is None: if meeting is None:
start_date = datetime.now(timezone.utc) start_date = datetime.now(timezone.utc)
end_date = start_date + timedelta(hours=1) end_date = start_date + timedelta(hours=1)
meeting = await create_meeting("", start_date=start_date, end_date=end_date) meeting = await create_meeting(
"", start_date=start_date, end_date=end_date, room=room
)
meeting = await meetings_controller.create( meeting = await meetings_controller.create(
id=meeting["meetingId"], id=meeting["meetingId"],
@@ -141,7 +155,10 @@ async def rooms_create_meeting(
start_date=datetime.fromisoformat(meeting["startDate"]), start_date=datetime.fromisoformat(meeting["startDate"]),
end_date=datetime.fromisoformat(meeting["endDate"]), end_date=datetime.fromisoformat(meeting["endDate"]),
user_id=user_id, user_id=user_id,
room_id=room.id, room=room,
) )
if user_id is None:
meeting.host_room_url = ""
return meeting return meeting

View File

@@ -1,11 +1,12 @@
from datetime import datetime from datetime import datetime
import httpx import httpx
from reflector.db.rooms import Room
from reflector.settings import settings from reflector.settings import settings
async def create_meeting( async def create_meeting(
room_name_prefix: str, start_date: datetime, end_date: datetime room_name_prefix: str, start_date: datetime, end_date: datetime, room: Room
): ):
headers = { headers = {
"Content-Type": "application/json; charset=utf-8", "Content-Type": "application/json; charset=utf-8",
@@ -13,14 +14,14 @@ async def create_meeting(
} }
data = { data = {
"templateType": "viewerMode", "templateType": "viewerMode",
"isLocked": False, "isLocked": room.is_locked,
"roomNamePrefix": room_name_prefix, "roomNamePrefix": room_name_prefix,
"roomNamePattern": "uuid", "roomNamePattern": "uuid",
"roomMode": "normal", "roomMode": room.room_mode,
"startDate": start_date.isoformat(), "startDate": start_date.isoformat(),
"endDate": end_date.isoformat(), "endDate": end_date.isoformat(),
"recording": { "recording": {
"type": "cloud", "type": room.recording_type,
"destination": { "destination": {
"provider": "s3", "provider": "s3",
"bucket": settings.AWS_WHEREBY_S3_BUCKET, "bucket": settings.AWS_WHEREBY_S3_BUCKET,
@@ -28,7 +29,7 @@ async def create_meeting(
"accessKeySecret": settings.AWS_WHEREBY_ACCESS_KEY_SECRET, "accessKeySecret": settings.AWS_WHEREBY_ACCESS_KEY_SECRET,
"fileFormat": "mp4", "fileFormat": "mp4",
}, },
"startTrigger": "automatic-2nd-participant", "startTrigger": room.recording_trigger,
}, },
} }

View File

@@ -32,7 +32,7 @@ import {
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { Container } from "@chakra-ui/react"; import { Container } from "@chakra-ui/react";
import { FaEllipsisVertical, FaTrash, FaPencil } from "react-icons/fa6"; import { FaEllipsisVertical, FaTrash, FaPencil, FaLink } from "react-icons/fa6";
import useApi from "../../lib/useApi"; import useApi from "../../lib/useApi";
import useRoomList from "./useRoomList"; import useRoomList from "./useRoomList";
import { DomainContext } from "../../domainContext"; import { DomainContext } from "../../domainContext";
@@ -51,14 +51,31 @@ interface SelectOption extends OptionBase {
const RESERVED_PATHS = ["browse", "rooms", "transcripts"]; const RESERVED_PATHS = ["browse", "rooms", "transcripts"];
export default function RoomsList() { const roomModeOptions: Options<SelectOption> = [
const { isOpen, onOpen, onClose } = useDisclosure(); { label: "2-4 people", value: "normal" },
const [room, setRoom] = useState({ { label: "2-200 people", value: "group" },
];
const recordingTriggerOptions: Options<SelectOption> = [
{ label: "None", value: "none" },
{ label: "Prompt", value: "prompt" },
{ label: "Automatic", value: "automatic-2nd-participant" },
];
const roomInitialState = {
name: "", name: "",
zulipAutoPost: false, zulipAutoPost: false,
zulipStream: "", zulipStream: "",
zulipTopic: "", zulipTopic: "",
}); isLocked: false,
roomMode: "normal",
recordingType: "cloud",
recordingTrigger: "none",
};
export default function RoomsList() {
const { isOpen, onOpen, onClose } = useDisclosure();
const [room, setRoom] = useState(roomInitialState);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [editRoomId, setEditRoomId] = useState(""); const [editRoomId, setEditRoomId] = useState("");
const api = useApi(); const api = useApi();
@@ -66,6 +83,7 @@ export default function RoomsList() {
const { loading, response, refetch } = useRoomList(page); const { loading, response, refetch } = useRoomList(page);
const [streams, setStreams] = useState<Stream[]>([]); const [streams, setStreams] = useState<Stream[]>([]);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [linkCopied, setLinkCopied] = useState("");
const { zulip_streams } = useContext(DomainContext); const { zulip_streams } = useContext(DomainContext);
@@ -100,6 +118,16 @@ export default function RoomsList() {
.find((stream) => stream.name === room.zulipStream) .find((stream) => stream.name === room.zulipStream)
?.topics.map((topic) => ({ label: topic, value: topic })) || []; ?.topics.map((topic) => ({ label: topic, value: topic })) || [];
const handleCopyUrl = (roomName: string) => {
const roomUrl = `${window.location.origin}/${roomName}`;
navigator.clipboard.writeText(roomUrl);
setLinkCopied(roomName);
setTimeout(() => {
setLinkCopied("");
}, 2000);
};
const handleSaveRoom = async () => { const handleSaveRoom = async () => {
try { try {
if (RESERVED_PATHS.includes(room.name)) { if (RESERVED_PATHS.includes(room.name)) {
@@ -107,32 +135,29 @@ export default function RoomsList() {
return; return;
} }
const roomData = {
name: room.name,
zulip_auto_post: room.zulipAutoPost,
zulip_stream: room.zulipStream,
zulip_topic: room.zulipTopic,
is_locked: room.isLocked,
room_mode: room.roomMode,
recording_type: room.recordingType,
recording_trigger: room.recordingTrigger,
};
if (isEditing) { if (isEditing) {
await api?.v1RoomsUpdate({ await api?.v1RoomsUpdate({
roomId: editRoomId, roomId: editRoomId,
requestBody: { requestBody: roomData,
name: room.name,
zulip_auto_post: room.zulipAutoPost,
zulip_stream: room.zulipStream,
zulip_topic: room.zulipTopic,
},
}); });
} else { } else {
await api?.v1RoomsCreate({ await api?.v1RoomsCreate({
requestBody: { requestBody: roomData,
name: room.name,
zulip_auto_post: room.zulipAutoPost,
zulip_stream: room.zulipStream,
zulip_topic: room.zulipTopic,
},
}); });
} }
setRoom({
name: "", setRoom(roomInitialState);
zulipAutoPost: false,
zulipStream: "",
zulipTopic: "",
});
setIsEditing(false); setIsEditing(false);
setEditRoomId(""); setEditRoomId("");
setError(""); setError("");
@@ -149,6 +174,10 @@ export default function RoomsList() {
zulipAutoPost: roomData.zulip_auto_post, zulipAutoPost: roomData.zulip_auto_post,
zulipStream: roomData.zulip_stream, zulipStream: roomData.zulip_stream,
zulipTopic: roomData.zulip_topic, zulipTopic: roomData.zulip_topic,
isLocked: roomData.is_locked,
roomMode: roomData.room_mode,
recordingType: roomData.recording_type,
recordingTrigger: roomData.recording_trigger,
}); });
setEditRoomId(roomId); setEditRoomId(roomId);
setIsEditing(true); setIsEditing(true);
@@ -204,12 +233,7 @@ export default function RoomsList() {
colorScheme="blue" colorScheme="blue"
onClick={() => { onClick={() => {
setIsEditing(false); setIsEditing(false);
setRoom({ setRoom(roomInitialState);
name: "",
zulipAutoPost: false,
zulipStream: "",
zulipTopic: "",
});
setError(""); setError("");
onOpen(); onOpen();
}} }}
@@ -236,6 +260,53 @@ export default function RoomsList() {
{error && <Text color="red.500">{error}</Text>} {error && <Text color="red.500">{error}</Text>}
</FormControl> </FormControl>
<FormControl mt={4}>
<Checkbox
name="isLocked"
isChecked={room.isLocked}
onChange={handleRoomChange}
>
Locked room
</Checkbox>
</FormControl>
<FormControl mt={4}>
<FormLabel>Room size</FormLabel>
<Select
name="roomMode"
options={roomModeOptions}
value={{
label: roomModeOptions.find(
(rm) => rm.value === room.roomMode,
)?.label,
value: room.roomMode,
}}
onChange={(newValue) =>
setRoom({
...room,
roomMode: newValue!.value,
})
}
/>
</FormControl>
<FormControl mt={4}>
<FormLabel>Recording start trigger</FormLabel>
<Select
name="recordingTrigger"
options={recordingTriggerOptions}
value={{
label: recordingTriggerOptions.find(
(rt) => rt.value === room.recordingTrigger,
)?.label,
value: room.recordingTrigger,
}}
onChange={(newValue) =>
setRoom({
...room,
recordingTrigger: newValue!.value,
})
}
/>
</FormControl>
<FormControl mt={8}> <FormControl mt={8}>
<Checkbox <Checkbox
name="zulipAutoPost" name="zulipAutoPost"
@@ -309,6 +380,19 @@ export default function RoomsList() {
<Link href={`/${roomData.name}`}>{roomData.name}</Link> <Link href={`/${roomData.name}`}>{roomData.name}</Link>
</Heading> </Heading>
<Spacer /> <Spacer />
{linkCopied === roomData.name ? (
<Text mr={2} color="green.500">
Link copied!
</Text>
) : (
<IconButton
aria-label="Copy URL"
icon={<FaLink />}
onClick={() => handleCopyUrl(roomData.name)}
mr={2}
/>
)}
<Menu closeOnSelect={true}> <Menu closeOnSelect={true}>
<MenuButton <MenuButton
as={IconButton} as={IconButton}

View File

@@ -71,9 +71,34 @@ export const $CreateRoom = {
type: "string", type: "string",
title: "Zulip Topic", title: "Zulip Topic",
}, },
is_locked: {
type: "boolean",
title: "Is Locked",
},
room_mode: {
type: "string",
title: "Room Mode",
},
recording_type: {
type: "string",
title: "Recording Type",
},
recording_trigger: {
type: "string",
title: "Recording Trigger",
},
}, },
type: "object", type: "object",
required: ["name", "zulip_auto_post", "zulip_stream", "zulip_topic"], required: [
"name",
"zulip_auto_post",
"zulip_stream",
"zulip_topic",
"is_locked",
"room_mode",
"recording_type",
"recording_trigger",
],
title: "CreateRoom", title: "CreateRoom",
} as const; } as const;
@@ -667,6 +692,22 @@ export const $Room = {
type: "string", type: "string",
title: "Zulip Topic", title: "Zulip Topic",
}, },
is_locked: {
type: "boolean",
title: "Is Locked",
},
room_mode: {
type: "string",
title: "Room Mode",
},
recording_type: {
type: "string",
title: "Recording Type",
},
recording_trigger: {
type: "string",
title: "Recording Trigger",
},
}, },
type: "object", type: "object",
required: [ required: [
@@ -677,6 +718,10 @@ export const $Room = {
"zulip_auto_post", "zulip_auto_post",
"zulip_stream", "zulip_stream",
"zulip_topic", "zulip_topic",
"is_locked",
"room_mode",
"recording_type",
"recording_trigger",
], ],
title: "Room", title: "Room",
} as const; } as const;
@@ -857,9 +902,34 @@ export const $UpdateRoom = {
type: "string", type: "string",
title: "Zulip Topic", title: "Zulip Topic",
}, },
is_locked: {
type: "boolean",
title: "Is Locked",
},
room_mode: {
type: "string",
title: "Room Mode",
},
recording_type: {
type: "string",
title: "Recording Type",
},
recording_trigger: {
type: "string",
title: "Recording Trigger",
},
}, },
type: "object", type: "object",
required: ["name", "zulip_auto_post", "zulip_stream", "zulip_topic"], required: [
"name",
"zulip_auto_post",
"zulip_stream",
"zulip_topic",
"is_locked",
"room_mode",
"recording_type",
"recording_trigger",
],
title: "UpdateRoom", title: "UpdateRoom",
} as const; } as const;

View File

@@ -19,6 +19,10 @@ export type CreateRoom = {
zulip_auto_post: boolean; zulip_auto_post: boolean;
zulip_stream: string; zulip_stream: string;
zulip_topic: string; zulip_topic: string;
is_locked: boolean;
room_mode: string;
recording_type: string;
recording_trigger: string;
}; };
export type CreateTranscript = { export type CreateTranscript = {
@@ -132,6 +136,10 @@ export type Room = {
zulip_auto_post: boolean; zulip_auto_post: boolean;
zulip_stream: string; zulip_stream: string;
zulip_topic: string; zulip_topic: string;
is_locked: boolean;
room_mode: string;
recording_type: string;
recording_trigger: string;
}; };
export type RtcOffer = { export type RtcOffer = {
@@ -176,6 +184,10 @@ export type UpdateRoom = {
zulip_auto_post: boolean; zulip_auto_post: boolean;
zulip_stream: string; zulip_stream: string;
zulip_topic: string; zulip_topic: string;
is_locked: boolean;
room_mode: string;
recording_type: string;
recording_trigger: string;
}; };
export type UpdateTranscript = { export type UpdateTranscript = {

View File

@@ -1,6 +1,7 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
output: "standalone", output: "standalone",
experimental: { esmExternals: "loose" },
}; };
module.exports = nextConfig; module.exports = nextConfig;