A community based topic aggregation platform built on atproto

style: apply gofumpt formatting and fix linter issues

Run go fmt, gofumpt, and make lint-fix to ensure code quality:

Formatting fixes:
- Standardize import block formatting across all files
- Apply gofumpt strict formatting rules
- Remove nil checks where len() is sufficient (gosimple)

Code cleanup:
- Remove unused setupIdentityResolver function from tests
- Fix comment_consumer.go: omit unnecessary nil checks

All critical lint issues resolved ✅
Only fieldalignment optimization suggestions remain (non-critical)

Files affected: 17 Go files across:
- cmd/server
- internal/api/handlers/comments
- internal/atproto/jetstream
- internal/core (comments, posts)
- internal/db/postgres
- tests/integration

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

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

+2 -1
cmd/server/main.go
···
"Coves/internal/atproto/identity"
"Coves/internal/atproto/jetstream"
"Coves/internal/core/aggregators"
+
"Coves/internal/core/comments"
"Coves/internal/core/communities"
"Coves/internal/core/communityFeeds"
"Coves/internal/core/discover"
···
"github.com/pressly/goose/v3"
commentsAPI "Coves/internal/api/handlers/comments"
-
"Coves/internal/core/comments"
+
postgresRepo "Coves/internal/db/postgres"
)
+7 -7
internal/api/handlers/comments/get_comments.go
···
// GetCommentsRequest represents the query parameters for fetching comments
// Matches social.coves.feed.getComments lexicon input
type GetCommentsRequest struct {
-
PostURI string `json:"post"` // Required: AT-URI of the post
-
Sort string `json:"sort,omitempty"` // Optional: "hot", "top", "new" (default: "hot")
-
Timeframe string `json:"timeframe,omitempty"` // Optional: For "top" sort - "hour", "day", "week", "month", "year", "all"
-
Depth int `json:"depth,omitempty"` // Optional: Max nesting depth (default: 10)
-
Limit int `json:"limit,omitempty"` // Optional: Max comments per page (default: 50, max: 100)
-
Cursor *string `json:"cursor,omitempty"` // Optional: Pagination cursor
-
ViewerDID *string `json:"-"` // Internal: Extracted from auth token
+
Cursor *string `json:"cursor,omitempty"`
+
ViewerDID *string `json:"-"`
+
PostURI string `json:"post"`
+
Sort string `json:"sort,omitempty"`
+
Timeframe string `json:"timeframe,omitempty"`
+
Depth int `json:"depth,omitempty"`
+
Limit int `json:"limit,omitempty"`
}
// NewGetCommentsHandler creates a new handler for fetching comments
+4 -3
internal/api/handlers/comments/middleware.go
···
// This ensures comment handlers can access viewer identity when available, but don't require authentication.
//
// Usage in router setup:
-
// commentHandler := comments.NewGetCommentsHandler(commentService)
-
// router.Handle("/xrpc/social.coves.feed.getComments",
-
// comments.OptionalAuthMiddleware(authMiddleware, commentHandler.HandleGetComments))
+
//
+
// commentHandler := comments.NewGetCommentsHandler(commentService)
+
// router.Handle("/xrpc/social.coves.feed.getComments",
+
// comments.OptionalAuthMiddleware(authMiddleware, commentHandler.HandleGetComments))
//
// The middleware extracts the viewer DID from the Authorization header if present and valid,
// making it available via middleware.GetUserDID(r) in the handler.
+6 -8
internal/atproto/jetstream/comment_consumer.go
···
time.Now(),
commentID,
)
-
if err != nil {
return fmt.Errorf("failed to resurrect comment: %w", err)
}
···
comment.Content, comment.ContentFacets, comment.Embed, comment.ContentLabels, pq.Array(comment.Langs),
comment.CreatedAt, time.Now(),
).Scan(&commentID)
-
if err != nil {
return fmt.Errorf("failed to insert comment: %w", err)
}
···
// CommentRecordFromJetstream represents a comment record as received from Jetstream
// Matches social.coves.feed.comment lexicon
type CommentRecordFromJetstream struct {
-
Type string `json:"$type"`
+
Labels interface{} `json:"labels,omitempty"`
+
Embed map[string]interface{} `json:"embed,omitempty"`
Reply ReplyRefFromJetstream `json:"reply"`
+
Type string `json:"$type"`
Content string `json:"content"`
+
CreatedAt string `json:"createdAt"`
Facets []interface{} `json:"facets,omitempty"`
-
Embed map[string]interface{} `json:"embed,omitempty"`
Langs []string `json:"langs,omitempty"`
-
Labels interface{} `json:"labels,omitempty"`
-
CreatedAt string `json:"createdAt"`
}
// ReplyRefFromJetstream represents the threading structure
···
// Returns nil pointers for empty/nil fields (DRY helper to avoid duplication)
func serializeOptionalFields(commentRecord *CommentRecordFromJetstream) (facetsJSON, embedJSON, labelsJSON *string) {
// Serialize facets if present
-
if commentRecord.Facets != nil && len(commentRecord.Facets) > 0 {
+
if len(commentRecord.Facets) > 0 {
if facetsBytes, err := json.Marshal(commentRecord.Facets); err == nil {
facetsStr := string(facetsBytes)
facetsJSON = &facetsStr
···
}
// Serialize embed if present
-
if commentRecord.Embed != nil && len(commentRecord.Embed) > 0 {
+
if len(commentRecord.Embed) > 0 {
if embedBytes, err := json.Marshal(commentRecord.Embed); err == nil {
embedStr := string(embedBytes)
embedJSON = &embedStr
+8 -3
internal/atproto/jetstream/community_consumer.go
···
// CommunityEventConsumer consumes community-related events from Jetstream
type CommunityEventConsumer struct {
-
repo communities.Repository // Repository for community operations
-
identityResolver interface{ Resolve(context.Context, string) (*identity.Identity, error) } // For resolving handles from DIDs
+
repo communities.Repository // Repository for community operations
+
identityResolver interface {
+
Resolve(context.Context, string) (*identity.Identity, error)
+
} // For resolving handles from DIDs
httpClient *http.Client // Shared HTTP client with connection pooling
didCache *lru.Cache[string, cachedDIDDoc] // Bounded LRU cache for .well-known verification results
wellKnownLimiter *rate.Limiter // Rate limiter for .well-known fetches
···
// instanceDID: The DID of this Coves instance (for hostedBy verification)
// skipVerification: Skip did:web verification (for dev mode)
// identityResolver: Optional resolver for resolving handles from DIDs (can be nil for tests)
-
func NewCommunityEventConsumer(repo communities.Repository, instanceDID string, skipVerification bool, identityResolver interface{ Resolve(context.Context, string) (*identity.Identity, error) }) *CommunityEventConsumer {
+
func NewCommunityEventConsumer(repo communities.Repository, instanceDID string, skipVerification bool, identityResolver interface {
+
Resolve(context.Context, string) (*identity.Identity, error)
+
},
+
) *CommunityEventConsumer {
// Create bounded LRU cache for DID document verification results
// Max 1000 entries to prevent unbounded memory growth (PR review feedback)
// Each entry ~100 bytes → max ~100KB memory overhead
+3 -3
internal/atproto/jetstream/post_consumer.go
···
// PostEventConsumer consumes post-related events from Jetstream
// Currently handles only CREATE operations for social.coves.community.post
// UPDATE and DELETE handlers will be added when those features are implemented
-
type PostEventConsumer struct{
+
type PostEventConsumer struct {
postRepo posts.Repository
communityRepo communities.Repository
userService users.UserService
···
// PostRecordFromJetstream represents a post record as received from Jetstream
// Matches the structure written to PDS via social.coves.community.post
-
type PostRecordFromJetstream struct{
+
type PostRecordFromJetstream struct {
OriginalAuthor interface{} `json:"originalAuthor,omitempty"`
FederatedFrom interface{} `json:"federatedFrom,omitempty"`
Location interface{} `json:"location,omitempty"`
Title *string `json:"title,omitempty"`
Content *string `json:"content,omitempty"`
Embed map[string]interface{} `json:"embed,omitempty"`
+
Labels *posts.SelfLabels `json:"labels,omitempty"`
Type string `json:"$type"`
Community string `json:"community"`
Author string `json:"author"`
CreatedAt string `json:"createdAt"`
Facets []interface{} `json:"facets,omitempty"`
-
Labels *posts.SelfLabels `json:"labels,omitempty"`
}
// parsePostRecord converts a raw Jetstream record map to a PostRecordFromJetstream
+32 -43
internal/core/comments/comment.go
···
// Comment represents a comment in the AppView database
// Comments are indexed from the firehose after being written to user repositories
type Comment struct {
-
ID int64 `json:"id" db:"id"`
-
URI string `json:"uri" db:"uri"`
-
CID string `json:"cid" db:"cid"`
-
RKey string `json:"rkey" db:"rkey"`
-
CommenterDID string `json:"commenterDid" db:"commenter_did"`
-
-
// Author info (hydrated from users table for view building)
-
// Only populated by ListByParentWithHotRank, not persisted in comments table
-
CommenterHandle string `json:"commenterHandle,omitempty" db:"-"`
-
-
// Threading (reply references)
-
RootURI string `json:"rootUri" db:"root_uri"`
-
RootCID string `json:"rootCid" db:"root_cid"`
-
ParentURI string `json:"parentUri" db:"parent_uri"`
-
ParentCID string `json:"parentCid" db:"parent_cid"`
-
-
// Content
-
Content string `json:"content" db:"content"`
-
ContentFacets *string `json:"contentFacets,omitempty" db:"content_facets"`
-
Embed *string `json:"embed,omitempty" db:"embed"`
-
ContentLabels *string `json:"labels,omitempty" db:"content_labels"`
-
Langs []string `json:"langs,omitempty" db:"langs"`
-
-
// Timestamps
-
CreatedAt time.Time `json:"createdAt" db:"created_at"`
-
IndexedAt time.Time `json:"indexedAt" db:"indexed_at"`
-
DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"`
-
-
// Stats (denormalized for performance)
-
UpvoteCount int `json:"upvoteCount" db:"upvote_count"`
-
DownvoteCount int `json:"downvoteCount" db:"downvote_count"`
-
Score int `json:"score" db:"score"`
-
ReplyCount int `json:"replyCount" db:"reply_count"`
+
IndexedAt time.Time `json:"indexedAt" db:"indexed_at"`
+
CreatedAt time.Time `json:"createdAt" db:"created_at"`
+
ContentFacets *string `json:"contentFacets,omitempty" db:"content_facets"`
+
DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"`
+
ContentLabels *string `json:"labels,omitempty" db:"content_labels"`
+
Embed *string `json:"embed,omitempty" db:"embed"`
+
CommenterHandle string `json:"commenterHandle,omitempty" db:"-"`
+
CommenterDID string `json:"commenterDid" db:"commenter_did"`
+
ParentURI string `json:"parentUri" db:"parent_uri"`
+
ParentCID string `json:"parentCid" db:"parent_cid"`
+
Content string `json:"content" db:"content"`
+
RootURI string `json:"rootUri" db:"root_uri"`
+
URI string `json:"uri" db:"uri"`
+
RootCID string `json:"rootCid" db:"root_cid"`
+
CID string `json:"cid" db:"cid"`
+
RKey string `json:"rkey" db:"rkey"`
+
Langs []string `json:"langs,omitempty" db:"langs"`
+
ID int64 `json:"id" db:"id"`
+
UpvoteCount int `json:"upvoteCount" db:"upvote_count"`
+
DownvoteCount int `json:"downvoteCount" db:"downvote_count"`
+
Score int `json:"score" db:"score"`
+
ReplyCount int `json:"replyCount" db:"reply_count"`
}
// CommentRecord represents the atProto record structure indexed from Jetstream
// This is the data structure that gets stored in the user's repository
// Matches social.coves.feed.comment lexicon
type CommentRecord struct {
-
Type string `json:"$type"`
-
Reply ReplyRef `json:"reply"`
-
Content string `json:"content"`
-
Facets []interface{} `json:"facets,omitempty"`
-
Embed map[string]interface{} `json:"embed,omitempty"`
-
Langs []string `json:"langs,omitempty"`
-
Labels *SelfLabels `json:"labels,omitempty"`
-
CreatedAt string `json:"createdAt"`
+
Embed map[string]interface{} `json:"embed,omitempty"`
+
Labels *SelfLabels `json:"labels,omitempty"`
+
Reply ReplyRef `json:"reply"`
+
Type string `json:"$type"`
+
Content string `json:"content"`
+
CreatedAt string `json:"createdAt"`
+
Facets []interface{} `json:"facets,omitempty"`
+
Langs []string `json:"langs,omitempty"`
}
// ReplyRef represents the threading structure from the comment lexicon
···
// SelfLabel represents a single label value per com.atproto.label.defs#selfLabel
// Neg is optional and negates the label when true
type SelfLabel struct {
-
Val string `json:"val"` // Required: label value (max 128 chars)
-
Neg *bool `json:"neg,omitempty"` // Optional: negates the label if true
+
Neg *bool `json:"neg,omitempty"`
+
Val string `json:"val"`
}
+11 -11
internal/core/comments/comment_service.go
···
// GetCommentsRequest defines the parameters for fetching comments
type GetCommentsRequest struct {
-
PostURI string // AT-URI of the post to fetch comments for
-
Sort string // "hot", "top", "new" - sorting algorithm
-
Timeframe string // "hour", "day", "week", "month", "year", "all" - for "top" sort only
-
Depth int // 0-100 - how many levels of nested replies to load (default 10)
-
Limit int // 1-100 - max top-level comments per page (default 50)
-
Cursor *string // Pagination cursor from previous response
-
ViewerDID *string // Optional DID of authenticated viewer (for vote state)
+
Cursor *string
+
ViewerDID *string
+
PostURI string
+
Sort string
+
Timeframe string
+
Depth int
+
Limit int
}
// commentService implements the Service interface
// Coordinates between repository layer and view model construction
type commentService struct {
-
commentRepo Repository // Comment data access
-
userRepo users.UserRepository // User lookup for author hydration
-
postRepo posts.Repository // Post lookup for building post views
-
communityRepo communities.Repository // Community lookup for community hydration
+
commentRepo Repository // Comment data access
+
userRepo users.UserRepository // User lookup for author hydration
+
postRepo posts.Repository // Post lookup for building post views
+
communityRepo communities.Repository // Community lookup for community hydration
}
// NewCommentService creates a new comment service instance
+12 -12
internal/core/comments/view_models.go
···
// Matches social.coves.feed.getComments#commentView lexicon
// Used in thread views and get endpoints
type CommentView struct {
-
URI string `json:"uri"`
-
CID string `json:"cid"`
+
Embed interface{} `json:"embed,omitempty"`
+
Record interface{} `json:"record"`
+
Viewer *CommentViewerState `json:"viewer,omitempty"`
Author *posts.AuthorView `json:"author"`
-
Record interface{} `json:"record"` // Original record verbatim
-
Post *CommentRef `json:"post"` // Reference to parent post
-
Parent *CommentRef `json:"parent,omitempty"` // Parent comment if nested
+
Post *CommentRef `json:"post"`
+
Parent *CommentRef `json:"parent,omitempty"`
+
Stats *CommentStats `json:"stats"`
Content string `json:"content"`
+
CreatedAt string `json:"createdAt"`
+
IndexedAt string `json:"indexedAt"`
+
URI string `json:"uri"`
+
CID string `json:"cid"`
ContentFacets []interface{} `json:"contentFacets,omitempty"`
-
Embed interface{} `json:"embed,omitempty"`
-
CreatedAt string `json:"createdAt"` // RFC3339
-
IndexedAt string `json:"indexedAt"` // RFC3339
-
Stats *CommentStats `json:"stats"`
-
Viewer *CommentViewerState `json:"viewer,omitempty"`
}
// ThreadViewComment represents a comment with its nested replies
···
// Matches social.coves.feed.getComments lexicon output
// Includes the full comment thread tree and original post reference
type GetCommentsResponse struct {
+
Post interface{} `json:"post"`
+
Cursor *string `json:"cursor,omitempty"`
Comments []*ThreadViewComment `json:"comments"`
-
Post interface{} `json:"post"` // PostView from post handler
-
Cursor *string `json:"cursor,omitempty"` // Pagination cursor
}
+5 -5
internal/core/posts/post.go
···
// SelfLabel represents a single label value per com.atproto.label.defs#selfLabel
// Neg is optional and negates the label when true
type SelfLabel struct {
-
Val string `json:"val"` // Required: label value (max 128 chars)
-
Neg *bool `json:"neg,omitempty"` // Optional: negates the label if true
+
Neg *bool `json:"neg,omitempty"`
+
Val string `json:"val"`
}
// Post represents a post in the AppView database
···
Title *string `json:"title,omitempty"`
Content *string `json:"content,omitempty"`
Embed map[string]interface{} `json:"embed,omitempty"`
+
Labels *SelfLabels `json:"labels,omitempty"`
Community string `json:"community"`
AuthorDID string `json:"authorDid"`
Facets []interface{} `json:"facets,omitempty"`
-
Labels *SelfLabels `json:"labels,omitempty"`
}
// CreatePostResponse represents the response from creating a post
// Matches social.coves.community.post.create lexicon output schema
-
type CreatePostResponse struct{
+
type CreatePostResponse struct {
URI string `json:"uri"` // AT-URI of created post
CID string `json:"cid"` // CID of created post
}
···
Title *string `json:"title,omitempty"`
Content *string `json:"content,omitempty"`
Embed map[string]interface{} `json:"embed,omitempty"`
+
Labels *SelfLabels `json:"labels,omitempty"`
Type string `json:"$type"`
Community string `json:"community"`
Author string `json:"author"`
CreatedAt string `json:"createdAt"`
Facets []interface{} `json:"facets,omitempty"`
-
Labels *SelfLabels `json:"labels,omitempty"`
}
// PostView represents the full view of a post with all metadata
+2 -2
internal/core/posts/service.go
···
// IMPORTANT: repo is set to community DID, not author DID
// This writes the post to the community's repository
payload := map[string]interface{}{
-
"repo": community.DID, // Community's repository
+
"repo": community.DID, // Community's repository
"collection": "social.coves.community.post", // Collection type
-
"record": record, // The post record
+
"record": record, // The post record
// "rkey" omitted - PDS will auto-generate TID
}
+1
internal/db/postgres/comment_repo.go
···
return result, nil
}
+
// ListByParentWithHotRank retrieves direct replies to a post or comment with sorting and pagination
// Supports three sort modes: hot (Lemmy algorithm), top (by score + timeframe), and new (by created_at)
// Uses cursor-based pagination with composite keys for consistent ordering
+2 -2
tests/integration/comment_consumer_test.go
···
RKey: rkey,
CID: "bafytest123",
Record: map[string]interface{}{
-
"$type": "social.coves.feed.comment",
+
"$type": "social.coves.feed.comment",
"content": "This is a test comment on a post!",
"reply": map[string]interface{}{
"root": map[string]interface{}{
···
RKey: rkey,
CID: "bafytest456",
Record: map[string]interface{}{
-
"$type": "social.coves.feed.comment",
+
"$type": "social.coves.feed.comment",
"content": "Idempotent test comment",
"reply": map[string]interface{}{
"root": map[string]interface{}{
+2 -2
tests/integration/comment_query_test.go
···
}
type testGetCommentsRequest struct {
+
Cursor *string
+
ViewerDID *string
PostURI string
Sort string
Timeframe string
Depth int
Limit int
-
Cursor *string
-
ViewerDID *string
}
func setupCommentServiceAdapter(db *sql.DB) *testCommentServiceAdapter {
+3 -6
tests/integration/community_consumer_test.go
···
// mockIdentityResolver is a test double for identity resolution
type mockIdentityResolver struct {
-
// Map of DID -> handle for successful resolutions
resolutions map[string]string
-
// If true, Resolve returns an error
-
shouldFail bool
-
// Track calls to verify invocation
-
callCount int
-
lastDID string
+
lastDID string
+
callCount int
+
shouldFail bool
}
func newMockIdentityResolver() *mockIdentityResolver {
-12
tests/integration/user_test.go
···
return db
}
-
// setupIdentityResolver creates an identity resolver configured for local PLC testing
-
func setupIdentityResolver(db *sql.DB) interface{ Resolve(context.Context, string) (*identity.Identity, error) } {
-
plcURL := os.Getenv("PLC_DIRECTORY_URL")
-
if plcURL == "" {
-
plcURL = "http://localhost:3002" // Local PLC directory
-
}
-
-
config := identity.DefaultConfig()
-
config.PLCURL = plcURL
-
return identity.NewResolver(db, config)
-
}
-
// generateTestDID generates a unique test DID for integration tests
// V2.0: No longer uses DID generator - just creates valid did:plc strings
func generateTestDID(suffix string) string {
+6 -6
tests/lexicon_validation_test.go
···
// Test specific cross-references that should work
crossRefs := map[string]string{
-
"social.coves.richtext.facet#byteSlice": "byteSlice definition in facet schema",
-
"social.coves.community.rules#rule": "rule definition in community rules",
-
"social.coves.actor.defs#profileView": "profileView definition in actor defs",
-
"social.coves.actor.defs#profileStats": "profileStats definition in actor defs",
-
"social.coves.actor.defs#viewerState": "viewerState definition in actor defs",
-
"social.coves.community.defs#communityView": "communityView definition in community defs",
+
"social.coves.richtext.facet#byteSlice": "byteSlice definition in facet schema",
+
"social.coves.community.rules#rule": "rule definition in community rules",
+
"social.coves.actor.defs#profileView": "profileView definition in actor defs",
+
"social.coves.actor.defs#profileStats": "profileStats definition in actor defs",
+
"social.coves.actor.defs#viewerState": "viewerState definition in actor defs",
+
"social.coves.community.defs#communityView": "communityView definition in community defs",
"social.coves.community.defs#communityStats": "communityStats definition in community defs",
}