[READ ONLY MIRROR] Spark Social AppView Server github.com/sprksocial/server
atproto deno hono lexicon
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 195 lines 5.3 kB view raw
1import { Database } from "../db/index.ts"; 2import { TimeCidKeyset } from "../db/pagination.ts"; 3import { compositeTime } from "../util.ts"; 4 5const STORIES_EXPIRY_HOURS = 24; 6 7export interface StoryItem { 8 uri: string; 9 cid: string; 10 authorDid: string; 11 createdAt: string; 12 indexedAt: string; 13 sortAt: string; 14 archived: boolean; 15} 16 17export class Stories { 18 private db: Database; 19 private timeCidKeyset: TimeCidKeyset; 20 21 constructor(db: Database) { 22 this.db = db; 23 this.timeCidKeyset = new TimeCidKeyset(); 24 } 25 26 /** 27 * Get active (non-expired) stories by URIs 28 */ 29 async getStories(uris: string[]): Promise<StoryItem[]> { 30 if (!uris.length) return []; 31 32 const twentyFourHoursAgo = new Date(); 33 twentyFourHoursAgo.setHours( 34 twentyFourHoursAgo.getHours() - STORIES_EXPIRY_HOURS, 35 ); 36 const minDate = twentyFourHoursAgo.toISOString(); 37 38 const stories = await this.db.models.Story.find({ 39 uri: { $in: uris }, 40 indexedAt: { $gte: minDate }, 41 }).lean(); 42 43 return stories.map((story) => ({ 44 uri: story.uri, 45 cid: story.cid, 46 authorDid: story.authorDid, 47 createdAt: story.createdAt, 48 indexedAt: story.indexedAt, 49 archived: false, 50 sortAt: compositeTime(story.createdAt, story.indexedAt) || 51 story.createdAt, 52 })); 53 } 54 55 /** 56 * Get timeline stories from followed users (including the viewer's own stories) 57 */ 58 async getTimeline( 59 actorDid: string, 60 followedDids: string[], 61 limit = 50, 62 cursor?: string, 63 ): Promise<{ stories: StoryItem[]; cursor?: string }> { 64 const timelineDids = [...followedDids, actorDid]; 65 66 if (timelineDids.length === 0) { 67 return { stories: [] }; 68 } 69 70 // Calculate 24-hour expiry threshold 71 const twentyFourHoursAgo = new Date(); 72 twentyFourHoursAgo.setHours( 73 twentyFourHoursAgo.getHours() - STORIES_EXPIRY_HOURS, 74 ); 75 const minDate = twentyFourHoursAgo.toISOString(); 76 77 // Build query with expiry filter 78 const storiesQuery = this.db.models.Story.find({ 79 authorDid: { $in: timelineDids }, 80 indexedAt: { $gte: minDate }, 81 }); 82 83 // Apply pagination 84 const paginatedQuery = this.timeCidKeyset.paginate(storiesQuery, { 85 limit: limit + 1, // Get one extra for cursor check 86 cursor, 87 direction: "desc", 88 }); 89 90 const stories = await paginatedQuery.exec(); 91 92 // Check if we have more results 93 const hasMore = stories.length > limit; 94 const resultStories = hasMore ? stories.slice(0, limit) : stories; 95 96 // Transform stories 97 const transformedStories: StoryItem[] = resultStories.map((story) => ({ 98 uri: story.uri, 99 cid: story.cid, 100 authorDid: story.authorDid, 101 createdAt: story.createdAt, 102 indexedAt: story.indexedAt, 103 archived: false, 104 sortAt: compositeTime(story.createdAt, story.indexedAt) || 105 story.createdAt, 106 })); 107 108 // Generate cursor from last item if we have more results 109 let nextCursor: string | undefined; 110 if (hasMore && resultStories.length > 0) { 111 nextCursor = this.timeCidKeyset.packFromResult(resultStories); 112 } 113 114 return { 115 stories: transformedStories, 116 cursor: nextCursor, 117 }; 118 } 119 120 /** 121 * Filter out expired stories (older than 24 hours) 122 */ 123 filterExpiredStories( 124 stories: StoryItem[], 125 ): StoryItem[] { 126 const twentyFourHoursAgo = new Date(); 127 twentyFourHoursAgo.setHours( 128 twentyFourHoursAgo.getHours() - STORIES_EXPIRY_HOURS, 129 ); 130 131 return stories.filter((story) => { 132 const storyDate = new Date(story.indexedAt); 133 return storyDate >= twentyFourHoursAgo; 134 }); 135 } 136 137 /** 138 * Get active stories grouped by actor DID 139 */ 140 async getActorStories( 141 dids: string[], 142 ): Promise<Map<string, { uri: string; cid: string }[]>> { 143 if (!dids.length) return new Map(); 144 145 const twentyFourHoursAgo = new Date(); 146 twentyFourHoursAgo.setHours( 147 twentyFourHoursAgo.getHours() - STORIES_EXPIRY_HOURS, 148 ); 149 const minDate = twentyFourHoursAgo.toISOString(); 150 151 const stories = await this.db.models.Story.find({ 152 authorDid: { $in: dids }, 153 indexedAt: { $gte: minDate }, 154 }).sort({ indexedAt: 1 }).lean(); 155 156 const result = new Map<string, { uri: string; cid: string }[]>(); 157 for (const story of stories) { 158 const existing = result.get(story.authorDid) ?? []; 159 existing.push({ uri: story.uri, cid: story.cid }); 160 result.set(story.authorDid, existing); 161 } 162 return result; 163 } 164 165 /** 166 * Get blocked author DIDs for a viewer 167 */ 168 async getBlockedAuthors( 169 viewerDid: string, 170 authorDids: string[], 171 ): Promise<Set<string>> { 172 if (authorDids.length === 0) { 173 return new Set(); 174 } 175 176 // Single query to get all block relationships 177 const [viewerBlocking, viewerBlocked] = await Promise.all([ 178 this.db.models.Block.find({ 179 authorDid: viewerDid, 180 subject: { $in: authorDids }, 181 }).select("subject").lean(), 182 this.db.models.Block.find({ 183 authorDid: { $in: authorDids }, 184 subject: viewerDid, 185 }).select("authorDid").lean(), 186 ]); 187 188 const blockedAuthorDids = new Set([ 189 ...viewerBlocking.map((b) => b.subject), 190 ...viewerBlocked.map((b) => b.authorDid), 191 ]); 192 193 return blockedAuthorDids; 194 } 195}