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 Visibility string `json:"visibility,omitempty"`
127 HostedBy string `json:"hostedBy,omitempty"`
128 SortBy string `json:"sortBy,omitempty"`
129 SortOrder string `json:"sortOrder,omitempty"`
130 Limit int `json:"limit"`
131 Offset int `json:"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 // len(".community.") = 11
163 instanceDomain := c.Handle[communityIndex+11:]
164
165 return fmt.Sprintf("!%s@%s", name, instanceDomain)
166}