A community based topic aggregation platform built on atproto
1package common
2
3import (
4 "Coves/internal/api/middleware"
5 "Coves/internal/core/communities"
6 "Coves/internal/core/posts"
7 "Coves/internal/core/votes"
8 "context"
9 "log"
10 "net/http"
11)
12
13// FeedPostProvider is implemented by any feed post wrapper that contains a PostView.
14// This allows the helper to work with different feed post types (discover, timeline, communityFeed).
15type FeedPostProvider interface {
16 GetPost() *posts.PostView
17}
18
19// PopulateViewerVoteState enriches feed posts with the authenticated user's vote state.
20// This is a no-op if voteService is nil or the request is unauthenticated.
21//
22// Parameters:
23// - ctx: Request context for PDS calls
24// - r: HTTP request (used to extract OAuth session)
25// - voteService: Vote service for cache lookup (may be nil)
26// - feedPosts: Posts to enrich with viewer state (must implement FeedPostProvider)
27//
28// The function logs but does not fail on errors - viewer state is optional enrichment.
29func PopulateViewerVoteState[T FeedPostProvider](
30 ctx context.Context,
31 r *http.Request,
32 voteService votes.Service,
33 feedPosts []T,
34) {
35 if voteService == nil {
36 return
37 }
38
39 session := middleware.GetOAuthSession(r)
40 if session == nil {
41 return
42 }
43
44 userDID := middleware.GetUserDID(r)
45
46 // Ensure vote cache is populated from PDS
47 if err := voteService.EnsureCachePopulated(ctx, session); err != nil {
48 log.Printf("Warning: failed to populate vote cache: %v", err)
49 return
50 }
51
52 // Collect post URIs to batch lookup
53 postURIs := make([]string, 0, len(feedPosts))
54 for _, feedPost := range feedPosts {
55 if post := feedPost.GetPost(); post != nil {
56 postURIs = append(postURIs, post.URI)
57 }
58 }
59
60 // Get viewer votes for all posts
61 viewerVotes := voteService.GetViewerVotesForSubjects(userDID, postURIs)
62
63 // Populate viewer state on each post
64 for _, feedPost := range feedPosts {
65 if post := feedPost.GetPost(); post != nil {
66 if vote, exists := viewerVotes[post.URI]; exists {
67 post.Viewer = &posts.ViewerState{
68 Vote: &vote.Direction,
69 VoteURI: &vote.URI,
70 }
71 }
72 }
73 }
74}
75
76// PopulateCommunityViewerState enriches communities with the authenticated user's subscription state.
77// This is a no-op if the request is unauthenticated.
78func PopulateCommunityViewerState(
79 ctx context.Context,
80 r *http.Request,
81 repo communities.Repository,
82 communityList []*communities.Community,
83) {
84 if repo == nil || len(communityList) == 0 {
85 return
86 }
87
88 userDID := middleware.GetUserDID(r)
89 if userDID == "" {
90 return // Not authenticated, leave viewer state nil
91 }
92
93 // Collect community DIDs
94 communityDIDs := make([]string, len(communityList))
95 for i, c := range communityList {
96 communityDIDs[i] = c.DID
97 }
98
99 // Batch query subscriptions
100 subscribed, err := repo.GetSubscribedCommunityDIDs(ctx, userDID, communityDIDs)
101 if err != nil {
102 log.Printf("Warning: failed to get subscription state for user %s (%d communities): %v",
103 userDID, len(communityDIDs), err)
104 return
105 }
106
107 // Populate viewer state on each community
108 for _, c := range communityList {
109 isSubscribed := subscribed[c.DID]
110 c.Viewer = &communities.CommunityViewerState{
111 Subscribed: &isSubscribed,
112 }
113 }
114}