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>

Changed files
+14 -12
internal
atproto
core
communities
+5 -5
internal/atproto/jetstream/community_consumer.go
···
// Extract domain from community handle
// Handle format examples:
// - "!gaming@coves.social" โ†’ domain: "coves.social"
-
// - "gaming.communities.coves.social" โ†’ domain: "coves.social"
handleDomain := extractDomainFromHandle(handle)
if handleDomain == "" {
return fmt.Errorf("failed to extract domain from handle: %s", handle)
···
// extractDomainFromHandle extracts the registrable domain from a community handle
// Handles both formats:
// - Bluesky-style: "!gaming@coves.social" โ†’ "coves.social"
-
// - DNS-style: "gaming.communities.coves.social" โ†’ "coves.social"
//
// Uses golang.org/x/net/publicsuffix to correctly handle multi-part TLDs:
-
// - "gaming.communities.coves.co.uk" โ†’ "coves.co.uk" (not "co.uk")
-
// - "gaming.communities.example.com.au" โ†’ "example.com.au" (not "com.au")
func extractDomainFromHandle(handle string) string {
// Remove leading ! if present
handle = strings.TrimPrefix(handle, "!")
···
return ""
}
-
// For DNS-style handles (e.g., "gaming.communities.coves.social")
// Extract the registrable domain (eTLD+1) using publicsuffix
// This correctly handles multi-part TLDs like .co.uk, .com.au, etc.
registrable, err := publicsuffix.EffectiveTLDPlusOne(handle)
···
// Extract domain from community handle
// Handle format examples:
// - "!gaming@coves.social" โ†’ domain: "coves.social"
+
// - "gaming.community.coves.social" โ†’ domain: "coves.social"
handleDomain := extractDomainFromHandle(handle)
if handleDomain == "" {
return fmt.Errorf("failed to extract domain from handle: %s", handle)
···
// extractDomainFromHandle extracts the registrable domain from a community handle
// Handles both formats:
// - Bluesky-style: "!gaming@coves.social" โ†’ "coves.social"
+
// - DNS-style: "gaming.community.coves.social" โ†’ "coves.social"
//
// Uses golang.org/x/net/publicsuffix to correctly handle multi-part TLDs:
+
// - "gaming.community.coves.co.uk" โ†’ "coves.co.uk" (not "co.uk")
+
// - "gaming.community.example.com.au" โ†’ "example.com.au" (not "com.au")
func extractDomainFromHandle(handle string) string {
// Remove leading ! if present
handle = strings.TrimPrefix(handle, "!")
···
return ""
}
+
// For DNS-style handles (e.g., "gaming.community.coves.social")
// Extract the registrable domain (eTLD+1) using publicsuffix
// This correctly handles multi-part TLDs like .co.uk, .com.au, etc.
registrable, err := publicsuffix.EffectiveTLDPlusOne(handle)
+9 -7
internal/core/communities/pds_provisioning.go
···
// CommunityPDSAccount represents PDS account credentials for a community
type CommunityPDSAccount struct {
DID string // Community's DID (owns the repository)
-
Handle string // Community's handle (e.g., gaming.communities.coves.social)
Email string // System email for PDS account
Password string // Cleartext password (MUST be encrypted before database storage)
AccessToken string // JWT for making API calls as the community
···
}
// 1. Generate unique handle for the community
-
// Format: {name}.communities.{instance-domain}
-
// Example: "gaming.communities.coves.social"
-
handle := fmt.Sprintf("%s.communities.%s", strings.ToLower(communityName), p.instanceDomain)
// 2. Generate system email for PDS account management
// This email is used for account operations, not for user communication
-
email := fmt.Sprintf("community-%s@communities.%s", strings.ToLower(communityName), p.instanceDomain)
// 3. Generate secure random password (32 characters)
// This password is never shown to users - it's for Coves to authenticate as the community
···
// The repository layer handles encryption using pgp_sym_encrypt()
return &CommunityPDSAccount{
DID: output.Did, // The community's DID (PDS-generated)
-
Handle: output.Handle, // e.g., gaming.communities.coves.social
-
Email: email, // community-gaming@communities.coves.social
Password: password, // Cleartext - will be encrypted by repository
AccessToken: output.AccessJwt, // JWT for making API calls
RefreshToken: output.RefreshJwt, // For refreshing sessions
···
// CommunityPDSAccount represents PDS account credentials for a community
type CommunityPDSAccount struct {
DID string // Community's DID (owns the repository)
+
Handle string // Community's handle (e.g., gaming.community.coves.social)
Email string // System email for PDS account
Password string // Cleartext password (MUST be encrypted before database storage)
AccessToken string // JWT for making API calls as the community
···
}
// 1. Generate unique handle for the community
+
// Format: {name}.community.{instance-domain}
+
// Example: "gaming.community.coves.social"
+
// NOTE: Using SINGULAR "community" to follow atProto lexicon conventions
+
// (all record types use singular: app.bsky.feed.post, app.bsky.graph.follow, etc.)
+
handle := fmt.Sprintf("%s.community.%s", strings.ToLower(communityName), p.instanceDomain)
// 2. Generate system email for PDS account management
// This email is used for account operations, not for user communication
+
email := fmt.Sprintf("community-%s@community.%s", strings.ToLower(communityName), p.instanceDomain)
// 3. Generate secure random password (32 characters)
// This password is never shown to users - it's for Coves to authenticate as the community
···
// The repository layer handles encryption using pgp_sym_encrypt()
return &CommunityPDSAccount{
DID: output.Did, // The community's DID (PDS-generated)
+
Handle: output.Handle, // e.g., gaming.community.coves.social
+
Email: email, // community-gaming@community.coves.social
Password: password, // Cleartext - will be encrypted by repository
AccessToken: output.AccessJwt, // JWT for making API calls
RefreshToken: output.RefreshJwt, // For refreshing sessions