feat(meetings): per-member meeting history with status, transcript and delete - #13
Open
Rahulkaushik01 wants to merge 1 commit into
Open
Conversation
…d delete One history covering both meeting bots and local recordings, scoped to the member named by the verified X-User-Token. * GET /meetings lists a member's meetings newest-first. Deleted meetings are excluded (delete_data keeps the row, so they would otherwise resurface as blank entries) and so are Zoom RTMS app sessions, which are neither a bot nor a local recording. An unknown ?source= is a 400 rather than silently returning everything. * Ownership is applied as a FILTER, never a check afterwards, so a meeting that isn't yours is indistinguishable from one that doesn't exist -- a 404 never confirms it is real. The member id is resolved before any queryset is built, because filter(owner_user_id=None) compiles to IS NULL and would return every unowned meeting in the project. * Listing costs a fixed number of queries however long the page is. BotSerializer exposes events/state/transcription_state as method fields that each hit the database per row, which turns one page into dozens of queries; this uses a lean serializer plus prefetch and annotated counts instead. Proven by test: 1 meeting and 25 meetings both take 3 queries. * Pagination is newest-first with the row id as a tiebreak. The existing BotCursorPagination sorts ascending, which would open history on the oldest meeting, and without a unique tiebreak rows sharing a created_at can be skipped or repeated across page boundaries. * DELETE routes on state: a meeting that hasn't run yet is CANCELLED (the row is removed), a finished one has its data deleted (row kept, contents wiped, Redis state cleared for local sessions), an already-deleted one returns 204 so a double-click or a retry cannot raise, and anything still running returns 409 telling the member to stop it first. * Reads are throttled per MEMBER, not per project: every desktop install shares one project API key, so a per-project limit would let one runaway client exhaust the budget for everyone. Co-Authored-By: Claude <noreply@anthropic.com>
Rahulkaushik01
changed the base branch from
feat/meeting-ownership-and-status
to
dev
July 22, 2026 04:41
Rahulkaushik01
changed the base branch from
dev
to
feat/meeting-ownership-and-status
July 22, 2026 04:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The read side of per-user history: one API that lists, inspects, transcribes and deletes a
member's meetings — covering both meeting bots and local recordings, so the desktop shows a
single history regardless of how a meeting was captured. This is what makes history work across
devices instead of living in a local file on one machine.
Split by purpose rather than by session type:
/local_sessions/*controls a recording while ithappens (start, upload, pause, stop);
/meetings/*is the read side (history, status,transcript, delete). Keeping one transcript implementation behind both avoids the two paths
drifting apart.
The endpoints
GET /meetings?source=bot|localThe member's meetings, newest first, 25 per page (cursor-paginated).
Excludes deleted meetings and Zoom RTMS app sessions. An unknown
sourceis a 400 ratherthan silently returning everything.
GET /meetings/{id}One meeting plus its status:
recording · paused · finalizing · processing · done · partially_failed · deleted(via PR 5's mapping of the 19 internal states).GET /meetings/{id}/transcriptOwner-scoped transcript for bot and local meetings alike.
/local_sessions/{id}/transcriptremains as an alias onto the same code so the desktop keeps working during migration.
DELETE /meetings/{id}Routes on state — see below.
Problems solved, and why these choices
Ownership is a filter, never a check afterwards
A meeting that isn't yours is indistinguishable from one that doesn't exist. Returning 404
(not 403) means the API never confirms another member's meeting is real.
The member id is resolved before any queryset is built
This is the one that would have been a genuine data leak. Verified behaviour: a filter built with
an empty owner id compiles to
WHERE owner_user_id IS NULL.That does not return nothing — it returns every unowned meeting in the project, i.e. all
historic bot meetings belonging to everyone. One missing value silently turns a personal query
into a company-wide one. The id is therefore resolved, and the request rejected with a 401,
before a queryset exists.
Listing costs a fixed number of queries, however long the page
BotSerializerexposesevents,state,transcription_stateandrecording_stateas methodfields, each of which hits the database per row — fine for one bot, but on a 25-row page that
is the classic N+1. This uses a lean serializer plus
prefetch_relatedand annotated counts.Proven, not assumed — the test asserts the query count does not grow with rows:
1 meeting → 3 queries; 25 meetings → 3 queries.
Kept in its own module because
bots/serializers.pyis already ~2,400 lines.Pagination is newest-first, with a tiebreak
The existing
BotCursorPaginationorders bycreated_atascending — verified with a livetest (meetings dated 17/18/19 July came back in that order), which would open a member's history
on their oldest meeting. This sorts by
-created_at, -id. The-idmatters as much as thedirection: without a unique tiebreak, meetings sharing a
created_atcan be skipped orrepeated across page boundaries.
Deleted meetings are excluded
delete_data()wipes contents but keeps the row (marked deleted). Without an explicitexclusion they resurface in history as blank entries.
Delete routes on state — three genuinely different operations
Verified:
DATA_DELETEDis only reachable fromENDEDorFATAL_ERROR.delete_bot()delete_data()Without the first branch, deleting a scheduled meeting would answer "stop the recording first"
for something that hasn't happened. Without the third, a double-click would raise out of
delete_data()and surface as a 500.delete_bot()was deliberately not used as thegeneral delete: it only accepts
SCHEDULEDbots and removes the row entirely, which iscancellation, not history deletion.
Reads are throttled per MEMBER, not per project
Every desktop install shares one project API key, so the existing per-project throttle would
let a single runaway client exhaust the budget for everyone. Keying on the team.day user id
confines a misbehaving client to itself. (The existing
ProjectPostThrottlealso returns earlyfor anything that isn't a POST, so these GET endpoints would otherwise have been entirely
unthrottled.)
Changes made
bots/meetings_api_views.py(new)bots/meetings_serializers.py(new)bots/meetings_api_urls.py(new)bots/throttling.pyMemberReadThrottle— per-member read budgetbots/team_day_user_auth.pyquiet_user_id()for throttling (never raises)attendee/settings/base.pymember_readrate (default 600/min)attendee/urls.pyversion.jsonfeattitle)Total: 306 changed lines, 1 commit.
Testing performed
Real HTTP against a running server, with real team.day-signed tokens and real audio.
PR 6 behaviour — 31/31
source=bot|localfilters ·source=garbage→ 400recording,paused,processing,done,partially_failedutterances gone, absent from history · second delete → 204 (no 500) · another member's →
404 · scheduled → cancelled and the row removed
Regression
test_bots_api_views,test_bots_api_utils,test_bot_data_deletion,test_cleanup→ 114 tests OK
member blocked → heartbeat → pause → status
paused→ resume → 8 chunks → appears in history→ delete-while-recording 409 → stop →
ENDED→ recordingCOMPLETE→ real transcription→ transcript via
/meetings→ delete → gone from historyMeasured latency at production pacing (1 chunk/second, a full 94-second recording):
Gates:
ruff check✅ ·ruff format --check✅ ·manage.py check✅ ·version.jsonbump simulated against the CI rule ✅Coding-guidelines compliance
serializer was put in its own module specifically to avoid growing
bots/serializers.py(~2,400 lines) further.
attendee/settings/base.pyis now 349 lines (+3 here) — under the350 hard limit but over the 300 warning; splitting settings by concern would be a small,
separate PR.
SOURCE_TO_SESSION_TYPE,SESSION_TYPE_TO_SOURCE, page size and throttlescope are named; no magic values. The throttle rate is env-overridable.
messages, and every path that could raise out of the model layer is guarded first.
mismatches return 404 so existence never leaks; reads are rate-limited per member.
coverage.
type(scope): description.Risks / notes
(no backfill), so a bot meeting only gains an owner when the desktop sends the user token on
dispatch, which is not built yet. Local recordings appear immediately. Anyone testing this PR
in isolation will see an empty
source=botlist.MeetingTranscriptViewsubclasses the existingTranscriptView, adding the ownership gatein front. Duplicating the transcript logic was rejected as guaranteed drift; the trade-off is a
dependency on that view's internals.
a very long meeting would return a large payload. Worth a limit later.
version.jsonis 1.51.0, one minor above this PR's base. If the earlier PRs' versions arebumped before this merges, this one needs re-bumping to stay one minor above its base.
shared JWT signing secret.