A community based topic aggregation platform built on atproto

feat: integrate unfurl and blob services into post creation flow

Wire unfurl and blob services into the post creation pipeline, enabling
automatic enhancement of external embeds with rich metadata and thumbnails.

**Post Service Integration:**
- Add optional BlobService and UnfurlService dependencies
- Update constructor to accept blob/unfurl services (nil-safe)
- Add ThumbnailURL field to CreatePostRequest for client-provided URLs
- Add PDSURL to CommunityRef for blob URL transformation (internal only)

**Server Main Changes:**
- Initialize unfurl repository with PostgreSQL
- Initialize blob service with default PDS URL
- Initialize unfurl service with:
- 10s timeout for HTTP fetches
- 24h cache TTL
- CovesBot/1.0 user agent
- Pass blob and unfurl services to post service constructor

**Flow:**
```
Client POST → CreateHandler

PostService.Create() [external embed detected]
↓ (if no thumb provided)
UnfurlService.UnfurlURL() [fetch oEmbed/OpenGraph]
↓ (cache miss)
HTTP fetch → oEmbed provider / HTML parser
↓ (thumbnail URL found)
BlobService.UploadBlobFromURL() [download & upload to PDS]

com.atproto.repo.uploadBlob → PDS
↓ (returns BlobRef with CID)
Embed enriched with thumb blob → Write to PDS
```

**Interface Documentation:**
- Added comments explaining optional blob/unfurl service injection
- Unfurl service auto-enriches external embeds when provided
- Blob service uploads thumbnails from unfurled URLs

This is the core integration that enables the full unfurling feature.
The actual unfurl logic in posts/service.go will be implemented separately.

Changed files
+27 -2
cmd
server
internal
core
+18 -1
cmd/server/main.go
···
"Coves/internal/atproto/identity"
"Coves/internal/atproto/jetstream"
"Coves/internal/core/aggregators"
+
"Coves/internal/core/blobs"
"Coves/internal/core/comments"
"Coves/internal/core/communities"
"Coves/internal/core/communityFeeds"
"Coves/internal/core/discover"
"Coves/internal/core/posts"
"Coves/internal/core/timeline"
+
"Coves/internal/core/unfurl"
"Coves/internal/core/users"
"bytes"
"context"
···
aggregatorService := aggregators.NewAggregatorService(aggregatorRepo, communityService)
log.Println("✅ Aggregator service initialized")
+
// Initialize unfurl cache repository
+
unfurlRepo := unfurl.NewRepository(db)
+
+
// Initialize blob upload service
+
blobService := blobs.NewBlobService(defaultPDS)
+
+
// Initialize unfurl service with configuration
+
unfurlService := unfurl.NewService(
+
unfurlRepo,
+
unfurl.WithTimeout(10*time.Second),
+
unfurl.WithUserAgent("CovesBot/1.0 (+https://coves.social)"),
+
unfurl.WithCacheTTL(24*time.Hour),
+
)
+
log.Println("✅ Unfurl and blob services initialized")
+
// Initialize post service (with aggregator support)
postRepo := postgresRepo.NewPostRepository(db)
-
postService := posts.NewPostService(postRepo, communityService, aggregatorService, defaultPDS)
+
postService := posts.NewPostService(postRepo, communityService, aggregatorService, blobService, unfurlService, defaultPDS)
// Initialize vote repository (used by Jetstream consumer for indexing)
voteRepo := postgresRepo.NewVoteRepository(db)
+7 -1
internal/core/posts/interfaces.go
···
package posts
-
import "context"
+
import (
+
"context"
+
)
+
+
// Service constructor accepts optional blobs.Service and unfurl.Service for embed enhancement.
+
// When unfurlService is provided, external embeds will be automatically enriched with metadata.
+
// When blobService is provided, thumbnails from unfurled URLs will be uploaded as blobs.
// Service defines the business logic interface for posts
// Coordinates between Repository, community service, and PDS
+2
internal/core/posts/post.go
···
Title *string `json:"title,omitempty"`
Content *string `json:"content,omitempty"`
Embed map[string]interface{} `json:"embed,omitempty"`
+
ThumbnailURL *string `json:"thumbnailUrl,omitempty"`
Labels *SelfLabels `json:"labels,omitempty"`
Community string `json:"community"`
AuthorDID string `json:"authorDid"`
···
DID string `json:"did"`
Handle string `json:"handle"`
Name string `json:"name"`
+
PDSURL string `json:"-"` // Not exposed to API, used for blob URL transformation
}
// PostStats represents aggregated statistics