ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
1import { AuthenticatedHandler } from "./core/types"; 2import { 3 UploadRepository, 4 SourceAccountRepository, 5 MatchRepository, 6} from "./repositories"; 7import { successResponse } from "./utils"; 8import { normalize } from "./utils/string.utils"; 9import { withAuthErrorHandling } from "./core/middleware"; 10import { ValidationError } from "./core/errors"; 11 12interface SearchResult { 13 sourceUser: { 14 username: string; 15 date: string; 16 }; 17 atprotoMatches: Array<{ 18 did: string; 19 handle: string; 20 displayName?: string; 21 avatar?: string; 22 description?: string; 23 matchScore: number; 24 postCount: number; 25 followerCount: number; 26 }>; 27 isSearching?: boolean; 28 error?: string; 29 selectedMatches?: any; 30} 31 32interface SaveResultsRequest { 33 uploadId: string; 34 sourcePlatform: string; 35 results: SearchResult[]; 36 saveData?: boolean; 37} 38 39const saveResultsHandler: AuthenticatedHandler = async (context) => { 40 const body: SaveResultsRequest = JSON.parse(context.event.body || "{}"); 41 const { uploadId, sourcePlatform, results, saveData } = body; 42 43 if (!uploadId || !sourcePlatform || !Array.isArray(results)) { 44 throw new ValidationError( 45 "uploadId, sourcePlatform, and results are required", 46 ); 47 } 48 49 if (saveData === false) { 50 console.log( 51 `User ${context.did} has data storage disabled - skipping save`, 52 ); 53 return successResponse({ 54 success: true, 55 message: "Data storage disabled - results not saved", 56 uploadId, 57 totalUsers: results.length, 58 matchedUsers: results.filter((r) => r.atprotoMatches.length > 0).length, 59 unmatchedUsers: results.filter((r) => r.atprotoMatches.length === 0) 60 .length, 61 }); 62 } 63 64 const uploadRepo = new UploadRepository(); 65 const sourceAccountRepo = new SourceAccountRepository(); 66 const matchRepo = new MatchRepository(); 67 let matchedCount = 0; 68 69 const hasRecent = await uploadRepo.hasRecentUpload(context.did); 70 if (hasRecent) { 71 console.log( 72 `User ${context.did} already saved within 5 seconds, skipping duplicate`, 73 ); 74 return successResponse({ 75 success: true, 76 message: "Recently saved", 77 }); 78 } 79 80 await uploadRepo.createUpload( 81 uploadId, 82 context.did, 83 sourcePlatform, 84 results.length, 85 0, 86 ); 87 88 const allUsernames = results.map((r) => r.sourceUser.username); 89 const sourceAccountIdMap = await sourceAccountRepo.bulkCreate( 90 sourcePlatform, 91 allUsernames, 92 ); 93 94 const links = results 95 .map((result) => { 96 const normalized = normalize(result.sourceUser.username); 97 const sourceAccountId = sourceAccountIdMap.get(normalized); 98 return { 99 sourceAccountId: sourceAccountId!, 100 sourceDate: result.sourceUser.date, 101 }; 102 }) 103 .filter((link) => link.sourceAccountId !== undefined); 104 105 await sourceAccountRepo.linkUserToAccounts(uploadId, context.did, links); 106 107 const allMatches: Array<{ 108 sourceAccountId: number; 109 atprotoDid: string; 110 atprotoHandle: string; 111 atprotoDisplayName?: string; 112 atprotoAvatar?: string; 113 atprotoDescription?: string; 114 matchScore: number; 115 postCount: number; 116 followerCount: number; 117 }> = []; 118 119 const matchedSourceAccountIds: number[] = []; 120 121 for (const result of results) { 122 const normalized = normalize(result.sourceUser.username); 123 const sourceAccountId = sourceAccountIdMap.get(normalized); 124 125 if ( 126 sourceAccountId && 127 result.atprotoMatches && 128 result.atprotoMatches.length > 0 129 ) { 130 matchedCount++; 131 matchedSourceAccountIds.push(sourceAccountId); 132 133 for (const match of result.atprotoMatches) { 134 allMatches.push({ 135 sourceAccountId, 136 atprotoDid: match.did, 137 atprotoHandle: match.handle, 138 atprotoDisplayName: match.displayName, 139 atprotoAvatar: match.avatar, 140 atprotoDescription: (match as any).description, 141 matchScore: match.matchScore, 142 postCount: match.postCount || 0, 143 followerCount: match.followerCount || 0, 144 }); 145 } 146 } 147 } 148 149 let matchIdMap = new Map<string, number>(); 150 if (allMatches.length > 0) { 151 matchIdMap = await matchRepo.bulkStoreMatches(allMatches); 152 } 153 154 if (matchedSourceAccountIds.length > 0) { 155 await sourceAccountRepo.markAsMatched(matchedSourceAccountIds); 156 } 157 158 const statuses: Array<{ 159 did: string; 160 atprotoMatchId: number; 161 sourceAccountId: number; 162 viewed: boolean; 163 }> = []; 164 165 for (const match of allMatches) { 166 const key = `${match.sourceAccountId}:${match.atprotoDid}`; 167 const matchId = matchIdMap.get(key); 168 if (matchId) { 169 statuses.push({ 170 did: context.did, 171 atprotoMatchId: matchId, 172 sourceAccountId: match.sourceAccountId, 173 viewed: true, 174 }); 175 } 176 } 177 178 if (statuses.length > 0) { 179 await matchRepo.upsertUserMatchStatus(statuses); 180 } 181 182 await uploadRepo.updateMatchCounts( 183 uploadId, 184 matchedCount, 185 results.length - matchedCount, 186 ); 187 188 return successResponse({ 189 success: true, 190 uploadId, 191 totalUsers: results.length, 192 matchedUsers: matchedCount, 193 unmatchedUsers: results.length - matchedCount, 194 }); 195}; 196 197export const handler = withAuthErrorHandling(saveResultsHandler);