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>

Changed files
+31 -10
internal
core
+6
internal/core/communities/interfaces.go
···
// Validation helpers
ValidateHandle(handle string) error
ResolveCommunityIdentifier(ctx context.Context, identifier string) (string, error) // Returns DID from handle or DID
}
···
// Validation helpers
ValidateHandle(handle string) error
ResolveCommunityIdentifier(ctx context.Context, identifier string) (string, error) // Returns DID from handle or DID
+
+
// Token management (for post service to use when writing to community repos)
+
EnsureFreshToken(ctx context.Context, community *Community) (*Community, error)
+
+
// Direct repository access (for post service)
+
GetByDID(ctx context.Context, did string) (*Community, error)
}
+25 -10
internal/core/communities/service.go
···
return nil, NewValidationError("identifier", "must be a DID or handle")
}
// UpdateCommunity updates a community via write-forward to PDS
func (s *communityService) UpdateCommunity(ctx context.Context, req UpdateCommunityRequest) (*Community, error) {
if req.CommunityDID == "" {
···
// CRITICAL: Ensure fresh PDS access token before write operation
// Community PDS tokens expire every ~2 hours and must be refreshed
-
existing, err = s.ensureFreshToken(ctx, existing)
if err != nil {
return nil, fmt.Errorf("failed to ensure fresh credentials: %w", err)
}
···
// ensureFreshToken checks if a community's access token needs refresh and updates if needed
// Returns updated community with fresh credentials (or original if no refresh needed)
// Thread-safe: Uses per-community mutex to prevent concurrent refresh attempts
-
func (s *communityService) ensureFreshToken(ctx context.Context, community *Community) (*Community, error) {
// Get or create mutex for this specific community DID
mutex := s.getOrCreateRefreshMutex(community.DID)
···
// Following Bluesky's pattern with Coves extensions:
//
// Accepts (like Bluesky's at-identifier):
-
// 1. DID: did:plc:abc123 (pass through)
-
// 2. Canonical handle: gardening.community.coves.social (atProto standard)
-
// 3. At-identifier: @gardening.community.coves.social (strip @ prefix)
//
// Coves-specific extensions:
-
// 4. Scoped format: !gardening@coves.social (parse and resolve)
//
// Returns: DID string
func (s *communityService) ResolveCommunityIdentifier(ctx context.Context, identifier string) (string, error) {
···
}
// 3. At-identifier format: @handle (Bluesky standard - strip @ prefix)
-
if strings.HasPrefix(identifier, "@") {
-
identifier = strings.TrimPrefix(identifier, "@")
-
}
// 4. Canonical handle: name.community.instance.com (Bluesky standard)
if strings.Contains(identifier, ".") {
···
// resolveScopedIdentifier handles Coves-specific !name@instance format
// Formats accepted:
-
// !gardening@coves.social -> gardening.community.coves.social
func (s *communityService) resolveScopedIdentifier(ctx context.Context, scoped string) (string, error) {
// Remove ! prefix
scoped = strings.TrimPrefix(scoped, "!")
···
return nil, NewValidationError("identifier", "must be a DID or handle")
}
+
// GetByDID retrieves a community by its DID
+
// Exported for use by post service when validating community references
+
func (s *communityService) GetByDID(ctx context.Context, did string) (*Community, error) {
+
if did == "" {
+
return nil, ErrInvalidInput
+
}
+
+
if !strings.HasPrefix(did, "did:") {
+
return nil, NewValidationError("did", "must be a valid DID")
+
}
+
+
return s.repo.GetByDID(ctx, did)
+
}
+
// UpdateCommunity updates a community via write-forward to PDS
func (s *communityService) UpdateCommunity(ctx context.Context, req UpdateCommunityRequest) (*Community, error) {
if req.CommunityDID == "" {
···
// CRITICAL: Ensure fresh PDS access token before write operation
// Community PDS tokens expire every ~2 hours and must be refreshed
+
existing, err = s.EnsureFreshToken(ctx, existing)
if err != nil {
return nil, fmt.Errorf("failed to ensure fresh credentials: %w", err)
}
···
// ensureFreshToken checks if a community's access token needs refresh and updates if needed
// Returns updated community with fresh credentials (or original if no refresh needed)
// Thread-safe: Uses per-community mutex to prevent concurrent refresh attempts
+
// EnsureFreshToken ensures the community's PDS access token is valid
+
// Exported for use by post service when writing posts to community repos
+
func (s *communityService) EnsureFreshToken(ctx context.Context, community *Community) (*Community, error) {
// Get or create mutex for this specific community DID
mutex := s.getOrCreateRefreshMutex(community.DID)
···
// Following Bluesky's pattern with Coves extensions:
//
// Accepts (like Bluesky's at-identifier):
+
// 1. DID: did:plc:abc123 (pass through)
+
// 2. Canonical handle: gardening.community.coves.social (atProto standard)
+
// 3. At-identifier: @gardening.community.coves.social (strip @ prefix)
//
// Coves-specific extensions:
+
// 4. Scoped format: !gardening@coves.social (parse and resolve)
//
// Returns: DID string
func (s *communityService) ResolveCommunityIdentifier(ctx context.Context, identifier string) (string, error) {
···
}
// 3. At-identifier format: @handle (Bluesky standard - strip @ prefix)
+
identifier = strings.TrimPrefix(identifier, "@")
// 4. Canonical handle: name.community.instance.com (Bluesky standard)
if strings.Contains(identifier, ".") {
···
// resolveScopedIdentifier handles Coves-specific !name@instance format
// Formats accepted:
+
//
+
// !gardening@coves.social -> gardening.community.coves.social
func (s *communityService) resolveScopedIdentifier(ctx context.Context, scoped string) (string, error) {
// Remove ! prefix
scoped = strings.TrimPrefix(scoped, "!")