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 84 // Validation helpers 85 85 ValidateHandle(handle string) error 86 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) 87 93 }
+25 -10
internal/core/communities/service.go
··· 244 244 return nil, NewValidationError("identifier", "must be a DID or handle") 245 245 } 246 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 + 247 261 // UpdateCommunity updates a community via write-forward to PDS 248 262 func (s *communityService) UpdateCommunity(ctx context.Context, req UpdateCommunityRequest) (*Community, error) { 249 263 if req.CommunityDID == "" { ··· 262 276 263 277 // CRITICAL: Ensure fresh PDS access token before write operation 264 278 // Community PDS tokens expire every ~2 hours and must be refreshed 265 - existing, err = s.ensureFreshToken(ctx, existing) 279 + existing, err = s.EnsureFreshToken(ctx, existing) 266 280 if err != nil { 267 281 return nil, fmt.Errorf("failed to ensure fresh credentials: %w", err) 268 282 } ··· 426 440 // ensureFreshToken checks if a community's access token needs refresh and updates if needed 427 441 // Returns updated community with fresh credentials (or original if no refresh needed) 428 442 // Thread-safe: Uses per-community mutex to prevent concurrent refresh attempts 429 - func (s *communityService) ensureFreshToken(ctx context.Context, community *Community) (*Community, error) { 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) { 430 446 // Get or create mutex for this specific community DID 431 447 mutex := s.getOrCreateRefreshMutex(community.DID) 432 448 ··· 834 850 // Following Bluesky's pattern with Coves extensions: 835 851 // 836 852 // 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) 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) 840 856 // 841 857 // Coves-specific extensions: 842 - // 4. Scoped format: !gardening@coves.social (parse and resolve) 858 + // 4. Scoped format: !gardening@coves.social (parse and resolve) 843 859 // 844 860 // Returns: DID string 845 861 func (s *communityService) ResolveCommunityIdentifier(ctx context.Context, identifier string) (string, error) { ··· 867 883 } 868 884 869 885 // 3. At-identifier format: @handle (Bluesky standard - strip @ prefix) 870 - if strings.HasPrefix(identifier, "@") { 871 - identifier = strings.TrimPrefix(identifier, "@") 872 - } 886 + identifier = strings.TrimPrefix(identifier, "@") 873 887 874 888 // 4. Canonical handle: name.community.instance.com (Bluesky standard) 875 889 if strings.Contains(identifier, ".") { ··· 885 899 886 900 // resolveScopedIdentifier handles Coves-specific !name@instance format 887 901 // Formats accepted: 888 - // !gardening@coves.social -> gardening.community.coves.social 902 + // 903 + // !gardening@coves.social -> gardening.community.coves.social 889 904 func (s *communityService) resolveScopedIdentifier(ctx context.Context, scoped string) (string, error) { 890 905 // Remove ! prefix 891 906 scoped = strings.TrimPrefix(scoped, "!")