commits
Implement a complete community suggestions feature allowing users to propose
ideas and vote on them. This is off-protocol (not stored on PDS/firehose)
and uses PostgreSQL directly for storage.
Changes:
- Add CRUD endpoints for community suggestions (create, get, list)
- Add voting with toggle semantics and atomic vote count updates
- Add admin-only status management (open/under_review/approved/declined)
- Add rate limiting: 3 suggestions/day per user, 10 req/min create, 30 req/min vote
- Add PostgreSQL migration (030) with community_suggestions and suggestion_votes tables
- Add repository with row-level locking for consistent vote counting
- Extract shared xrpc.WriteError() helper, refactor adminreport to use it
- Add Caddy proxy port (8080) to mobile port forwarding script
- Add comprehensive E2E integration tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
XRPC method names in atProto follow camelCase convention. The getProfile
endpoint was incorrectly using all-lowercase "getprofile".
Changes:
- Rename route from social.coves.actor.getprofile to social.coves.actor.getProfile
- Update startup log message, handler comment, and docs
- Update all E2E and integration test URLs and error messages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Close the network-use loophole so forks running as hosted services
must share their modifications.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Narrow the Caddy /api/* catch-all to only proxy /api/me to the Go
backend, since SvelteKit now owns /api/auth/* and /api/proxy/*. Update
the dev script to auto-detect the coves-frontend directory (falling
back to kelp) and switch from npm to pnpm.
Changes:
- Replace broad /api/* proxy rule with specific /api/me route
- Add coves-frontend directory auto-detection in web-dev-run.sh
- Switch frontend dev server from npm to pnpm
- Normalize Caddyfile.dev indentation to tabs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Refactors the mobile-only OAuth redirect URI system into a configurable
allowlist that supports both mobile apps (custom schemes + Universal Links)
and web clients (HTTPS redirects). Adds Caddy-based reverse proxy for web
frontend development, enabling same-origin cookie sharing between Vite
frontend and Coves backend.
Changes:
- Refactor isAllowedMobileRedirectURI() into OAuthHandler.isAllowedRedirectURI()
with BuildAllowedRedirectURIs() builder for the configurable allowlist
- Add smart redirect: HTTPS clients get direct HTTP redirect, custom scheme
clients get the intermediate redirect page
- Add Caddyfile.dev reverse proxy config (Vite :5173 + Coves :8081 on :8080)
- Add scripts/web-dev-run.sh for combined backend+frontend dev startup
- Add Makefile targets: run-web, web-proxy, web-proxy-bg, web-proxy-stop
- Auto-run db-migrate before server start in make run and make run-web
- Update OAuth client comments to clarify ATProto loopback client_id spec
- Add PAR request debug logging in dev auth resolver
- Update all security tests to use OAuthHandler instance methods
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement one-directional user blocking following the atProto write-forward
pattern. Blocks are written to the blocker's PDS repo and indexed via the
Jetstream firehose consumer into a new user_blocks table.
Changes:
- Add social.coves.actor.block lexicon and DB migration (029)
- Add userblocks domain package (service, repository, interfaces, errors)
- Add XRPC endpoints: blockUser, unblockUser, getBlockedUsers
- Extend Jetstream consumer to index block create/delete events
- Filter blocked users' posts from timeline, discover, and community feeds
- Filter blocked users' comments from comment threads
- Hydrate viewer.blocking on profile responses for authenticated viewers
- Refactor test helpers: DRY PDS client factories, atomic counters
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a new /api/me endpoint that returns the authenticated user's own
profile with stats. This provides a convenient way for the frontend to
fetch the current user's profile without needing to know their DID.
Changes:
- Add MeHandler with HandleMe serving GET /api/me
- Register route with RequireAuth middleware
- Generalize handleServiceError with operation parameter for reuse
- Add 6 unit tests covering success, nil stats, auth, errors, timeout
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update the build image to Go 1.25 and add the missing environment
variables for the OAuth confidential client feature added in c65da94.
Changes:
- Bump Go version from 1.24-alpine to 1.25-alpine in Dockerfile
- Add OAUTH_CLIENT_PRIVATE_KEY and OAUTH_CLIENT_KEY_ID env vars to
docker-compose.prod.yml with empty defaults for backward compatibility
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements OAuth confidential client authentication to enable 1-year session
lifetimes (vs 14 days for public clients). Auth servers enforce shorter limits
for public clients, so this upgrade is necessary for improved UX.
Changes:
- Add OAUTH_CLIENT_PRIVATE_KEY and OAUTH_CLIENT_KEY_ID env configuration
- Add /oauth-client-keys.json JWKS endpoint for public key discovery
- Expand OAuth scopes to include all Coves record types and blob uploads
- Add LogoURI and PolicyURI to client metadata for auth screen branding
- Add cmd/tools/generate-oauth-key utility for P-256 key generation
- Update session TTL defaults to 1 year / 18 months for confidential clients
- Add scope validation with warnings in callback handler
- Fix WebSocket timeout handling in integration tests to prevent panics
- Increase unique name entropy in tests to reduce collision probability
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update bluesky-social/indigo dependency to the latest version and adapt
to its API changes. Also simplify OAuth configuration by removing the
deprecated transition:generic scope.
Changes:
- Update Go version from 1.24.0 to 1.25
- Update indigo to v0.0.0-20260202181658-ea3d39eec464
- Fix DID parsing calls to use value type instead of pointer (indigo API change)
- Remove transition:generic from OAuth scopes (now using atproto only)
- Fix test isolation in CleanupExpiredSessions test
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix schema URL conflict between resource.Default() and our semconv.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Axiom uses OTLP/HTTP, not gRPC. The gRPC exporter was failing with
HTTP 464 errors.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Pass OpenTelemetry environment variables to appview container.
Disabled by default for self-hosters.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add pluggable observability infrastructure that supports any OTLP-compatible
backend (Jaeger, Axiom, Grafana, Honeycomb). Disabled by default with zero
overhead when not enabled.
Changes:
- Add internal/observability package with config, provider, and middleware
- Integrate tracing into main.go with graceful shutdown
- Add Jaeger service to docker-compose with 'observability' profile
- Add `make dev-up-otel` for local tracing with Jaeger UI
- Document OTEL_* environment variables in example files
- Add OpenTelemetry SDK dependencies (otel, otlptracegrpc, otelhttp)
Usage:
make dev-up-otel # Start dev stack with Jaeger
OTEL_ENABLED=true OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 make run
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When paginating with hot sort, posts created after the cursor timestamp
would cause a negative value in the POWER function base, resulting in
PostgreSQL error "a negative number raised to a non-integer power yields
a complex result".
Fixed by wrapping the base with GREATEST(..., 0.1) to ensure it's always
positive.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement two significant changes to improve content moderation and thread integrity:
1. Admin Reports System
- Add off-protocol content reporting for serious issues (CSAM, doxing, etc.)
- New endpoint: POST /xrpc/social.coves.admin.submitReport
- Rate limited to 10 requests/minute, requires OAuth authentication
- Full stack: handler, service, repository, and database migration
2. Preserve Deleted Comment Thread Structure
- Deleted comments now appear as "[deleted]" placeholders instead of being hidden
- Comment counts are preserved on deletion to maintain accurate thread totals
- Nested replies now properly increment root post's comment_count
- Improved resurrection handling to avoid double-counting when same parent
3. Error Handling Improvements
- serializeOptionalFields() now returns errors instead of silently ignoring failures
- Reconciliation failures now roll back transactions (previously just logged warnings)
- Post consumer uses errors.Is() for proper error type checking
Changes:
- Add internal/api/handlers/adminreport/* for report submission
- Add internal/core/adminreports/* for domain logic and errors
- Add internal/db/postgres/admin_report_repo.go for persistence
- Add migration 028_create_admin_reports_table.sql
- Update comment_consumer.go to preserve counts on delete
- Update comment_repo.go to include deleted comments in queries
- Update post_consumer.go error handling and nested reply counting
- Update tests to verify new placeholder behavior
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a comprehensive child safety standards page at /safety/child-safety
that outlines Coves' policies and practices for preventing, detecting,
and responding to child sexual abuse and exploitation (CSAE/CSAM).
Changes:
- Add GET /safety/child-safety route in web routes
- Add ChildSafetyHandler to render the safety page
- Create child-safety.html template with full policy documentation
The page covers prohibited content definitions, age requirements (18+),
reporting mechanisms, response procedures, AT Protocol federation
handling, and compliance with NCMEC reporting requirements.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add complete post deletion flow following atProto federation pattern:
Client → Service → PDS DeleteRecord → Jetstream → AppView soft-delete.
Changes:
- Add delete handler with XRPC error formatting
- Implement DeletePost service method with security checks
- Add SoftDelete to repository with idempotent behavior
- Handle delete events in Jetstream consumer
- Register POST /xrpc/social.coves.community.post.delete endpoint
Security: Author verification via PDS record fetch - only post author
can delete their own posts. Idempotent: deleting already-deleted posts
returns success.
Includes integration tests covering:
- Jetstream create→delete flow
- Authorization checks (attacker blocked)
- Idempotent delete behavior
- E2E test with real PDS and Jetstream
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Align PostView and CommentView with atProto patterns by accessing content
through the Record object instead of redundant top-level fields. This matches
the Bluesky approach where the record contains the authoritative content.
Changes:
- Remove content field from CommentView (access via Record.Content)
- Remove title/text fields from PostView (access via Record)
- Update lexicons to remove redundant field definitions
- Update comment_service, feed_repo_base, post_repo to stop setting removed fields
- Add test for deleted comments with nil Record
- Add helper functions in integration tests to extract content from Record
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove redundant contentFacets/textFacets from view structs to match
Bluesky's API pattern where facets are accessed via the record field.
This simplifies the API surface and eliminates data duplication.
Changes:
- Remove ContentFacets from CommentView, access via record.Facets
- Remove TextFacets from PostView, access via record.facets
- Update lexicon schemas (comment/defs.json, post/get.json)
- Replace log.Printf with structured slog.Warn/slog.Error throughout
- Convert hard errors for optional JSON fields to warning logs
- Update tests to use record-based facet access pattern
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add missing IMAGE_PROXY_* environment variables and persistent volume
for the image proxy cache. This fixes community profile avatars not
loading in production due to relative URLs being generated when
IMAGE_PROXY_BASE_URL was not set.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Migrate user profile handling from app.bsky.actor.profile to the Coves-specific
social.coves.actor.profile lexicon, and improve the OAuth authentication flow
with proper DPoP token handling.
Changes:
- Migrate profile collection from app.bsky.actor.profile to social.coves.actor.profile
- Refactor UpdateProfileHandler to use pds.Client interface with DPoP auth
- Add PDSClientFactory for dependency injection (enables password auth in E2E tests)
- Add ErrRateLimited (429) and ErrPayloadTooLarge (413) error handling
- Update Jetstream consumer and config to filter on Coves profile collection
- Add constructor validation with panic on nil dependencies
- Add 15+ new tests: nil handling, boundary conditions, error paths
- Refactor tests to use mock pds.Client instead of HTTP roundtrippers
- Fix logging levels for Jetstream consumer (nil data → WARN, cache purge → ERROR)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement a comprehensive image proxy service that fetches, resizes, and caches
images from AT Protocol Personal Data Servers. This enables optimized image
delivery with preset-based transformations for avatars, banners, and content.
Changes:
- Add imageproxy core package with multi-tier architecture (service, cache, fetcher, processor)
- Implement disk-based LRU cache with configurable TTL and background cleanup
- Add HTTP handler at /img/{preset}/plain/{did}/{cid} with ETag support
- Define 6 presets: avatar, avatar_small, banner, content_preview, content_full, embed_thumbnail
- Add DID and CID validation with proper error handling
- Integrate with communities to serve optimized avatar/banner URLs
- Add HydrateImageURL helper with smart proxy/direct URL selection
- Add comprehensive E2E and unit tests
- Configure via environment variables (IMAGE_PROXY_*)
- Add disintegration/imaging dependency for image processing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Communities API now returns properly structured view objects with
hydrated avatar/banner URLs instead of raw CIDs. This aligns the API
responses with the lexicon definitions (communityView and
communityViewDetailed) and ensures clients receive usable image URLs.
Changes:
- Add CommunityView and CommunityViewDetailed structs for API responses
- Add HydrateBlobURL helper to convert CIDs to PDS blob URLs
- Update get/list/search handlers to use view conversion methods
- Add proper validation for limit/cursor params in search endpoint
- Include pds_url in List/Search queries for blob URL hydration
- Fix feed/post repos to hydrate community avatar URLs
- Add comprehensive tests for HydrateBlobURL
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a Claude Code skill that reviews current changes, generates a
comprehensive commit message, and offers squash/regular merge options
to main with optional branch cleanup.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive user profile customization including avatar and banner
images, with full validation, PDS integration, and Jetstream event handling.
## User Profile Avatar/Banner Feature
- Database migration 027: add display_name, bio, avatar_cid, banner_cid columns
- POST /xrpc/social.coves.actor.updateProfile endpoint with blob upload
- Size limits: avatar (1MB), banner (2MB); MIME validation (png/jpeg/webp)
- Request body limit (10MB) and field length validation (64/256 chars)
- Jetstream consumer handles app.bsky.actor.profile commit events
- CID-to-URL transformation for profile images via PDS getBlob endpoint
- UpdateProfileInput struct for type-safe partial updates
- CHECK constraints in migration for display_name/bio lengths
- Improved PDS error handling with pdsError type
## Testing
- Repository tests: 8 new tests for UpdateProfile and queries
- Service tests: 11 new tests for profile operations
- Consumer tests: 20 new tests for Jetstream event handling
- Handler tests: 27 new tests including validation edge cases
- E2E integration tests: 4 new tests with real infrastructure
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Address PR review feedback for community list endpoint:
Critical fixes:
- Log JSON encoding errors instead of silently discarding them
- Add security test for subscribed=true without auth (returns 401)
- Add core functionality test for subscribed filter
Important fixes:
- Return consistent JSON errors (using writeError) for auth failures
- Return 400 for invalid limit parameter (e.g., limit=abc)
- Return 400 for invalid cursor parameter (non-numeric or negative)
New feature:
- Add subscribed=true filter to list only communities user is subscribed to
- Requires authentication when enabled
- Uses INNER JOIN with community_subscriptions table
Test coverage:
- TestListHandler_SubscribedWithoutAuth_Returns401
- TestListHandler_SubscribedWithAuth_FiltersCorrectly
- TestListHandler_InvalidLimit_Returns400
- TestListHandler_InvalidCursor_Returns400
- TestListHandler_ValidLimitBoundaries
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Increase HTTP timeout from 10s to 30s to handle slow CDNs and large images
- Add User-Agent header to avoid being blocked by CDNs that filter bot traffic
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace O(n) PDS pagination with O(1) cache lookups for vote existence
checks in CreateVote and DeleteVote. First operation populates cache
from PDS, subsequent operations use fast hashmap lookups.
- Add findExistingVoteWithCache for cache-first lookups
- Rename findExistingVote to findExistingVoteFromPDS (fallback only)
- Propagate auth errors immediately instead of attempting doomed fallback
- Update comments to accurately reflect complexity characteristics
Closes Coves-fqg
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Increase integration test timeout from 180s to 600s in Makefile
- Add uniqueTestID() helper for generating unique test handles/emails
- Fix generateTID() to use atomic counter for rapid successive calls
- Add cleanup between subtests in comment_consumer_test for isolation
- Fix table name: subscriptions -> community_subscriptions
- Add debug logging for test failures in out-of-order reconciliation tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add proactive token refresh that runs every 30 minutes to refresh
aggregator OAuth tokens before they expire. This prevents API failures
due to expired tokens when aggregators are idle.
- Add ListAggregatorsNeedingTokenRefresh to repository interface
- Implement RefreshExpiringTokens in APIKeyService with per-aggregator timeout
- Start background goroutine in main.go with proper shutdown handling
- Add comprehensive tests for the new refresh functionality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously limited to first 3 feed entries regardless of content type.
Now scans the entire feed to find the top 3 posts with streamable links,
ensuring we always get actual highlight videos rather than discussion posts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reduces highlight volume to prevent drowning out original content.
Only processes top N entries from Reddit's hot feed (configurable via
max_posts_per_run). Also reduces run frequency from every 30min to hourly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement full avatar and banner image support for community creation and updates:
- Add BlobOwner interface to break import cycle between blobs and communities
- Community and CommunityPDSAccount now implement BlobOwner for blob uploads
- CreateCommunity and UpdateCommunity now handle avatar/banner blob uploads to PDS
- Add avatarMimeType/bannerMimeType fields to request structs and lexicons
- Move authorization check before blob uploads to prevent orphaned blob DoS
- Add PDS response validation for blob uploads
- Fix Makefile db-migrate port to match .env.dev (5435)
Includes comprehensive E2E tests for avatar/banner CRUD operations via Jetstream.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Hot sort cursor:
- Remove hot_rank from cursor to avoid floating-point precision issues
- Use subquery to compare hot_ranks with identical SQL expressions
- Cursor format changed from hot_rank::created_at::uri::timestamp to
created_at::uri::timestamp
E2E test fixes:
- community_e2e_test: Create fresh PDS account instead of using
hardcoded credentials that don't exist
- Add consecutive timeout tracking to websocket subscriptions to
prevent gorilla/websocket panic after repeated reads on failed
connections
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update landing page test to check for "Coming soon" placeholder text
instead of expecting AppStoreURL/PlayStoreURL in rendered output
- Update delete account test to expect "Sign In" button text
- Update user repo test to match actual DID validation error message
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add /privacy route with privacy policy template covering:
- atProto data model and federation
- Data collection practices (minimal)
- 18+ age requirement
- Cloudflare as third-party service
- Contact information
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Kagi updates their RSS feeds throughout the day, not just at noon.
Running every 2 hours ensures we catch new articles promptly instead
of waiting up to 24 hours.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Some services (like PDS profile displays) look up /favicon.ico directly
rather than parsing HTML for favicon links. Add a multi-resolution ICO
file (48x48, 32x32, 16x16) with lil_dude mascot.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The og:image and OAuth client metadata were still using the old shark
logo. Replace with lil_dude crab mascot for consistent branding.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Integrate "Coming Soon" message into button text instead of using a
floating badge. Changes label from "Download on the" to "Coming soon to"
with coral accent color for better visual hierarchy.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add static directory to Dockerfile so images are included in container
- Update CSP in Caddyfile to allow Google Fonts (fonts.googleapis.com,
fonts.gstatic.com) and data: URIs for inline SVG textures
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Rewrote tagline and description to better reflect Coves' cooperative
model and future multi-tenant ownership vision.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reworked landing page with warm coral/teal color palette extracted
from brand SVGs. Switched to Shrikhand + Nunito fonts for a playful,
friendly feel. Added floating bubble decorations, subtle ocean
gradient, and improved mascot animation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Corrected step numbering that jumped from 3 to 5.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Coves-fqg: Documents the root cause of slow vote creation (~2-3s)
and initial feed loads (~800ms) due to repeated listRecords calls
to user's PDS for vote existence checks.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Split the mascot into separate body and claw images to enable a smooth
waving animation. The claw rotates from the shoulder joint for a natural
wave effect. Also improved mobile layout with better spacing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Register web frontend routes in main server
- Add post-login redirect support to OAuth handlers
- Store redirect URL in cookie during OAuth flow for delete-account
- Validate redirects are relative paths to prevent open redirect attacks
- Update user routes registration to include auth middleware
- Fix test mocks for actor handlers and comment service
- Update go.mod dependencies
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add landing page template with custom typography (Fraunces + Plus Jakarta Sans)
- Implement brand lockup with mascot + logo side-by-side layout
- Add ambient gradient background with noise texture overlay
- Include floating decorative blur elements with drift animation
- Add staggered fade-in animations for hero content
- Implement responsive design with mobile-first approach
- Add delete account and success page templates
- Include static assets (mascot, logo, app icon)
- Add web routes and handlers for serving pages
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Critical fixes:
- Remove duplicate DeleteAccountHandler in tests, use actual production handler
- Add comprehensive integration test for complete deletion flow
- Tests now use middleware.SetTestUserDID() instead of X-User-DID header
Error handling improvements:
- Add InvalidDIDError domain type for DID validation errors
- Handler returns specific errors for timeout (504) and cancellation (400)
- Marshal JSON before writing headers to catch encoding errors early
- Include DID in all repository error messages for debugging
Logging improvements:
- Replace log.Printf with slog.Error/Warn for structured logging
- Consistent slog usage across handler, service, and repository layers
Documentation improvements:
- Update interface comments to list all 8 tables including votes
- Fix CASCADE statement: "FK CASCADE deletes posts" (not votes)
- Add authorization context to DeleteAccount service documentation
Test additions:
- DID with leading/trailing whitespace handling
- Concurrent delete requests (race condition scenario)
- Integration test verifying all 8 tables are cleaned up
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add did:plc:jn4tlbpkdms5tahfrylct5g7 (Reddit Highlights aggregator)
to the TRUSTED_AGGREGATOR_DIDS list in development config.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, trusted aggregators that didn't provide a thumbnailUrl would
get no thumbnail because unfurl was completely skipped.
Now the logic is:
- If trusted aggregator provides thumbnailUrl → use it (unchanged)
- If trusted aggregator has no thumbnail AND URL is unfurl-supported →
fetch thumbnail via unfurl service
- Regular users get full unfurl as before
This allows the Reddit Highlights aggregator to get Streamable thumbnails
automatically via the backend unfurl service, without needing to fetch
them client-side.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add containerized deployment configuration:
- Dockerfile: Python 3.11-slim with cron support
- docker-compose.yml: Service definition with volume mounts
- docker-entrypoint.sh: Cron setup and environment handling
- crontab: 10-minute polling schedule
- .env.example: Environment variable template
Deployment uses the same pattern as the Kagi News aggregator.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add 137 tests covering all aggregator components:
- test_config.py: Config loading, validation, URL schemes, env overrides
- test_coves_client.py: API client, authentication, error handling
- test_link_extractor.py: URL detection, normalization, thumbnail fetching
- test_models.py: Data models, validation, immutability
- test_rss_fetcher.py: Feed fetching, retries, rate limit handling
- test_state_manager.py: State persistence, atomic writes, cleanup
All tests use mocking to avoid external dependencies.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add a new aggregator that pulls posts with streamable.com video links
from Reddit RSS feeds and posts them to Coves communities.
Core components:
- RSS feed fetcher with retry logic and exponential backoff
- Link extractor for detecting streamable.com URLs
- Coves API client with proper error handling
- State manager with atomic writes and corruption recovery
- Configuration loader with validation
Features:
- Configurable subreddit-to-community mappings
- Deduplication via persistent state
- Video embed metadata (embedType, provider, domain)
- Anti-detection jitter for Reddit rate limits
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Use errors.As instead of direct type assertion for proper wrapped error
handling, and handle additional close error codes (CloseGoingAway,
CloseAbnormalClosure) and EOF conditions to prevent gorilla/websocket
from panicking on subsequent reads.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change OAuth client_id from /oauth/client-metadata.json to
/oauth-client-metadata.json to match atproto's "conventional" pattern.
This causes the PDS OAuth authorization screen to display just
"coves.social" instead of the full URL path, providing a cleaner
user experience.
Per atproto spec, a "conventional" client_id must be:
- https:// protocol
- pathname exactly /oauth-client-metadata.json
- no port or query string
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The listCommunities endpoint was missing OptionalAuth middleware, which
meant GetUserDID() always returned empty string even for authenticated
requests. This caused viewer.subscribed to never be populated.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement a complete community suggestions feature allowing users to propose
ideas and vote on them. This is off-protocol (not stored on PDS/firehose)
and uses PostgreSQL directly for storage.
Changes:
- Add CRUD endpoints for community suggestions (create, get, list)
- Add voting with toggle semantics and atomic vote count updates
- Add admin-only status management (open/under_review/approved/declined)
- Add rate limiting: 3 suggestions/day per user, 10 req/min create, 30 req/min vote
- Add PostgreSQL migration (030) with community_suggestions and suggestion_votes tables
- Add repository with row-level locking for consistent vote counting
- Extract shared xrpc.WriteError() helper, refactor adminreport to use it
- Add Caddy proxy port (8080) to mobile port forwarding script
- Add comprehensive E2E integration tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
XRPC method names in atProto follow camelCase convention. The getProfile
endpoint was incorrectly using all-lowercase "getprofile".
Changes:
- Rename route from social.coves.actor.getprofile to social.coves.actor.getProfile
- Update startup log message, handler comment, and docs
- Update all E2E and integration test URLs and error messages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Narrow the Caddy /api/* catch-all to only proxy /api/me to the Go
backend, since SvelteKit now owns /api/auth/* and /api/proxy/*. Update
the dev script to auto-detect the coves-frontend directory (falling
back to kelp) and switch from npm to pnpm.
Changes:
- Replace broad /api/* proxy rule with specific /api/me route
- Add coves-frontend directory auto-detection in web-dev-run.sh
- Switch frontend dev server from npm to pnpm
- Normalize Caddyfile.dev indentation to tabs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Refactors the mobile-only OAuth redirect URI system into a configurable
allowlist that supports both mobile apps (custom schemes + Universal Links)
and web clients (HTTPS redirects). Adds Caddy-based reverse proxy for web
frontend development, enabling same-origin cookie sharing between Vite
frontend and Coves backend.
Changes:
- Refactor isAllowedMobileRedirectURI() into OAuthHandler.isAllowedRedirectURI()
with BuildAllowedRedirectURIs() builder for the configurable allowlist
- Add smart redirect: HTTPS clients get direct HTTP redirect, custom scheme
clients get the intermediate redirect page
- Add Caddyfile.dev reverse proxy config (Vite :5173 + Coves :8081 on :8080)
- Add scripts/web-dev-run.sh for combined backend+frontend dev startup
- Add Makefile targets: run-web, web-proxy, web-proxy-bg, web-proxy-stop
- Auto-run db-migrate before server start in make run and make run-web
- Update OAuth client comments to clarify ATProto loopback client_id spec
- Add PAR request debug logging in dev auth resolver
- Update all security tests to use OAuthHandler instance methods
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement one-directional user blocking following the atProto write-forward
pattern. Blocks are written to the blocker's PDS repo and indexed via the
Jetstream firehose consumer into a new user_blocks table.
Changes:
- Add social.coves.actor.block lexicon and DB migration (029)
- Add userblocks domain package (service, repository, interfaces, errors)
- Add XRPC endpoints: blockUser, unblockUser, getBlockedUsers
- Extend Jetstream consumer to index block create/delete events
- Filter blocked users' posts from timeline, discover, and community feeds
- Filter blocked users' comments from comment threads
- Hydrate viewer.blocking on profile responses for authenticated viewers
- Refactor test helpers: DRY PDS client factories, atomic counters
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a new /api/me endpoint that returns the authenticated user's own
profile with stats. This provides a convenient way for the frontend to
fetch the current user's profile without needing to know their DID.
Changes:
- Add MeHandler with HandleMe serving GET /api/me
- Register route with RequireAuth middleware
- Generalize handleServiceError with operation parameter for reuse
- Add 6 unit tests covering success, nil stats, auth, errors, timeout
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update the build image to Go 1.25 and add the missing environment
variables for the OAuth confidential client feature added in c65da94.
Changes:
- Bump Go version from 1.24-alpine to 1.25-alpine in Dockerfile
- Add OAUTH_CLIENT_PRIVATE_KEY and OAUTH_CLIENT_KEY_ID env vars to
docker-compose.prod.yml with empty defaults for backward compatibility
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements OAuth confidential client authentication to enable 1-year session
lifetimes (vs 14 days for public clients). Auth servers enforce shorter limits
for public clients, so this upgrade is necessary for improved UX.
Changes:
- Add OAUTH_CLIENT_PRIVATE_KEY and OAUTH_CLIENT_KEY_ID env configuration
- Add /oauth-client-keys.json JWKS endpoint for public key discovery
- Expand OAuth scopes to include all Coves record types and blob uploads
- Add LogoURI and PolicyURI to client metadata for auth screen branding
- Add cmd/tools/generate-oauth-key utility for P-256 key generation
- Update session TTL defaults to 1 year / 18 months for confidential clients
- Add scope validation with warnings in callback handler
- Fix WebSocket timeout handling in integration tests to prevent panics
- Increase unique name entropy in tests to reduce collision probability
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update bluesky-social/indigo dependency to the latest version and adapt
to its API changes. Also simplify OAuth configuration by removing the
deprecated transition:generic scope.
Changes:
- Update Go version from 1.24.0 to 1.25
- Update indigo to v0.0.0-20260202181658-ea3d39eec464
- Fix DID parsing calls to use value type instead of pointer (indigo API change)
- Remove transition:generic from OAuth scopes (now using atproto only)
- Fix test isolation in CleanupExpiredSessions test
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add pluggable observability infrastructure that supports any OTLP-compatible
backend (Jaeger, Axiom, Grafana, Honeycomb). Disabled by default with zero
overhead when not enabled.
Changes:
- Add internal/observability package with config, provider, and middleware
- Integrate tracing into main.go with graceful shutdown
- Add Jaeger service to docker-compose with 'observability' profile
- Add `make dev-up-otel` for local tracing with Jaeger UI
- Document OTEL_* environment variables in example files
- Add OpenTelemetry SDK dependencies (otel, otlptracegrpc, otelhttp)
Usage:
make dev-up-otel # Start dev stack with Jaeger
OTEL_ENABLED=true OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 make run
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When paginating with hot sort, posts created after the cursor timestamp
would cause a negative value in the POWER function base, resulting in
PostgreSQL error "a negative number raised to a non-integer power yields
a complex result".
Fixed by wrapping the base with GREATEST(..., 0.1) to ensure it's always
positive.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement two significant changes to improve content moderation and thread integrity:
1. Admin Reports System
- Add off-protocol content reporting for serious issues (CSAM, doxing, etc.)
- New endpoint: POST /xrpc/social.coves.admin.submitReport
- Rate limited to 10 requests/minute, requires OAuth authentication
- Full stack: handler, service, repository, and database migration
2. Preserve Deleted Comment Thread Structure
- Deleted comments now appear as "[deleted]" placeholders instead of being hidden
- Comment counts are preserved on deletion to maintain accurate thread totals
- Nested replies now properly increment root post's comment_count
- Improved resurrection handling to avoid double-counting when same parent
3. Error Handling Improvements
- serializeOptionalFields() now returns errors instead of silently ignoring failures
- Reconciliation failures now roll back transactions (previously just logged warnings)
- Post consumer uses errors.Is() for proper error type checking
Changes:
- Add internal/api/handlers/adminreport/* for report submission
- Add internal/core/adminreports/* for domain logic and errors
- Add internal/db/postgres/admin_report_repo.go for persistence
- Add migration 028_create_admin_reports_table.sql
- Update comment_consumer.go to preserve counts on delete
- Update comment_repo.go to include deleted comments in queries
- Update post_consumer.go error handling and nested reply counting
- Update tests to verify new placeholder behavior
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a comprehensive child safety standards page at /safety/child-safety
that outlines Coves' policies and practices for preventing, detecting,
and responding to child sexual abuse and exploitation (CSAE/CSAM).
Changes:
- Add GET /safety/child-safety route in web routes
- Add ChildSafetyHandler to render the safety page
- Create child-safety.html template with full policy documentation
The page covers prohibited content definitions, age requirements (18+),
reporting mechanisms, response procedures, AT Protocol federation
handling, and compliance with NCMEC reporting requirements.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add complete post deletion flow following atProto federation pattern:
Client → Service → PDS DeleteRecord → Jetstream → AppView soft-delete.
Changes:
- Add delete handler with XRPC error formatting
- Implement DeletePost service method with security checks
- Add SoftDelete to repository with idempotent behavior
- Handle delete events in Jetstream consumer
- Register POST /xrpc/social.coves.community.post.delete endpoint
Security: Author verification via PDS record fetch - only post author
can delete their own posts. Idempotent: deleting already-deleted posts
returns success.
Includes integration tests covering:
- Jetstream create→delete flow
- Authorization checks (attacker blocked)
- Idempotent delete behavior
- E2E test with real PDS and Jetstream
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Align PostView and CommentView with atProto patterns by accessing content
through the Record object instead of redundant top-level fields. This matches
the Bluesky approach where the record contains the authoritative content.
Changes:
- Remove content field from CommentView (access via Record.Content)
- Remove title/text fields from PostView (access via Record)
- Update lexicons to remove redundant field definitions
- Update comment_service, feed_repo_base, post_repo to stop setting removed fields
- Add test for deleted comments with nil Record
- Add helper functions in integration tests to extract content from Record
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove redundant contentFacets/textFacets from view structs to match
Bluesky's API pattern where facets are accessed via the record field.
This simplifies the API surface and eliminates data duplication.
Changes:
- Remove ContentFacets from CommentView, access via record.Facets
- Remove TextFacets from PostView, access via record.facets
- Update lexicon schemas (comment/defs.json, post/get.json)
- Replace log.Printf with structured slog.Warn/slog.Error throughout
- Convert hard errors for optional JSON fields to warning logs
- Update tests to use record-based facet access pattern
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Migrate user profile handling from app.bsky.actor.profile to the Coves-specific
social.coves.actor.profile lexicon, and improve the OAuth authentication flow
with proper DPoP token handling.
Changes:
- Migrate profile collection from app.bsky.actor.profile to social.coves.actor.profile
- Refactor UpdateProfileHandler to use pds.Client interface with DPoP auth
- Add PDSClientFactory for dependency injection (enables password auth in E2E tests)
- Add ErrRateLimited (429) and ErrPayloadTooLarge (413) error handling
- Update Jetstream consumer and config to filter on Coves profile collection
- Add constructor validation with panic on nil dependencies
- Add 15+ new tests: nil handling, boundary conditions, error paths
- Refactor tests to use mock pds.Client instead of HTTP roundtrippers
- Fix logging levels for Jetstream consumer (nil data → WARN, cache purge → ERROR)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement a comprehensive image proxy service that fetches, resizes, and caches
images from AT Protocol Personal Data Servers. This enables optimized image
delivery with preset-based transformations for avatars, banners, and content.
Changes:
- Add imageproxy core package with multi-tier architecture (service, cache, fetcher, processor)
- Implement disk-based LRU cache with configurable TTL and background cleanup
- Add HTTP handler at /img/{preset}/plain/{did}/{cid} with ETag support
- Define 6 presets: avatar, avatar_small, banner, content_preview, content_full, embed_thumbnail
- Add DID and CID validation with proper error handling
- Integrate with communities to serve optimized avatar/banner URLs
- Add HydrateImageURL helper with smart proxy/direct URL selection
- Add comprehensive E2E and unit tests
- Configure via environment variables (IMAGE_PROXY_*)
- Add disintegration/imaging dependency for image processing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Communities API now returns properly structured view objects with
hydrated avatar/banner URLs instead of raw CIDs. This aligns the API
responses with the lexicon definitions (communityView and
communityViewDetailed) and ensures clients receive usable image URLs.
Changes:
- Add CommunityView and CommunityViewDetailed structs for API responses
- Add HydrateBlobURL helper to convert CIDs to PDS blob URLs
- Update get/list/search handlers to use view conversion methods
- Add proper validation for limit/cursor params in search endpoint
- Include pds_url in List/Search queries for blob URL hydration
- Fix feed/post repos to hydrate community avatar URLs
- Add comprehensive tests for HydrateBlobURL
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive user profile customization including avatar and banner
images, with full validation, PDS integration, and Jetstream event handling.
## User Profile Avatar/Banner Feature
- Database migration 027: add display_name, bio, avatar_cid, banner_cid columns
- POST /xrpc/social.coves.actor.updateProfile endpoint with blob upload
- Size limits: avatar (1MB), banner (2MB); MIME validation (png/jpeg/webp)
- Request body limit (10MB) and field length validation (64/256 chars)
- Jetstream consumer handles app.bsky.actor.profile commit events
- CID-to-URL transformation for profile images via PDS getBlob endpoint
- UpdateProfileInput struct for type-safe partial updates
- CHECK constraints in migration for display_name/bio lengths
- Improved PDS error handling with pdsError type
## Testing
- Repository tests: 8 new tests for UpdateProfile and queries
- Service tests: 11 new tests for profile operations
- Consumer tests: 20 new tests for Jetstream event handling
- Handler tests: 27 new tests including validation edge cases
- E2E integration tests: 4 new tests with real infrastructure
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Address PR review feedback for community list endpoint:
Critical fixes:
- Log JSON encoding errors instead of silently discarding them
- Add security test for subscribed=true without auth (returns 401)
- Add core functionality test for subscribed filter
Important fixes:
- Return consistent JSON errors (using writeError) for auth failures
- Return 400 for invalid limit parameter (e.g., limit=abc)
- Return 400 for invalid cursor parameter (non-numeric or negative)
New feature:
- Add subscribed=true filter to list only communities user is subscribed to
- Requires authentication when enabled
- Uses INNER JOIN with community_subscriptions table
Test coverage:
- TestListHandler_SubscribedWithoutAuth_Returns401
- TestListHandler_SubscribedWithAuth_FiltersCorrectly
- TestListHandler_InvalidLimit_Returns400
- TestListHandler_InvalidCursor_Returns400
- TestListHandler_ValidLimitBoundaries
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace O(n) PDS pagination with O(1) cache lookups for vote existence
checks in CreateVote and DeleteVote. First operation populates cache
from PDS, subsequent operations use fast hashmap lookups.
- Add findExistingVoteWithCache for cache-first lookups
- Rename findExistingVote to findExistingVoteFromPDS (fallback only)
- Propagate auth errors immediately instead of attempting doomed fallback
- Update comments to accurately reflect complexity characteristics
Closes Coves-fqg
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Increase integration test timeout from 180s to 600s in Makefile
- Add uniqueTestID() helper for generating unique test handles/emails
- Fix generateTID() to use atomic counter for rapid successive calls
- Add cleanup between subtests in comment_consumer_test for isolation
- Fix table name: subscriptions -> community_subscriptions
- Add debug logging for test failures in out-of-order reconciliation tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add proactive token refresh that runs every 30 minutes to refresh
aggregator OAuth tokens before they expire. This prevents API failures
due to expired tokens when aggregators are idle.
- Add ListAggregatorsNeedingTokenRefresh to repository interface
- Implement RefreshExpiringTokens in APIKeyService with per-aggregator timeout
- Start background goroutine in main.go with proper shutdown handling
- Add comprehensive tests for the new refresh functionality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement full avatar and banner image support for community creation and updates:
- Add BlobOwner interface to break import cycle between blobs and communities
- Community and CommunityPDSAccount now implement BlobOwner for blob uploads
- CreateCommunity and UpdateCommunity now handle avatar/banner blob uploads to PDS
- Add avatarMimeType/bannerMimeType fields to request structs and lexicons
- Move authorization check before blob uploads to prevent orphaned blob DoS
- Add PDS response validation for blob uploads
- Fix Makefile db-migrate port to match .env.dev (5435)
Includes comprehensive E2E tests for avatar/banner CRUD operations via Jetstream.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Hot sort cursor:
- Remove hot_rank from cursor to avoid floating-point precision issues
- Use subquery to compare hot_ranks with identical SQL expressions
- Cursor format changed from hot_rank::created_at::uri::timestamp to
created_at::uri::timestamp
E2E test fixes:
- community_e2e_test: Create fresh PDS account instead of using
hardcoded credentials that don't exist
- Add consecutive timeout tracking to websocket subscriptions to
prevent gorilla/websocket panic after repeated reads on failed
connections
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update landing page test to check for "Coming soon" placeholder text
instead of expecting AppStoreURL/PlayStoreURL in rendered output
- Update delete account test to expect "Sign In" button text
- Update user repo test to match actual DID validation error message
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Rewrote tagline and description to better reflect Coves' cooperative
model and future multi-tenant ownership vision.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Register web frontend routes in main server
- Add post-login redirect support to OAuth handlers
- Store redirect URL in cookie during OAuth flow for delete-account
- Validate redirects are relative paths to prevent open redirect attacks
- Update user routes registration to include auth middleware
- Fix test mocks for actor handlers and comment service
- Update go.mod dependencies
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add landing page template with custom typography (Fraunces + Plus Jakarta Sans)
- Implement brand lockup with mascot + logo side-by-side layout
- Add ambient gradient background with noise texture overlay
- Include floating decorative blur elements with drift animation
- Add staggered fade-in animations for hero content
- Implement responsive design with mobile-first approach
- Add delete account and success page templates
- Include static assets (mascot, logo, app icon)
- Add web routes and handlers for serving pages
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Critical fixes:
- Remove duplicate DeleteAccountHandler in tests, use actual production handler
- Add comprehensive integration test for complete deletion flow
- Tests now use middleware.SetTestUserDID() instead of X-User-DID header
Error handling improvements:
- Add InvalidDIDError domain type for DID validation errors
- Handler returns specific errors for timeout (504) and cancellation (400)
- Marshal JSON before writing headers to catch encoding errors early
- Include DID in all repository error messages for debugging
Logging improvements:
- Replace log.Printf with slog.Error/Warn for structured logging
- Consistent slog usage across handler, service, and repository layers
Documentation improvements:
- Update interface comments to list all 8 tables including votes
- Fix CASCADE statement: "FK CASCADE deletes posts" (not votes)
- Add authorization context to DeleteAccount service documentation
Test additions:
- DID with leading/trailing whitespace handling
- Concurrent delete requests (race condition scenario)
- Integration test verifying all 8 tables are cleaned up
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, trusted aggregators that didn't provide a thumbnailUrl would
get no thumbnail because unfurl was completely skipped.
Now the logic is:
- If trusted aggregator provides thumbnailUrl → use it (unchanged)
- If trusted aggregator has no thumbnail AND URL is unfurl-supported →
fetch thumbnail via unfurl service
- Regular users get full unfurl as before
This allows the Reddit Highlights aggregator to get Streamable thumbnails
automatically via the backend unfurl service, without needing to fetch
them client-side.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add containerized deployment configuration:
- Dockerfile: Python 3.11-slim with cron support
- docker-compose.yml: Service definition with volume mounts
- docker-entrypoint.sh: Cron setup and environment handling
- crontab: 10-minute polling schedule
- .env.example: Environment variable template
Deployment uses the same pattern as the Kagi News aggregator.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add 137 tests covering all aggregator components:
- test_config.py: Config loading, validation, URL schemes, env overrides
- test_coves_client.py: API client, authentication, error handling
- test_link_extractor.py: URL detection, normalization, thumbnail fetching
- test_models.py: Data models, validation, immutability
- test_rss_fetcher.py: Feed fetching, retries, rate limit handling
- test_state_manager.py: State persistence, atomic writes, cleanup
All tests use mocking to avoid external dependencies.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add a new aggregator that pulls posts with streamable.com video links
from Reddit RSS feeds and posts them to Coves communities.
Core components:
- RSS feed fetcher with retry logic and exponential backoff
- Link extractor for detecting streamable.com URLs
- Coves API client with proper error handling
- State manager with atomic writes and corruption recovery
- Configuration loader with validation
Features:
- Configurable subreddit-to-community mappings
- Deduplication via persistent state
- Video embed metadata (embedType, provider, domain)
- Anti-detection jitter for Reddit rate limits
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Use errors.As instead of direct type assertion for proper wrapped error
handling, and handle additional close error codes (CloseGoingAway,
CloseAbnormalClosure) and EOF conditions to prevent gorilla/websocket
from panicking on subsequent reads.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change OAuth client_id from /oauth/client-metadata.json to
/oauth-client-metadata.json to match atproto's "conventional" pattern.
This causes the PDS OAuth authorization screen to display just
"coves.social" instead of the full URL path, providing a cleaner
user experience.
Per atproto spec, a "conventional" client_id must be:
- https:// protocol
- pathname exactly /oauth-client-metadata.json
- no port or query string
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The listCommunities endpoint was missing OptionalAuth middleware, which
meant GetUserDID() always returned empty string even for authenticated
requests. This caused viewer.subscribed to never be populated.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>