···
return s.repo.ListMembers(ctx, communityDID, limit, offset)
519
+
// BlockCommunity blocks a community via write-forward to PDS
520
+
func (s *communityService) BlockCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) (*CommunityBlock, error) {
522
+
return nil, NewValidationError("userDid", "required")
524
+
if userAccessToken == "" {
525
+
return nil, NewValidationError("userAccessToken", "required")
528
+
// Resolve community identifier
529
+
communityDID, err := s.ResolveCommunityIdentifier(ctx, communityIdentifier)
534
+
// Verify community exists
535
+
_, err = s.repo.GetByDID(ctx, communityDID)
540
+
// Build block record
541
+
// CRITICAL: Collection is social.coves.community.block (RECORD TYPE)
542
+
// This record will be created in the USER's repository: at://user_did/social.coves.community.block/{tid}
543
+
// Following atProto conventions and Bluesky's app.bsky.graph.block pattern
544
+
blockRecord := map[string]interface{}{
545
+
"$type": "social.coves.community.block",
546
+
"subject": communityDID, // DID of community being blocked
547
+
"createdAt": time.Now().Format(time.RFC3339),
550
+
// Write-forward: create block record in user's repo using their access token
551
+
// Note: We don't check for existing blocks first because:
552
+
// 1. The PDS may reject duplicates (depending on implementation)
553
+
// 2. The repository layer handles idempotency with ON CONFLICT DO NOTHING
554
+
// 3. This avoids a race condition where two concurrent requests both pass the check
555
+
recordURI, recordCID, err := s.createRecordOnPDSAs(ctx, userDID, "social.coves.community.block", "", blockRecord, userAccessToken)
557
+
// Check if this is a duplicate error from PDS
558
+
errMsg := err.Error()
559
+
if strings.Contains(errMsg, "duplicate") || strings.Contains(errMsg, "already exists") {
560
+
// Fetch and return existing block from our indexed view
561
+
existingBlock, getErr := s.repo.GetBlock(ctx, userDID, communityDID)
563
+
return existingBlock, nil
565
+
// If we can't find it in our index, return the original PDS error
567
+
return nil, fmt.Errorf("failed to create block on PDS: %w", err)
570
+
// Return block representation
571
+
block := &CommunityBlock{
573
+
CommunityDID: communityDID,
574
+
BlockedAt: time.Now(),
575
+
RecordURI: recordURI,
576
+
RecordCID: recordCID,
582
+
// UnblockCommunity removes a block via PDS delete
583
+
func (s *communityService) UnblockCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) error {
585
+
return NewValidationError("userDid", "required")
587
+
if userAccessToken == "" {
588
+
return NewValidationError("userAccessToken", "required")
591
+
// Resolve community identifier
592
+
communityDID, err := s.ResolveCommunityIdentifier(ctx, communityIdentifier)
597
+
// Get the block from AppView to find the record key
598
+
block, err := s.repo.GetBlock(ctx, userDID, communityDID)
603
+
// Extract rkey from record URI (at://did/collection/rkey)
604
+
rkey := extractRKeyFromURI(block.RecordURI)
606
+
return fmt.Errorf("invalid block record URI")
609
+
// Write-forward: delete record from PDS using user's access token
610
+
if err := s.deleteRecordOnPDSAs(ctx, userDID, "social.coves.community.block", rkey, userAccessToken); err != nil {
611
+
return fmt.Errorf("failed to delete block on PDS: %w", err)
617
+
// GetBlockedCommunities queries AppView DB for user's blocks
618
+
func (s *communityService) GetBlockedCommunities(ctx context.Context, userDID string, limit, offset int) ([]*CommunityBlock, error) {
619
+
if limit <= 0 || limit > 100 {
623
+
return s.repo.ListBlockedCommunities(ctx, userDID, limit, offset)
626
+
// IsBlocked checks if a user has blocked a community
627
+
func (s *communityService) IsBlocked(ctx context.Context, userDID, communityIdentifier string) (bool, error) {
628
+
communityDID, err := s.ResolveCommunityIdentifier(ctx, communityIdentifier)
633
+
return s.repo.IsBlocked(ctx, userDID, communityDID)
// ValidateHandle checks if a community handle is valid
func (s *communityService) ValidateHandle(handle string) error {