+172
internal/api/handlers/community/block.go
+172
internal/api/handlers/community/block.go
···
···+// Extract authenticated user DID and access token from request context (injected by auth middleware)+// Extract authenticated user DID and access token from request context (injected by auth middleware)
+23
-4
internal/api/handlers/community/subscribe.go
+23
-4
internal/api/handlers/community/subscribe.go
·····················
·····················
+7
internal/api/routes/community.go
+7
internal/api/routes/community.go
······r.With(authMiddleware.RequireAuth).Post("/xrpc/social.coves.community.unsubscribe", subscribeHandler.HandleUnsubscribe)// r.With(authMiddleware.RequireAuth).Post("/xrpc/social.coves.community.delete", deleteHandler.HandleDelete)
······r.With(authMiddleware.RequireAuth).Post("/xrpc/social.coves.community.unsubscribe", subscribeHandler.HandleUnsubscribe)+r.With(authMiddleware.RequireAuth).Post("/xrpc/social.coves.community.blockCommunity", blockHandler.HandleBlock)+r.With(authMiddleware.RequireAuth).Post("/xrpc/social.coves.community.unblockCommunity", blockHandler.HandleUnblock)// r.With(authMiddleware.RequireAuth).Post("/xrpc/social.coves.community.delete", deleteHandler.HandleDelete)
+99
-1
internal/atproto/jetstream/community_consumer.go
+99
-1
internal/atproto/jetstream/community_consumer.go
···············
···············+func (c *CommunityEventConsumer) handleBlock(ctx context.Context, userDID string, commit *CommitEvent) error {+func (c *CommunityEventConsumer) createBlock(ctx context.Context, userDID string, commit *CommitEvent) error {+func (c *CommunityEventConsumer) deleteBlock(ctx context.Context, userDID string, commit *CommitEvent) error {
+49
internal/atproto/utils/record_utils.go
+49
internal/atproto/utils/record_utils.go
···
···
+11
internal/core/communities/community.go
+11
internal/core/communities/community.go
···+// Block records live in the user's repository (at://user_did/social.coves.community.block/{rkey})
+9
-1
internal/core/communities/errors.go
+9
-1
internal/core/communities/errors.go
·········
·········
+14
internal/core/communities/interfaces.go
+14
internal/core/communities/interfaces.go
···ListSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*Subscription, error)ListSubscribers(ctx context.Context, communityDID string, limit, offset int) ([]*Subscription, error)···UnsubscribeFromCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) errorGetUserSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*Subscription, error)GetCommunitySubscribers(ctx context.Context, communityIdentifier string, limit, offset int) ([]*Subscription, error)
···ListSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*Subscription, error)ListSubscribers(ctx context.Context, communityDID string, limit, offset int) ([]*Subscription, error)+GetBlockByURI(ctx context.Context, recordURI string) (*CommunityBlock, error) // For Jetstream delete operations+ListBlockedCommunities(ctx context.Context, userDID string, limit, offset int) ([]*CommunityBlock, error)···UnsubscribeFromCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) errorGetUserSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*Subscription, error)GetCommunitySubscribers(ctx context.Context, communityIdentifier string, limit, offset int) ([]*Subscription, error)+BlockCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) (*CommunityBlock, error)+UnblockCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) error+GetBlockedCommunities(ctx context.Context, userDID string, limit, offset int) ([]*CommunityBlock, error)
+140
-46
internal/core/communities/service.go
+140
-46
internal/core/communities/service.go
···············-func (s *communityService) createRecordOnPDS(ctx context.Context, repoDID, collection, rkey string, record map[string]interface{}) (string, string, error) {-endpoint := fmt.Sprintf("%s/xrpc/com.atproto.repo.createRecord", strings.TrimSuffix(s.pdsURL, "/"))func (s *communityService) createRecordOnPDSAs(ctx context.Context, repoDID, collection, rkey string, record map[string]interface{}, accessToken string) (string, string, error) {endpoint := fmt.Sprintf("%s/xrpc/com.atproto.repo.createRecord", strings.TrimSuffix(s.pdsURL, "/"))···-func (s *communityService) deleteRecordOnPDS(ctx context.Context, repoDID, collection, rkey string) error {-endpoint := fmt.Sprintf("%s/xrpc/com.atproto.repo.deleteRecord", strings.TrimSuffix(s.pdsURL, "/"))// deleteRecordOnPDSAs deletes a record with a specific access token (for user-scoped deletions)-func (s *communityService) deleteRecordOnPDSAs(ctx context.Context, repoDID, collection, rkey string, accessToken string) error {endpoint := fmt.Sprintf("%s/xrpc/com.atproto.repo.deleteRecord", strings.TrimSuffix(s.pdsURL, "/"))···-func (s *communityService) callPDS(ctx context.Context, method, endpoint string, payload map[string]interface{}) (string, string, error) {// callPDSWithAuth makes a PDS call with a specific access token (V2: for community authentication)···
·········+func (s *communityService) BlockCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) (*CommunityBlock, error) {+// This record will be created in the USER's repository: at://user_did/social.coves.community.block/{tid}+recordURI, recordCID, err := s.createRecordOnPDSAs(ctx, userDID, "social.coves.community.block", "", blockRecord, userAccessToken)+// PDS should return 409 Conflict for duplicate records, but we also check common error messages+return nil, fmt.Errorf("PDS reported duplicate block but failed to fetch from index: %w", getErr)+func (s *communityService) UnblockCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) error {+if err := s.deleteRecordOnPDSAs(ctx, userDID, "social.coves.community.block", rkey, userAccessToken); err != nil {+func (s *communityService) GetBlockedCommunities(ctx context.Context, userDID string, limit, offset int) ([]*CommunityBlock, error) {+func (s *communityService) IsBlocked(ctx context.Context, userDID, communityIdentifier string) (bool, error) {······func (s *communityService) createRecordOnPDSAs(ctx context.Context, repoDID, collection, rkey string, record map[string]interface{}, accessToken string) (string, string, error) {endpoint := fmt.Sprintf("%s/xrpc/com.atproto.repo.createRecord", strings.TrimSuffix(s.pdsURL, "/"))···// deleteRecordOnPDSAs deletes a record with a specific access token (for user-scoped deletions)+func (s *communityService) deleteRecordOnPDSAs(ctx context.Context, repoDID, collection, rkey, accessToken string) error {endpoint := fmt.Sprintf("%s/xrpc/com.atproto.repo.deleteRecord", strings.TrimSuffix(s.pdsURL, "/"))···// callPDSWithAuth makes a PDS call with a specific access token (V2: for community authentication)···
+28
internal/db/migrations/009_create_community_blocks_table.sql
+28
internal/db/migrations/009_create_community_blocks_table.sql
···
···+record_uri TEXT NOT NULL, -- atProto record identifier (at://user_did/social.coves.community.block/rkey)+-- Note: UNIQUE constraint on (user_did, community_did) already creates an index for those columns+CREATE INDEX idx_blocks_record_uri ON community_blocks(record_uri); -- For GetBlockByURI (Jetstream DELETE operations)
+173
internal/db/postgres/community_repo_blocks.go
+173
internal/db/postgres/community_repo_blocks.go
···
···+func (r *postgresCommunityRepo) BlockCommunity(ctx context.Context, block *communities.CommunityBlock) (*communities.CommunityBlock, error) {+func (r *postgresCommunityRepo) UnblockCommunity(ctx context.Context, userDID, communityDID string) error {+func (r *postgresCommunityRepo) GetBlock(ctx context.Context, userDID, communityDID string) (*communities.CommunityBlock, error) {+func (r *postgresCommunityRepo) GetBlockByURI(ctx context.Context, recordURI string) (*communities.CommunityBlock, error) {+func (r *postgresCommunityRepo) ListBlockedCommunities(ctx context.Context, userDID string, limit, offset int) ([]*communities.CommunityBlock, error) {+func (r *postgresCommunityRepo) IsBlocked(ctx context.Context, userDID, communityDID string) (bool, error) {
+470
tests/integration/community_blocking_test.go
+470
tests/integration/community_blocking_test.go
···
···+testCommunities[i] = createBlockingTestCommunity(t, repo, fmt.Sprintf("community-list-%d", i), communityDID)+func createBlockingTestCommunity(t *testing.T, repo communities.Repository, name, did string) *communities.Community {
+333
-19
tests/integration/community_e2e_test.go
+333
-19
tests/integration/community_e2e_test.go
·································pdsResp, pdsErr := http.Get(fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=%s&rkey=%s",···
···························+req, err := http.NewRequest("POST", httpServer.URL+"/xrpc/social.coves.community.blockCommunity", bytes.NewBuffer(blockJSON))+pdsResp, pdsErr := http.Get(fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=%s&rkey=%s",+t.Fatalf("Block record not found on PDS (status: %d, failed to read body: %v)", pdsResp.StatusCode, readErr)+t.Errorf("RecordURI mismatch: expected %s, got %s", blockResp.Block.RecordURI, block.RecordURI)+blockHttpReq, err := http.NewRequest("POST", httpServer.URL+"/xrpc/social.coves.community.blockCommunity", bytes.NewBuffer(blockJSON))+req, err := http.NewRequest("POST", httpServer.URL+"/xrpc/social.coves.community.unblockCommunity", bytes.NewBuffer(unblockJSON))+pdsResp, pdsErr := http.Get(fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=%s&rkey=%s",+req, err := http.NewRequest("POST", httpServer.URL+"/xrpc/social.coves.community.blockCommunity", bytes.NewBuffer(blockJSON))······pdsResp, pdsErr := http.Get(fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=%s&rkey=%s",···
+12
-12
tests/integration/subscription_indexing_test.go
+12
-12
tests/integration/subscription_indexing_test.go
·····················
·····················
+24
tests/unit/community_service_test.go
+24
tests/unit/community_service_test.go
···+func (m *mockCommunityRepo) BlockCommunity(ctx context.Context, block *communities.CommunityBlock) (*communities.CommunityBlock, error) {+func (m *mockCommunityRepo) UnblockCommunity(ctx context.Context, userDID, communityDID string) error {+func (m *mockCommunityRepo) GetBlock(ctx context.Context, userDID, communityDID string) (*communities.CommunityBlock, error) {+func (m *mockCommunityRepo) GetBlockByURI(ctx context.Context, recordURI string) (*communities.CommunityBlock, error) {+func (m *mockCommunityRepo) ListBlockedCommunities(ctx context.Context, userDID string, limit, offset int) ([]*communities.CommunityBlock, error) {+func (m *mockCommunityRepo) IsBlocked(ctx context.Context, userDID, communityDID string) (bool, error) {func (m *mockCommunityRepo) CreateMembership(ctx context.Context, membership *communities.Membership) (*communities.Membership, error) {