diff --git a/internal/atproto/lexicon/social/coves/feed/getCommunity.json b/internal/atproto/lexicon/social/coves/feed/getCommunity.json --- a/internal/atproto/lexicon/social/coves/feed/getCommunity.json +++ b/internal/atproto/lexicon/social/coves/feed/getCommunity.json @@ -20,19 +20,6 @@ "enum": ["hot", "top", "new"], "default": "hot", "description": "Sort order for community feed" }, - "postType": { - "type": "string", - "enum": ["text", "article", "image", "video", "microblog"], - "description": "Filter by a single post type (computed from embed structure)" - }, - "postTypes": { - "type": "array", - "items": { - "type": "string", - "enum": ["text", "article", "image", "video", "microblog"] - }, - "description": "Filter by multiple post types (computed from embed structure)" - }, "timeframe": { "type": "string", "enum": ["hour", "day", "week", "month", "year", "all"], diff --git a/internal/atproto/lexicon/social/coves/post/get.json b/internal/atproto/lexicon/social/coves/post/get.json --- a/internal/atproto/lexicon/social/coves/post/get.json +++ b/internal/atproto/lexicon/social/coves/post/get.json @@ -4,15 +4,20 @@ "id": "social.coves.post.get", "defs": { "main": { "type": "query", - "description": "Get a single post with all its details", + "description": "Get posts by AT-URI. Supports batch fetching for feed hydration. Returns posts in same order as input URIs.", "parameters": { "type": "params", - "required": ["uri"], + "required": ["uris"], "properties": { - "uri": { - "type": "string", - "format": "at-uri", - "description": "AT-URI of the post" + "uris": { + "type": "array", + "description": "List of post AT-URIs to fetch (max 25)", + "items": { + "type": "string", + "format": "at-uri" + }, + "maxLength": 25, + "minLength": 1 } } }, @@ -20,15 +25,22 @@ "output": { "encoding": "application/json", "schema": { "type": "object", - "required": ["post"], + "required": ["posts"], "properties": { - "post": { - "type": "ref", - "ref": "#postView" + "posts": { + "type": "array", + "description": "Array of post views. May include notFound/blocked entries for missing posts.", + "items": { + "type": "union", + "refs": ["#postView", "#notFoundPost", "#blockedPost"] + } } } } - } + }, + "errors": [ + {"name": "InvalidRequest", "description": "Invalid URI format or empty array"} + ] }, "postView": { "type": "object", @@ -67,20 +79,16 @@ "type": "ref", "ref": "social.coves.richtext.facet" } }, - "images": { - "type": "array", - "items": { - "type": "ref", - "ref": "#imageView" - } - }, - "video": { - "type": "ref", - "ref": "#videoView" - }, - "external": { - "type": "ref", - "ref": "#externalView" + "embed": { + "type": "union", + "description": "Embedded content (images, video, link preview, or quoted post)", + "refs": [ + "social.coves.embed.images#view", + "social.coves.embed.video#view", + "social.coves.embed.external#view", + "social.coves.embed.record#view", + "social.coves.embed.recordWithMedia#view" + ] }, "language": { "type": "string", @@ -151,57 +159,71 @@ "format": "uri" } } }, - "imageView": { + "notFoundPost": { "type": "object", - "required": ["fullsize"], + "description": "Post was not found (deleted, never indexed, or invalid URI)", + "required": ["uri", "notFound"], "properties": { - "fullsize": { - "type": "string", - "format": "uri" - }, - "thumb": { + "uri": { "type": "string", - "format": "uri" + "format": "at-uri" }, - "alt": { - "type": "string" + "notFound": { + "type": "boolean", + "const": true } } }, - "videoView": { + "blockedPost": { "type": "object", - "required": ["url"], + "description": "Post is blocked due to viewer blocking author/community, or community moderation", + "required": ["uri", "blocked"], "properties": { - "url": { + "uri": { "type": "string", - "format": "uri" + "format": "at-uri" }, - "thumbnail": { + "blocked": { + "type": "boolean", + "const": true + }, + "blockedBy": { "type": "string", - "format": "uri" + "enum": ["author", "community", "moderator"], + "description": "What caused the block: viewer blocked author, viewer blocked community, or post was removed by moderators" }, - "alt": { - "type": "string" + "author": { + "type": "ref", + "ref": "#blockedAuthor" + }, + "community": { + "type": "ref", + "ref": "#blockedCommunity" } } }, - "externalView": { + "blockedAuthor": { + "type": "object", + "description": "Minimal author info for blocked posts", + "required": ["did"], + "properties": { + "did": { + "type": "string", + "format": "did" + } + } + }, + "blockedCommunity": { "type": "object", - "required": ["uri"], + "description": "Minimal community info for blocked posts", + "required": ["did"], "properties": { - "uri": { + "did": { "type": "string", - "format": "uri" - }, - "title": { - "type": "string" + "format": "did" }, - "description": { + "name": { "type": "string" - }, - "thumb": { - "type": "string", - "format": "uri" } } }, diff --git a/internal/core/posts/post.go b/internal/core/posts/post.go --- a/internal/core/posts/post.go +++ b/internal/core/posts/post.go @@ -66,3 +66,64 @@ CreatedAt string `json:"createdAt"` Facets []interface{} `json:"facets,omitempty"` ContentLabels []string `json:"contentLabels,omitempty"` } + +// PostView represents the full view of a post with all metadata +// Matches social.coves.post.get#postView lexicon +// Used in feeds and get endpoints +type PostView struct { + IndexedAt time.Time `json:"indexedAt"` + CreatedAt time.Time `json:"createdAt"` + Record interface{} `json:"record,omitempty"` + Embed interface{} `json:"embed,omitempty"` + Language *string `json:"language,omitempty"` + EditedAt *time.Time `json:"editedAt,omitempty"` + Title *string `json:"title,omitempty"` + Text *string `json:"text,omitempty"` + Viewer *ViewerState `json:"viewer,omitempty"` + Author *AuthorView `json:"author"` + Stats *PostStats `json:"stats,omitempty"` + Community *CommunityRef `json:"community"` + RKey string `json:"rkey"` + CID string `json:"cid"` + URI string `json:"uri"` + TextFacets []interface{} `json:"textFacets,omitempty"` + UpvoteCount int `json:"-"` + DownvoteCount int `json:"-"` + Score int `json:"-"` + CommentCount int `json:"-"` +} + +// AuthorView represents author information in post views +type AuthorView struct { + DisplayName *string `json:"displayName,omitempty"` + Avatar *string `json:"avatar,omitempty"` + Reputation *int `json:"reputation,omitempty"` + DID string `json:"did"` + Handle string `json:"handle"` +} + +// CommunityRef represents minimal community info in post views +type CommunityRef struct { + Avatar *string `json:"avatar,omitempty"` + DID string `json:"did"` + Name string `json:"name"` +} + +// PostStats represents aggregated statistics +type PostStats struct { + TagCounts map[string]int `json:"tagCounts,omitempty"` + Upvotes int `json:"upvotes"` + Downvotes int `json:"downvotes"` + Score int `json:"score"` + CommentCount int `json:"commentCount"` + ShareCount int `json:"shareCount,omitempty"` +} + +// ViewerState represents the viewer's relationship with the post +type ViewerState struct { + Vote *string `json:"vote,omitempty"` + VoteURI *string `json:"voteUri,omitempty"` + SavedURI *string `json:"savedUri,omitempty"` + Tags []string `json:"tags,omitempty"` + Saved bool `json:"saved"` +}