A community based topic aggregation platform built on atproto
1package communities 2 3import ( 4 "fmt" 5 "strings" 6 "time" 7) 8 9// Community represents a Coves community indexed from the firehose 10// Communities are federated, instance-scoped forums built on atProto 11type Community struct { 12 CreatedAt time.Time `json:"createdAt" db:"created_at"` 13 UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` 14 RecordURI string `json:"recordUri,omitempty" db:"record_uri"` 15 FederatedFrom string `json:"federatedFrom,omitempty" db:"federated_from"` 16 DisplayName string `json:"displayName" db:"display_name"` 17 Description string `json:"description" db:"description"` 18 PDSURL string `json:"-" db:"pds_url"` 19 AvatarCID string `json:"avatarCid,omitempty" db:"avatar_cid"` 20 BannerCID string `json:"bannerCid,omitempty" db:"banner_cid"` 21 OwnerDID string `json:"ownerDid" db:"owner_did"` 22 CreatedByDID string `json:"createdByDid" db:"created_by_did"` 23 HostedByDID string `json:"hostedByDid" db:"hosted_by_did"` 24 PDSEmail string `json:"-" db:"pds_email"` 25 PDSPassword string `json:"-" db:"pds_password_encrypted"` 26 Name string `json:"name" db:"name"` // Short name (e.g., "gardening") 27 DisplayHandle string `json:"displayHandle,omitempty" db:"-"` // UI hint: !gardening@coves.social (computed, not stored) 28 RecordCID string `json:"recordCid,omitempty" db:"record_cid"` 29 FederatedID string `json:"federatedId,omitempty" db:"federated_id"` 30 PDSAccessToken string `json:"-" db:"pds_access_token"` 31 SigningKeyPEM string `json:"-" db:"signing_key_encrypted"` 32 ModerationType string `json:"moderationType,omitempty" db:"moderation_type"` 33 Handle string `json:"handle" db:"handle"` // Canonical atProto handle (e.g., gardening.community.coves.social) 34 PDSRefreshToken string `json:"-" db:"pds_refresh_token"` 35 Visibility string `json:"visibility" db:"visibility"` 36 RotationKeyPEM string `json:"-" db:"rotation_key_encrypted"` 37 DID string `json:"did" db:"did"` 38 ContentWarnings []string `json:"contentWarnings,omitempty" db:"content_warnings"` 39 DescriptionFacets []byte `json:"descriptionFacets,omitempty" db:"description_facets"` 40 PostCount int `json:"postCount" db:"post_count"` 41 SubscriberCount int `json:"subscriberCount" db:"subscriber_count"` 42 MemberCount int `json:"memberCount" db:"member_count"` 43 ID int `json:"id" db:"id"` 44 AllowExternalDiscovery bool `json:"allowExternalDiscovery" db:"allow_external_discovery"` 45} 46 47// Subscription represents a lightweight feed follow (user subscribes to see posts) 48type Subscription struct { 49 SubscribedAt time.Time `json:"subscribedAt" db:"subscribed_at"` 50 UserDID string `json:"userDid" db:"user_did"` 51 CommunityDID string `json:"communityDid" db:"community_did"` 52 RecordURI string `json:"recordUri,omitempty" db:"record_uri"` 53 RecordCID string `json:"recordCid,omitempty" db:"record_cid"` 54 ContentVisibility int `json:"contentVisibility" db:"content_visibility"` // Feed slider: 1-5 (1=best content only, 5=all content) 55 ID int `json:"id" db:"id"` 56} 57 58// CommunityBlock represents a user blocking a community 59// Block records live in the user's repository (at://user_did/social.coves.community.block/{rkey}) 60type CommunityBlock struct { 61 BlockedAt time.Time `json:"blockedAt" db:"blocked_at"` 62 UserDID string `json:"userDid" db:"user_did"` 63 CommunityDID string `json:"communityDid" db:"community_did"` 64 RecordURI string `json:"recordUri,omitempty" db:"record_uri"` 65 RecordCID string `json:"recordCid,omitempty" db:"record_cid"` 66 ID int `json:"id" db:"id"` 67} 68 69// Membership represents active participation with reputation tracking 70type Membership struct { 71 JoinedAt time.Time `json:"joinedAt" db:"joined_at"` 72 LastActiveAt time.Time `json:"lastActiveAt" db:"last_active_at"` 73 UserDID string `json:"userDid" db:"user_did"` 74 CommunityDID string `json:"communityDid" db:"community_did"` 75 ID int `json:"id" db:"id"` 76 ReputationScore int `json:"reputationScore" db:"reputation_score"` 77 ContributionCount int `json:"contributionCount" db:"contribution_count"` 78 IsBanned bool `json:"isBanned" db:"is_banned"` 79 IsModerator bool `json:"isModerator" db:"is_moderator"` 80} 81 82// ModerationAction represents a moderation action taken against a community 83type ModerationAction struct { 84 CreatedAt time.Time `json:"createdAt" db:"created_at"` 85 ExpiresAt *time.Time `json:"expiresAt,omitempty" db:"expires_at"` 86 CommunityDID string `json:"communityDid" db:"community_did"` 87 Action string `json:"action" db:"action"` 88 Reason string `json:"reason,omitempty" db:"reason"` 89 InstanceDID string `json:"instanceDid" db:"instance_did"` 90 ID int `json:"id" db:"id"` 91 Broadcast bool `json:"broadcast" db:"broadcast"` 92} 93 94// CreateCommunityRequest represents input for creating a new community 95type CreateCommunityRequest struct { 96 Name string `json:"name"` 97 DisplayName string `json:"displayName,omitempty"` 98 Description string `json:"description"` 99 Language string `json:"language,omitempty"` 100 Visibility string `json:"visibility"` 101 CreatedByDID string `json:"createdByDid"` 102 HostedByDID string `json:"hostedByDid"` 103 AvatarBlob []byte `json:"avatarBlob,omitempty"` 104 BannerBlob []byte `json:"bannerBlob,omitempty"` 105 Rules []string `json:"rules,omitempty"` 106 Categories []string `json:"categories,omitempty"` 107 AllowExternalDiscovery bool `json:"allowExternalDiscovery"` 108} 109 110// UpdateCommunityRequest represents input for updating community metadata 111type UpdateCommunityRequest struct { 112 CommunityDID string `json:"communityDid"` 113 UpdatedByDID string `json:"updatedByDid"` // User making the update (for authorization) 114 DisplayName *string `json:"displayName,omitempty"` 115 Description *string `json:"description,omitempty"` 116 AvatarBlob []byte `json:"avatarBlob,omitempty"` 117 BannerBlob []byte `json:"bannerBlob,omitempty"` 118 Visibility *string `json:"visibility,omitempty"` 119 AllowExternalDiscovery *bool `json:"allowExternalDiscovery,omitempty"` 120 ModerationType *string `json:"moderationType,omitempty"` 121 ContentWarnings []string `json:"contentWarnings,omitempty"` 122} 123 124// ListCommunitiesRequest represents query parameters for listing communities 125type ListCommunitiesRequest struct { 126 Sort string `json:"sort,omitempty"` // Enum: popular, active, new, alphabetical 127 Visibility string `json:"visibility,omitempty"` // Filter: public, unlisted, private 128 Category string `json:"category,omitempty"` // Optional: filter by category (future) 129 Language string `json:"language,omitempty"` // Optional: filter by language (future) 130 Limit int `json:"limit"` // 1-100, default 50 131 Offset int `json:"offset"` // Pagination offset 132} 133 134// SearchCommunitiesRequest represents query parameters for searching communities 135type SearchCommunitiesRequest struct { 136 Query string `json:"query"` 137 Visibility string `json:"visibility,omitempty"` 138 Limit int `json:"limit"` 139 Offset int `json:"offset"` 140} 141 142// GetDisplayHandle returns the user-facing display format for a community handle 143// Following Bluesky's pattern where client adds @ prefix for users, but for communities we use ! prefix 144// Example: "gardening.community.coves.social" -> "!gardening@coves.social" 145// 146// Handles various domain formats correctly: 147// - "gaming.community.coves.social" -> "!gaming@coves.social" 148// - "gaming.community.coves.co.uk" -> "!gaming@coves.co.uk" 149// - "test.community.dev.coves.social" -> "!test@dev.coves.social" 150func (c *Community) GetDisplayHandle() string { 151 // Find the ".community." substring in the handle 152 communityIndex := strings.Index(c.Handle, ".community.") 153 if communityIndex == -1 { 154 // Fallback if format doesn't match expected pattern 155 return c.Handle 156 } 157 158 // Extract name (everything before ".community.") 159 name := c.Handle[:communityIndex] 160 161 // Extract instance domain (everything after ".community.") 162 communitySegment := ".community." 163 instanceDomain := c.Handle[communityIndex+len(communitySegment):] 164 165 return fmt.Sprintf("!%s@%s", name, instanceDomain) 166}