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}