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}