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.

+27 -2
+18 -1
cmd/server/main.go
··· 7 7 "Coves/internal/atproto/identity" 8 8 "Coves/internal/atproto/jetstream" 9 9 "Coves/internal/core/aggregators" 10 + "Coves/internal/core/blobs" 10 11 "Coves/internal/core/comments" 11 12 "Coves/internal/core/communities" 12 13 "Coves/internal/core/communityFeeds" 13 14 "Coves/internal/core/discover" 14 15 "Coves/internal/core/posts" 15 16 "Coves/internal/core/timeline" 17 + "Coves/internal/core/unfurl" 16 18 "Coves/internal/core/users" 17 19 "bytes" 18 20 "context" ··· 281 283 aggregatorService := aggregators.NewAggregatorService(aggregatorRepo, communityService) 282 284 log.Println("✅ Aggregator service initialized") 283 285 286 + // Initialize unfurl cache repository 287 + unfurlRepo := unfurl.NewRepository(db) 288 + 289 + // Initialize blob upload service 290 + blobService := blobs.NewBlobService(defaultPDS) 291 + 292 + // Initialize unfurl service with configuration 293 + unfurlService := unfurl.NewService( 294 + unfurlRepo, 295 + unfurl.WithTimeout(10*time.Second), 296 + unfurl.WithUserAgent("CovesBot/1.0 (+https://coves.social)"), 297 + unfurl.WithCacheTTL(24*time.Hour), 298 + ) 299 + log.Println("✅ Unfurl and blob services initialized") 300 + 284 301 // Initialize post service (with aggregator support) 285 302 postRepo := postgresRepo.NewPostRepository(db) 286 - postService := posts.NewPostService(postRepo, communityService, aggregatorService, defaultPDS) 303 + postService := posts.NewPostService(postRepo, communityService, aggregatorService, blobService, unfurlService, defaultPDS) 287 304 288 305 // Initialize vote repository (used by Jetstream consumer for indexing) 289 306 voteRepo := postgresRepo.NewVoteRepository(db)
+7 -1
internal/core/posts/interfaces.go
··· 1 1 package posts 2 2 3 - import "context" 3 + import ( 4 + "context" 5 + ) 6 + 7 + // Service constructor accepts optional blobs.Service and unfurl.Service for embed enhancement. 8 + // When unfurlService is provided, external embeds will be automatically enriched with metadata. 9 + // When blobService is provided, thumbnails from unfurled URLs will be uploaded as blobs. 4 10 5 11 // Service defines the business logic interface for posts 6 12 // Coordinates between Repository, community service, and PDS
+2
internal/core/posts/post.go
··· 50 50 Title *string `json:"title,omitempty"` 51 51 Content *string `json:"content,omitempty"` 52 52 Embed map[string]interface{} `json:"embed,omitempty"` 53 + ThumbnailURL *string `json:"thumbnailUrl,omitempty"` 53 54 Labels *SelfLabels `json:"labels,omitempty"` 54 55 Community string `json:"community"` 55 56 AuthorDID string `json:"authorDid"` ··· 121 122 DID string `json:"did"` 122 123 Handle string `json:"handle"` 123 124 Name string `json:"name"` 125 + PDSURL string `json:"-"` // Not exposed to API, used for blob URL transformation 124 126 } 125 127 126 128 // PostStats represents aggregated statistics