From 57ed860e917846b5cf216a075f5ab8517a08dc9b Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Mon, 1 Jun 2026 20:46:01 -0700 Subject: [PATCH] place.stream.video: add required createdAt, set server-side on publishVideo Adds a createdAt datetime to the video record (required, matching livestream/vod.comment convention). publishVideo now stamps it server-side alongside source + durationMs; the upload edit path preserves the existing value. Regenerated lexicon types + docs. Co-Authored-By: Claude Opus 4.8 --- js/app/src/screens/upload.tsx | 7 ++++ .../docs/lex-reference/place-stream-video.md | 8 ++++- lexicons/place/stream/video.json | 7 +++- pkg/streamplace/cbor_gen.go | 36 ++++++++++++++++++- pkg/streamplace/streamvideo.go | 2 ++ pkg/vod/publish_video.go | 7 ++-- 6 files changed, 62 insertions(+), 5 deletions(-) diff --git a/js/app/src/screens/upload.tsx b/js/app/src/screens/upload.tsx index 28173e3eb..ba8aae3d4 100644 --- a/js/app/src/screens/upload.tsx +++ b/js/app/src/screens/upload.tsx @@ -348,6 +348,10 @@ export default function UploadScreen() { const record: PlaceStreamVideo.Record = { $type: "place.stream.video", title: title.trim() || file?.name || "Untitled", + // Like source + durationMs, createdAt is server-authoritative: the + // server overrides this with the publish time. We still send a value + // to satisfy the (required) record type. + createdAt: new Date().toISOString(), durationMs, source: { $type: "place.stream.media.defs#sourceTracks", @@ -505,6 +509,9 @@ export default function UploadScreen() { const existingRec = existing.data.value as any; record.source = existingRec.source; record.durationMs = existingRec.durationMs; + // Preserve the original creation timestamp on edit; fall back for + // records that predate the createdAt field. + record.createdAt = existingRec.createdAt || new Date().toISOString(); if (description.trim()) record.description = description.trim(); if (activity) record.activity = activity; if (tags.length > 0) record.tags = tags; diff --git a/js/docs/src/content/docs/lex-reference/place-stream-video.md b/js/docs/src/content/docs/lex-reference/place-stream-video.md index 6604c169e..586c8053a 100644 --- a/js/docs/src/content/docs/lex-reference/place-stream-video.md +++ b/js/docs/src/content/docs/lex-reference/place-stream-video.md @@ -22,6 +22,7 @@ Some audiovisual content. | Name | Type | Req'd | Description | Constraints | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | | `title` | `string` | ✅ | Title of the video referenced by this record | Max Length: 1400
Max Graphemes: 140 | +| `createdAt` | `string` | ✅ | Timestamp when this video record was created. Populated server-side at publication time by place.stream.media.publishVideo. | Format: `datetime` | | `source` | Union of:
  [`place.stream.media.defs#sourceTracks`](/lex-reference/place-stream-media-defs#sourcetracks)
  [`place.stream.media.defs#sourceClip`](/lex-reference/place-stream-media-defs#sourceclip) | ✅ | What is the source of this video? | | | `description` | `string` | ❌ | Description of this video | Max Length: 50000
Max Graphemes: 5000 | | `durationMs` | `integer` | ✅ | Duration of the video in milliseconds | | @@ -61,7 +62,7 @@ Some audiovisual content. "description": "Some audiovisual content.", "key": "tid", "record": { - "required": ["title", "source", "durationMs"], + "required": ["title", "source", "durationMs", "createdAt"], "type": "object", "properties": { "title": { @@ -70,6 +71,11 @@ Some audiovisual content. "maxGraphemes": 140, "description": "Title of the video referenced by this record" }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Timestamp when this video record was created. Populated server-side at publication time by place.stream.media.publishVideo." + }, "source": { "type": "union", "refs": [ diff --git a/lexicons/place/stream/video.json b/lexicons/place/stream/video.json index d72d66652..9f3a7aaf9 100644 --- a/lexicons/place/stream/video.json +++ b/lexicons/place/stream/video.json @@ -7,7 +7,7 @@ "description": "Some audiovisual content.", "key": "tid", "record": { - "required": ["title", "source", "durationMs"], + "required": ["title", "source", "durationMs", "createdAt"], "type": "object", "properties": { "title": { @@ -16,6 +16,11 @@ "maxGraphemes": 140, "description": "Title of the video referenced by this record" }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Timestamp when this video record was created. Populated server-side at publication time by place.stream.media.publishVideo." + }, "source": { "type": "union", "refs": [ diff --git a/pkg/streamplace/cbor_gen.go b/pkg/streamplace/cbor_gen.go index 71aa568bf..c9f37b356 100644 --- a/pkg/streamplace/cbor_gen.go +++ b/pkg/streamplace/cbor_gen.go @@ -8292,7 +8292,7 @@ func (t *Video) MarshalCBOR(w io.Writer) error { } cw := cbg.NewCborWriter(w) - fieldCount := 12 + fieldCount := 13 if t.Activity == nil { fieldCount-- @@ -8462,6 +8462,29 @@ func (t *Video) MarshalCBOR(w io.Writer) error { } } + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + // t.DurationMs (int64) (int64) if len("durationMs") > 1000000 { return xerrors.Errorf("Value in field \"durationMs\" was too long") @@ -8777,6 +8800,17 @@ func (t *Video) UnmarshalCBOR(r io.Reader) (err error) { } } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } // t.DurationMs (int64) (int64) case "durationMs": { diff --git a/pkg/streamplace/streamvideo.go b/pkg/streamplace/streamvideo.go index 0833d9a08..306d94540 100644 --- a/pkg/streamplace/streamvideo.go +++ b/pkg/streamplace/streamvideo.go @@ -29,6 +29,8 @@ type Video struct { ContentRights *MetadataContentRights `json:"contentRights,omitempty" cborgen:"contentRights,omitempty"` // contentWarnings: content warning data for this VOD ContentWarnings *MetadataContentWarnings `json:"contentWarnings,omitempty" cborgen:"contentWarnings,omitempty"` + // createdAt: Timestamp when this video record was created. Populated server-side at publication time by place.stream.media.publishVideo. + CreatedAt string `json:"createdAt" cborgen:"createdAt"` // description: Description of this video Description *string `json:"description,omitempty" cborgen:"description,omitempty"` // descriptionFacets: Annotations of text (mentions, URLs, etc) diff --git a/pkg/vod/publish_video.go b/pkg/vod/publish_video.go index bb16366d6..0b96abd87 100644 --- a/pkg/vod/publish_video.go +++ b/pkg/vod/publish_video.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "time" comatproto "github.com/bluesky-social/indigo/api/atproto" lexutil "github.com/bluesky-social/indigo/lex/util" @@ -62,10 +63,12 @@ func PublishVideo(ctx context.Context, state *statedb.StatefulDB, store blob.Sto return "", "", ErrUploadNotReady } - // Server-authoritative fields: source tracks and duration come from - // processing, not from whatever the client put in the record. + // Server-authoritative fields: source tracks, duration, and the + // creation timestamp come from the server at publish time, not from + // whatever the client put in the record. video.LexiconTypeID = constants.PLACE_STREAM_VIDEO video.DurationMs = upload.DurationMS + video.CreatedAt = time.Now().UTC().Format(time.RFC3339) tracks, err := sourceTracksFromUpload(upload) if err != nil { span.RecordError(err) -- 2.51.2