ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
at master 4.3 kB view raw
1import { AuthenticatedHandler } from "./core/types"; 2import { SessionService } from "./services/SessionService"; 3import { FollowService } from "./services/FollowService"; 4import { MatchRepository } from "./repositories"; 5import { successResponse, validateArrayInput, ValidationSchemas } from "./utils"; 6import { withAuthErrorHandling } from "./core/middleware"; 7import { 8 createRateLimiter, 9 applyRateLimit, 10} from "./core/middleware/rateLimit.middleware"; 11 12// Rate limit: 8 requests per hour 13// Each request can follow up to 100 users (300 points) 14// Leaves ~50% of 5,000 points/hr for other operations 15const checkRateLimit = createRateLimiter({ 16 maxRequests: 8, 17 windowMs: 60 * 60 * 1000, // 1 hour 18}); 19 20const batchFollowHandler: AuthenticatedHandler = async (context) => { 21 applyRateLimit(checkRateLimit, context.event, "minutes"); 22 const body = JSON.parse(context.event.body || "{}"); 23 const dids = validateArrayInput<string>( 24 context.event.body, 25 "dids", 26 ValidationSchemas.didsArray, 27 ); 28 const followLexicon: string = body.followLexicon || "app.bsky.graph.follow"; 29 30 const { agent } = await SessionService.getAgentForSession( 31 context.sessionId, 32 context.event, 33 ); 34 35 const alreadyFollowing = await FollowService.getAlreadyFollowing( 36 agent, 37 context.did, 38 dids, 39 followLexicon, 40 ); 41 42 const matchRepo = new MatchRepository(); 43 const CONCURRENCY = 5; // Process 5 follows in parallel 44 45 // Helper function to follow a single user 46 const followUser = async (did: string) => { 47 if (alreadyFollowing.has(did)) { 48 try { 49 await matchRepo.updateFollowStatus(did, followLexicon, true); 50 } catch (dbError) { 51 console.error("Failed to update follow status in DB:", dbError); 52 } 53 54 return { 55 did, 56 success: true, 57 alreadyFollowing: true, 58 error: null, 59 }; 60 } 61 62 try { 63 await agent.api.com.atproto.repo.createRecord({ 64 repo: context.did, 65 collection: followLexicon, 66 record: { 67 $type: followLexicon, 68 subject: did, 69 createdAt: new Date().toISOString(), 70 }, 71 }); 72 73 try { 74 await matchRepo.updateFollowStatus(did, followLexicon, true); 75 } catch (dbError) { 76 console.error("Failed to update follow status in DB:", dbError); 77 } 78 79 return { 80 did, 81 success: true, 82 alreadyFollowing: false, 83 error: null, 84 }; 85 } catch (error) { 86 // Rate limit handling with backoff 87 if ( 88 error instanceof Error && 89 (error.message.includes("rate limit") || error.message.includes("429")) 90 ) { 91 const backoffDelay = 1000; // 1 second backoff for rate limits 92 console.log(`Rate limit hit for ${did}. Backing off for ${backoffDelay}ms...`); 93 await new Promise((resolve) => setTimeout(resolve, backoffDelay)); 94 } 95 96 return { 97 did, 98 success: false, 99 alreadyFollowing: false, 100 error: error instanceof Error ? error.message : "Follow failed", 101 }; 102 } 103 }; 104 105 // Process follows in chunks with controlled concurrency 106 const results = []; 107 for (let i = 0; i < dids.length; i += CONCURRENCY) { 108 const chunk = dids.slice(i, i + CONCURRENCY); 109 const chunkResults = await Promise.allSettled( 110 chunk.map(did => followUser(did)) 111 ); 112 113 // Extract results from Promise.allSettled 114 for (const result of chunkResults) { 115 if (result.status === 'fulfilled') { 116 results.push(result.value); 117 } else { 118 // This shouldn't happen as we handle errors in followUser 119 console.error('Unexpected promise rejection:', result.reason); 120 results.push({ 121 did: 'unknown', 122 success: false, 123 alreadyFollowing: false, 124 error: 'Unexpected error', 125 }); 126 } 127 } 128 } 129 130 const successCount = results.filter((r) => r.success).length; 131 const failCount = results.filter((r) => !r.success).length; 132 const alreadyFollowingCount = results.filter( 133 (r) => r.alreadyFollowing, 134 ).length; 135 136 return successResponse({ 137 success: true, 138 total: dids.length, 139 succeeded: successCount, 140 failed: failCount, 141 alreadyFollowing: alreadyFollowingCount, 142 results, 143 }); 144}; 145 146export const handler = withAuthErrorHandling(batchFollowHandler);