A community based topic aggregation platform built on atproto

feat(lexicon): add PostView types and update feed schema

Extends post types to support feed views and updates lexicon definitions:

Post Types Added:
- PostView: Full post representation with all metadata for feeds
- AuthorView: Author info (DID, handle, displayName, avatar, reputation)
- CommunityRef: Minimal community reference in posts
- PostStats: Aggregated statistics (votes, comments, shares)
- ViewerState: User's relationship with post (votes, saves, tags)

Lexicon Updates:
- social.coves.post.get: Restructured postView definition
- social.coves.feed.getCommunity: Updated query parameters and response

These types align with atProto patterns:
- Follows app.bsky.feed.defs#feedViewPost structure
- Supports viewer-specific state
- Enables efficient feed rendering
- Provides all data clients need in single request

The PostView types are used by feed endpoints to return rich post data.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+138 -68
internal
atproto
lexicon
social
coves
core
posts
-13
internal/atproto/lexicon/social/coves/feed/getCommunity.json
···
"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"],
···
"default": "hot",
"description": "Sort order for community feed"
},
"timeframe": {
"type": "string",
"enum": ["hour", "day", "week", "month", "year", "all"],
+77 -55
internal/atproto/lexicon/social/coves/post/get.json
···
"defs": {
"main": {
"type": "query",
-
"description": "Get a single post with all its details",
"parameters": {
"type": "params",
-
"required": ["uri"],
"properties": {
-
"uri": {
-
"type": "string",
-
"format": "at-uri",
-
"description": "AT-URI of the post"
}
}
},
···
"encoding": "application/json",
"schema": {
"type": "object",
-
"required": ["post"],
"properties": {
-
"post": {
-
"type": "ref",
-
"ref": "#postView"
}
}
}
-
}
},
"postView": {
"type": "object",
···
"ref": "social.coves.richtext.facet"
}
},
-
"images": {
-
"type": "array",
-
"items": {
-
"type": "ref",
-
"ref": "#imageView"
-
}
-
},
-
"video": {
-
"type": "ref",
-
"ref": "#videoView"
-
},
-
"external": {
-
"type": "ref",
-
"ref": "#externalView"
},
"language": {
"type": "string",
···
}
}
},
-
"imageView": {
"type": "object",
-
"required": ["fullsize"],
"properties": {
-
"fullsize": {
-
"type": "string",
-
"format": "uri"
-
},
-
"thumb": {
"type": "string",
-
"format": "uri"
},
-
"alt": {
-
"type": "string"
}
}
},
-
"videoView": {
"type": "object",
-
"required": ["url"],
"properties": {
-
"url": {
"type": "string",
-
"format": "uri"
},
-
"thumbnail": {
"type": "string",
-
"format": "uri"
},
-
"alt": {
-
"type": "string"
}
}
},
-
"externalView": {
"type": "object",
-
"required": ["uri"],
"properties": {
-
"uri": {
"type": "string",
-
"format": "uri"
-
},
-
"title": {
-
"type": "string"
},
-
"description": {
"type": "string"
-
},
-
"thumb": {
-
"type": "string",
-
"format": "uri"
}
}
},
···
"defs": {
"main": {
"type": "query",
+
"description": "Get posts by AT-URI. Supports batch fetching for feed hydration. Returns posts in same order as input URIs.",
"parameters": {
"type": "params",
+
"required": ["uris"],
"properties": {
+
"uris": {
+
"type": "array",
+
"description": "List of post AT-URIs to fetch (max 25)",
+
"items": {
+
"type": "string",
+
"format": "at-uri"
+
},
+
"maxLength": 25,
+
"minLength": 1
}
}
},
···
"encoding": "application/json",
"schema": {
"type": "object",
+
"required": ["posts"],
"properties": {
+
"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",
···
"ref": "social.coves.richtext.facet"
}
},
+
"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",
···
}
}
},
+
"notFoundPost": {
"type": "object",
+
"description": "Post was not found (deleted, never indexed, or invalid URI)",
+
"required": ["uri", "notFound"],
"properties": {
+
"uri": {
"type": "string",
+
"format": "at-uri"
},
+
"notFound": {
+
"type": "boolean",
+
"const": true
}
}
},
+
"blockedPost": {
"type": "object",
+
"description": "Post is blocked due to viewer blocking author/community, or community moderation",
+
"required": ["uri", "blocked"],
"properties": {
+
"uri": {
"type": "string",
+
"format": "at-uri"
},
+
"blocked": {
+
"type": "boolean",
+
"const": true
+
},
+
"blockedBy": {
"type": "string",
+
"enum": ["author", "community", "moderator"],
+
"description": "What caused the block: viewer blocked author, viewer blocked community, or post was removed by moderators"
},
+
"author": {
+
"type": "ref",
+
"ref": "#blockedAuthor"
+
},
+
"community": {
+
"type": "ref",
+
"ref": "#blockedCommunity"
}
}
},
+
"blockedAuthor": {
+
"type": "object",
+
"description": "Minimal author info for blocked posts",
+
"required": ["did"],
+
"properties": {
+
"did": {
+
"type": "string",
+
"format": "did"
+
}
+
}
+
},
+
"blockedCommunity": {
"type": "object",
+
"description": "Minimal community info for blocked posts",
+
"required": ["did"],
"properties": {
+
"did": {
"type": "string",
+
"format": "did"
},
+
"name": {
"type": "string"
}
}
},
+61
internal/core/posts/post.go
···
Facets []interface{} `json:"facets,omitempty"`
ContentLabels []string `json:"contentLabels,omitempty"`
}
···
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"`
+
}