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