A community based topic aggregation platform built on atproto

refactor: update handle generation to use .community. format

Update PDS provisioning and Jetstream consumer to generate handles
using singular .community. instead of .communities.

Changes:
- PDSAccountProvisioner: Generate {name}.community.{domain} handles
- JetstreamConsumer: Parse and validate new handle format
- Update handle extraction logic for consistency

Example handle formats:
- gardening.community.coves.social
- gaming.community.coves.social

This maintains consistency with the lexicon schema update and aligns
with AT Protocol naming conventions.

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

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

+14 -12
+5 -5
internal/atproto/jetstream/community_consumer.go
··· 312 312 // Extract domain from community handle 313 313 // Handle format examples: 314 314 // - "!gaming@coves.social" โ†’ domain: "coves.social" 315 - // - "gaming.communities.coves.social" โ†’ domain: "coves.social" 315 + // - "gaming.community.coves.social" โ†’ domain: "coves.social" 316 316 handleDomain := extractDomainFromHandle(handle) 317 317 if handleDomain == "" { 318 318 return fmt.Errorf("failed to extract domain from handle: %s", handle) ··· 430 430 // extractDomainFromHandle extracts the registrable domain from a community handle 431 431 // Handles both formats: 432 432 // - Bluesky-style: "!gaming@coves.social" โ†’ "coves.social" 433 - // - DNS-style: "gaming.communities.coves.social" โ†’ "coves.social" 433 + // - DNS-style: "gaming.community.coves.social" โ†’ "coves.social" 434 434 // 435 435 // Uses golang.org/x/net/publicsuffix to correctly handle multi-part TLDs: 436 - // - "gaming.communities.coves.co.uk" โ†’ "coves.co.uk" (not "co.uk") 437 - // - "gaming.communities.example.com.au" โ†’ "example.com.au" (not "com.au") 436 + // - "gaming.community.coves.co.uk" โ†’ "coves.co.uk" (not "co.uk") 437 + // - "gaming.community.example.com.au" โ†’ "example.com.au" (not "com.au") 438 438 func extractDomainFromHandle(handle string) string { 439 439 // Remove leading ! if present 440 440 handle = strings.TrimPrefix(handle, "!") ··· 456 456 return "" 457 457 } 458 458 459 - // For DNS-style handles (e.g., "gaming.communities.coves.social") 459 + // For DNS-style handles (e.g., "gaming.community.coves.social") 460 460 // Extract the registrable domain (eTLD+1) using publicsuffix 461 461 // This correctly handles multi-part TLDs like .co.uk, .com.au, etc. 462 462 registrable, err := publicsuffix.EffectiveTLDPlusOne(handle)
+9 -7
internal/core/communities/pds_provisioning.go
··· 15 15 // CommunityPDSAccount represents PDS account credentials for a community 16 16 type CommunityPDSAccount struct { 17 17 DID string // Community's DID (owns the repository) 18 - Handle string // Community's handle (e.g., gaming.communities.coves.social) 18 + Handle string // Community's handle (e.g., gaming.community.coves.social) 19 19 Email string // System email for PDS account 20 20 Password string // Cleartext password (MUST be encrypted before database storage) 21 21 AccessToken string // JWT for making API calls as the community ··· 67 67 } 68 68 69 69 // 1. Generate unique handle for the community 70 - // Format: {name}.communities.{instance-domain} 71 - // Example: "gaming.communities.coves.social" 72 - handle := fmt.Sprintf("%s.communities.%s", strings.ToLower(communityName), p.instanceDomain) 70 + // Format: {name}.community.{instance-domain} 71 + // Example: "gaming.community.coves.social" 72 + // NOTE: Using SINGULAR "community" to follow atProto lexicon conventions 73 + // (all record types use singular: app.bsky.feed.post, app.bsky.graph.follow, etc.) 74 + handle := fmt.Sprintf("%s.community.%s", strings.ToLower(communityName), p.instanceDomain) 73 75 74 76 // 2. Generate system email for PDS account management 75 77 // This email is used for account operations, not for user communication 76 - email := fmt.Sprintf("community-%s@communities.%s", strings.ToLower(communityName), p.instanceDomain) 78 + email := fmt.Sprintf("community-%s@community.%s", strings.ToLower(communityName), p.instanceDomain) 77 79 78 80 // 3. Generate secure random password (32 characters) 79 81 // This password is never shown to users - it's for Coves to authenticate as the community ··· 116 118 // The repository layer handles encryption using pgp_sym_encrypt() 117 119 return &CommunityPDSAccount{ 118 120 DID: output.Did, // The community's DID (PDS-generated) 119 - Handle: output.Handle, // e.g., gaming.communities.coves.social 120 - Email: email, // community-gaming@communities.coves.social 121 + Handle: output.Handle, // e.g., gaming.community.coves.social 122 + Email: email, // community-gaming@community.coves.social 121 123 Password: password, // Cleartext - will be encrypted by repository 122 124 AccessToken: output.AccessJwt, // JWT for making API calls 123 125 RefreshToken: output.RefreshJwt, // For refreshing sessions