A community based topic aggregation platform built on atproto

feat(communities): export methods for post service integration

Export community service methods for post creation:
- EnsureFreshToken() - Auto-refresh PDS tokens before write operations
- GetByDID() - Direct repository access for post service

These methods enable posts service to:
1. Fetch community from AppView by DID
2. Ensure fresh PDS credentials before writing to community repo
3. Use community's access token for PDS write-forward

Changes:
- Made ensureFreshToken() public as EnsureFreshToken()
- Added GetByDID() wrapper for repository access
- No functional changes, just visibility

Supports write-forward architecture where posts are written to
community's PDS repository using community's credentials.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

+31 -10
+6
internal/core/communities/interfaces.go
··· 84 // Validation helpers 85 ValidateHandle(handle string) error 86 ResolveCommunityIdentifier(ctx context.Context, identifier string) (string, error) // Returns DID from handle or DID 87 }
··· 84 // Validation helpers 85 ValidateHandle(handle string) error 86 ResolveCommunityIdentifier(ctx context.Context, identifier string) (string, error) // Returns DID from handle or DID 87 + 88 + // Token management (for post service to use when writing to community repos) 89 + EnsureFreshToken(ctx context.Context, community *Community) (*Community, error) 90 + 91 + // Direct repository access (for post service) 92 + GetByDID(ctx context.Context, did string) (*Community, error) 93 }
+25 -10
internal/core/communities/service.go
··· 244 return nil, NewValidationError("identifier", "must be a DID or handle") 245 } 246 247 // UpdateCommunity updates a community via write-forward to PDS 248 func (s *communityService) UpdateCommunity(ctx context.Context, req UpdateCommunityRequest) (*Community, error) { 249 if req.CommunityDID == "" { ··· 262 263 // CRITICAL: Ensure fresh PDS access token before write operation 264 // Community PDS tokens expire every ~2 hours and must be refreshed 265 - existing, err = s.ensureFreshToken(ctx, existing) 266 if err != nil { 267 return nil, fmt.Errorf("failed to ensure fresh credentials: %w", err) 268 } ··· 426 // ensureFreshToken checks if a community's access token needs refresh and updates if needed 427 // Returns updated community with fresh credentials (or original if no refresh needed) 428 // Thread-safe: Uses per-community mutex to prevent concurrent refresh attempts 429 - func (s *communityService) ensureFreshToken(ctx context.Context, community *Community) (*Community, error) { 430 // Get or create mutex for this specific community DID 431 mutex := s.getOrCreateRefreshMutex(community.DID) 432 ··· 834 // Following Bluesky's pattern with Coves extensions: 835 // 836 // Accepts (like Bluesky's at-identifier): 837 - // 1. DID: did:plc:abc123 (pass through) 838 - // 2. Canonical handle: gardening.community.coves.social (atProto standard) 839 - // 3. At-identifier: @gardening.community.coves.social (strip @ prefix) 840 // 841 // Coves-specific extensions: 842 - // 4. Scoped format: !gardening@coves.social (parse and resolve) 843 // 844 // Returns: DID string 845 func (s *communityService) ResolveCommunityIdentifier(ctx context.Context, identifier string) (string, error) { ··· 867 } 868 869 // 3. At-identifier format: @handle (Bluesky standard - strip @ prefix) 870 - if strings.HasPrefix(identifier, "@") { 871 - identifier = strings.TrimPrefix(identifier, "@") 872 - } 873 874 // 4. Canonical handle: name.community.instance.com (Bluesky standard) 875 if strings.Contains(identifier, ".") { ··· 885 886 // resolveScopedIdentifier handles Coves-specific !name@instance format 887 // Formats accepted: 888 - // !gardening@coves.social -> gardening.community.coves.social 889 func (s *communityService) resolveScopedIdentifier(ctx context.Context, scoped string) (string, error) { 890 // Remove ! prefix 891 scoped = strings.TrimPrefix(scoped, "!")
··· 244 return nil, NewValidationError("identifier", "must be a DID or handle") 245 } 246 247 + // GetByDID retrieves a community by its DID 248 + // Exported for use by post service when validating community references 249 + func (s *communityService) GetByDID(ctx context.Context, did string) (*Community, error) { 250 + if did == "" { 251 + return nil, ErrInvalidInput 252 + } 253 + 254 + if !strings.HasPrefix(did, "did:") { 255 + return nil, NewValidationError("did", "must be a valid DID") 256 + } 257 + 258 + return s.repo.GetByDID(ctx, did) 259 + } 260 + 261 // UpdateCommunity updates a community via write-forward to PDS 262 func (s *communityService) UpdateCommunity(ctx context.Context, req UpdateCommunityRequest) (*Community, error) { 263 if req.CommunityDID == "" { ··· 276 277 // CRITICAL: Ensure fresh PDS access token before write operation 278 // Community PDS tokens expire every ~2 hours and must be refreshed 279 + existing, err = s.EnsureFreshToken(ctx, existing) 280 if err != nil { 281 return nil, fmt.Errorf("failed to ensure fresh credentials: %w", err) 282 } ··· 440 // ensureFreshToken checks if a community's access token needs refresh and updates if needed 441 // Returns updated community with fresh credentials (or original if no refresh needed) 442 // Thread-safe: Uses per-community mutex to prevent concurrent refresh attempts 443 + // EnsureFreshToken ensures the community's PDS access token is valid 444 + // Exported for use by post service when writing posts to community repos 445 + func (s *communityService) EnsureFreshToken(ctx context.Context, community *Community) (*Community, error) { 446 // Get or create mutex for this specific community DID 447 mutex := s.getOrCreateRefreshMutex(community.DID) 448 ··· 850 // Following Bluesky's pattern with Coves extensions: 851 // 852 // Accepts (like Bluesky's at-identifier): 853 + // 1. DID: did:plc:abc123 (pass through) 854 + // 2. Canonical handle: gardening.community.coves.social (atProto standard) 855 + // 3. At-identifier: @gardening.community.coves.social (strip @ prefix) 856 // 857 // Coves-specific extensions: 858 + // 4. Scoped format: !gardening@coves.social (parse and resolve) 859 // 860 // Returns: DID string 861 func (s *communityService) ResolveCommunityIdentifier(ctx context.Context, identifier string) (string, error) { ··· 883 } 884 885 // 3. At-identifier format: @handle (Bluesky standard - strip @ prefix) 886 + identifier = strings.TrimPrefix(identifier, "@") 887 888 // 4. Canonical handle: name.community.instance.com (Bluesky standard) 889 if strings.Contains(identifier, ".") { ··· 899 900 // resolveScopedIdentifier handles Coves-specific !name@instance format 901 // Formats accepted: 902 + // 903 + // !gardening@coves.social -> gardening.community.coves.social 904 func (s *communityService) resolveScopedIdentifier(ctx context.Context, scoped string) (string, error) { 905 // Remove ! prefix 906 scoped = strings.TrimPrefix(scoped, "!")