A community based topic aggregation platform built on atproto
1package postgres 2 3import ( 4 "Coves/internal/core/communities" 5 "context" 6 "database/sql" 7 "fmt" 8 "log" 9) 10 11// BlockCommunity creates a new block record (idempotent) 12func (r *postgresCommunityRepo) BlockCommunity(ctx context.Context, block *communities.CommunityBlock) (*communities.CommunityBlock, error) { 13 query := ` 14 INSERT INTO community_blocks (user_did, community_did, blocked_at, record_uri, record_cid) 15 VALUES ($1, $2, $3, $4, $5) 16 ON CONFLICT (user_did, community_did) DO UPDATE SET 17 record_uri = EXCLUDED.record_uri, 18 record_cid = EXCLUDED.record_cid, 19 blocked_at = EXCLUDED.blocked_at 20 RETURNING id, blocked_at` 21 22 err := r.db.QueryRowContext(ctx, query, 23 block.UserDID, 24 block.CommunityDID, 25 block.BlockedAt, 26 block.RecordURI, 27 block.RecordCID, 28 ).Scan(&block.ID, &block.BlockedAt) 29 if err != nil { 30 return nil, fmt.Errorf("failed to create block: %w", err) 31 } 32 33 return block, nil 34} 35 36// UnblockCommunity removes a block record 37func (r *postgresCommunityRepo) UnblockCommunity(ctx context.Context, userDID, communityDID string) error { 38 query := `DELETE FROM community_blocks WHERE user_did = $1 AND community_did = $2` 39 40 result, err := r.db.ExecContext(ctx, query, userDID, communityDID) 41 if err != nil { 42 return fmt.Errorf("failed to unblock community: %w", err) 43 } 44 45 rowsAffected, err := result.RowsAffected() 46 if err != nil { 47 return fmt.Errorf("failed to check unblock result: %w", err) 48 } 49 50 if rowsAffected == 0 { 51 return communities.ErrBlockNotFound 52 } 53 54 return nil 55} 56 57// GetBlock retrieves a block record by user DID and community DID 58func (r *postgresCommunityRepo) GetBlock(ctx context.Context, userDID, communityDID string) (*communities.CommunityBlock, error) { 59 query := ` 60 SELECT id, user_did, community_did, blocked_at, record_uri, record_cid 61 FROM community_blocks 62 WHERE user_did = $1 AND community_did = $2` 63 64 var block communities.CommunityBlock 65 66 err := r.db.QueryRowContext(ctx, query, userDID, communityDID).Scan( 67 &block.ID, 68 &block.UserDID, 69 &block.CommunityDID, 70 &block.BlockedAt, 71 &block.RecordURI, 72 &block.RecordCID, 73 ) 74 if err != nil { 75 if err == sql.ErrNoRows { 76 return nil, communities.ErrBlockNotFound 77 } 78 return nil, fmt.Errorf("failed to get block: %w", err) 79 } 80 81 return &block, nil 82} 83 84// GetBlockByURI retrieves a block record by its AT-URI (for Jetstream DELETE operations) 85func (r *postgresCommunityRepo) GetBlockByURI(ctx context.Context, recordURI string) (*communities.CommunityBlock, error) { 86 query := ` 87 SELECT id, user_did, community_did, blocked_at, record_uri, record_cid 88 FROM community_blocks 89 WHERE record_uri = $1` 90 91 var block communities.CommunityBlock 92 93 err := r.db.QueryRowContext(ctx, query, recordURI).Scan( 94 &block.ID, 95 &block.UserDID, 96 &block.CommunityDID, 97 &block.BlockedAt, 98 &block.RecordURI, 99 &block.RecordCID, 100 ) 101 if err != nil { 102 if err == sql.ErrNoRows { 103 return nil, communities.ErrBlockNotFound 104 } 105 return nil, fmt.Errorf("failed to get block by URI: %w", err) 106 } 107 108 return &block, nil 109} 110 111// ListBlockedCommunities retrieves all communities blocked by a user 112func (r *postgresCommunityRepo) ListBlockedCommunities(ctx context.Context, userDID string, limit, offset int) ([]*communities.CommunityBlock, error) { 113 query := ` 114 SELECT id, user_did, community_did, blocked_at, record_uri, record_cid 115 FROM community_blocks 116 WHERE user_did = $1 117 ORDER BY blocked_at DESC 118 LIMIT $2 OFFSET $3` 119 120 rows, err := r.db.QueryContext(ctx, query, userDID, limit, offset) 121 if err != nil { 122 return nil, fmt.Errorf("failed to list blocked communities: %w", err) 123 } 124 defer func() { 125 if closeErr := rows.Close(); closeErr != nil { 126 // Log error but don't override the main error 127 log.Printf("Failed to close rows: %v", closeErr) 128 } 129 }() 130 131 var blocks []*communities.CommunityBlock 132 for rows.Next() { 133 // Allocate a new block for each iteration to avoid pointer reuse bug 134 block := &communities.CommunityBlock{} 135 136 err = rows.Scan( 137 &block.ID, 138 &block.UserDID, 139 &block.CommunityDID, 140 &block.BlockedAt, 141 &block.RecordURI, 142 &block.RecordCID, 143 ) 144 if err != nil { 145 return nil, fmt.Errorf("failed to scan block: %w", err) 146 } 147 148 blocks = append(blocks, block) 149 } 150 151 if err = rows.Err(); err != nil { 152 return nil, fmt.Errorf("error iterating blocks: %w", err) 153 } 154 155 return blocks, nil 156} 157 158// IsBlocked checks if a user has blocked a specific community (fast EXISTS check) 159func (r *postgresCommunityRepo) IsBlocked(ctx context.Context, userDID, communityDID string) (bool, error) { 160 query := ` 161 SELECT EXISTS( 162 SELECT 1 FROM community_blocks 163 WHERE user_did = $1 AND community_did = $2 164 )` 165 166 var exists bool 167 err := r.db.QueryRowContext(ctx, query, userDID, communityDID).Scan(&exists) 168 if err != nil { 169 return false, fmt.Errorf("failed to check if blocked: %w", err) 170 } 171 172 return exists, nil 173}