Permanent room urls

This commit is contained in:
2024-08-16 22:26:00 +02:00
parent ad84e4626c
commit 55697e670d
20 changed files with 1001 additions and 31 deletions

View File

@@ -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")

View File

@@ -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")

View File

@@ -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(

View File

@@ -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.

View 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()

View File

@@ -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)

View 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

View File

@@ -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,

View File

@@ -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}"