+24
-5
cmd/server/main.go
···
···+// IMPORTANT: We listen to social.coves.community.subscription (not social.coves.community.subscribe)+communityJetstreamURL = "ws://localhost:6008/subscribe?wantedCollections=social.coves.community.profile&wantedCollections=social.coves.community.subscription"+communityJetstreamConnector := jetstream.NewCommunityJetstreamConnector(communityEventConsumer, communityJetstreamURL)
+4
-2
internal/api/handlers/community/subscribe.go
······// Extract authenticated user DID and access token from request context (injected by auth middleware)···-subscription, err := h.service.SubscribeToCommunity(r.Context(), userDID, userAccessToken, req.Community)
······// Extract authenticated user DID and access token from request context (injected by auth middleware)···+subscription, err := h.service.SubscribeToCommunity(r.Context(), userDID, userAccessToken, req.Community, req.ContentVisibility)
+112
-34
internal/atproto/jetstream/community_consumer.go
······func (c *CommunityEventConsumer) handleSubscription(ctx context.Context, userDID string, commit *CommitEvent) error {-func (c *CommunityEventConsumer) handleUnsubscribe(ctx context.Context, userDID string, commit *CommitEvent) error {···
······func (c *CommunityEventConsumer) handleSubscription(ctx context.Context, userDID string, commit *CommitEvent) error {+func (c *CommunityEventConsumer) createSubscription(ctx context.Context, userDID string, commit *CommitEvent) error {+// IMPORTANT: Collection is social.coves.community.subscription (record type), not the XRPC endpoint+func (c *CommunityEventConsumer) deleteSubscription(ctx context.Context, userDID string, commit *CommitEvent) error {···
+136
internal/atproto/jetstream/community_jetstream_connector.go
···
···+// NewCommunityJetstreamConnector creates a new Jetstream WebSocket connector for community events+func NewCommunityJetstreamConnector(consumer *CommunityEventConsumer, wsURL string) *CommunityJetstreamConnector {
+4
-5
internal/atproto/jetstream/user_consumer.go
·········
·········
+7
-6
internal/core/communities/community.go
···
···+ContentVisibility int `json:"contentVisibility" db:"content_visibility"` // Feed slider: 1-5 (1=best content only, 5=all content)
+2
-1
internal/core/communities/interfaces.go
···UnsubscribeWithCount(ctx context.Context, userDID, communityDID string) error // Atomic: unsubscribe + decrement countListSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*Subscription, error)ListSubscribers(ctx context.Context, communityDID string, limit, offset int) ([]*Subscription, error)···-SubscribeToCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) (*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)
···UnsubscribeWithCount(ctx context.Context, userDID, communityDID string) error // Atomic: unsubscribe + decrement count+GetSubscriptionByURI(ctx context.Context, recordURI string) (*Subscription, error) // For Jetstream delete operationsListSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*Subscription, error)ListSubscribers(ctx context.Context, communityDID string, limit, offset int) ([]*Subscription, error)···+SubscribeToCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string, contentVisibility 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)
+23
-10
internal/core/communities/service.go
···-func (s *communityService) SubscribeToCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) (*Subscription, error) {···-recordURI, recordCID, err := s.createRecordOnPDSAs(ctx, userDID, "social.coves.community.subscribe", "", subRecord, userAccessToken)···-if err := s.deleteRecordOnPDSAs(ctx, userDID, "social.coves.community.subscribe", rkey, userAccessToken); err != nil {
···+func (s *communityService) SubscribeToCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string, contentVisibility int) (*Subscription, error) {···+// CRITICAL: Collection is social.coves.community.subscription (RECORD TYPE), not social.coves.community.subscribe (XRPC procedure)+// This record will be created in the USER's repository: at://user_did/social.coves.community.subscription/{tid}+recordURI, recordCID, err := s.createRecordOnPDSAs(ctx, userDID, "social.coves.community.subscription", "", subRecord, userAccessToken)···+// CRITICAL: Delete from social.coves.community.subscription (RECORD TYPE), not social.coves.community.unsubscribe+if err := s.deleteRecordOnPDSAs(ctx, userDID, "social.coves.community.subscription", rkey, userAccessToken); err != nil {
+50
-11
internal/db/postgres/community_repo_subscriptions.go
···func (r *postgresCommunityRepo) Subscribe(ctx context.Context, subscription *communities.Subscription) (*communities.Subscription, error) {-INSERT INTO community_subscriptions (user_did, community_did, subscribed_at, record_uri, record_cid)······-INSERT INTO community_subscriptions (user_did, community_did, subscribed_at, record_uri, record_cid)···-query = `SELECT id, subscribed_at FROM community_subscriptions WHERE user_did = $1 AND community_did = $2`-err = tx.QueryRowContext(ctx, query, subscription.UserDID, subscription.CommunityDID).Scan(&subscription.ID, &subscription.SubscribedAt)···func (r *postgresCommunityRepo) GetSubscription(ctx context.Context, userDID, communityDID string) (*communities.Subscription, error) {······func (r *postgresCommunityRepo) ListSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*communities.Subscription, error) {······func (r *postgresCommunityRepo) ListSubscribers(ctx context.Context, communityDID string, limit, offset int) ([]*communities.Subscription, error) {···
···func (r *postgresCommunityRepo) Subscribe(ctx context.Context, subscription *communities.Subscription) (*communities.Subscription, error) {+INSERT INTO community_subscriptions (user_did, community_did, subscribed_at, record_uri, record_cid, content_visibility)······+INSERT INTO community_subscriptions (user_did, community_did, subscribed_at, record_uri, record_cid, content_visibility)···+query = `SELECT id, subscribed_at, content_visibility FROM community_subscriptions WHERE user_did = $1 AND community_did = $2`+err = tx.QueryRowContext(ctx, query, subscription.UserDID, subscription.CommunityDID).Scan(&subscription.ID, &subscription.SubscribedAt, &subscription.ContentVisibility)···func (r *postgresCommunityRepo) GetSubscription(ctx context.Context, userDID, communityDID string) (*communities.Subscription, error) {······+func (r *postgresCommunityRepo) GetSubscriptionByURI(ctx context.Context, recordURI string) (*communities.Subscription, error) {func (r *postgresCommunityRepo) ListSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*communities.Subscription, error) {······func (r *postgresCommunityRepo) ListSubscribers(ctx context.Context, communityDID string, limit, offset int) ([]*communities.Subscription, error) {···