ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
at master 5.3 kB view raw
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 // Check if this specific upload already exists 70 const existingUpload = await uploadRepo.getUpload(uploadId, context.did); 71 72 if (!existingUpload) { 73 // Upload doesn't exist - create it (file upload flow) 74 await uploadRepo.createUpload( 75 uploadId, 76 context.did, 77 sourcePlatform, 78 results.length, 79 0, 80 ); 81 } else { 82 // Upload exists (extension flow) - just update it with matches 83 console.log(`[save-results] Updating existing upload ${uploadId} with matches`); 84 } 85 86 const allUsernames = results.map((r) => r.sourceUser.username); 87 const sourceAccountIdMap = await sourceAccountRepo.bulkCreate( 88 sourcePlatform, 89 allUsernames, 90 ); 91 92 const links = results 93 .map((result) => { 94 const normalized = normalize(result.sourceUser.username); 95 const sourceAccountId = sourceAccountIdMap.get(normalized); 96 return { 97 sourceAccountId: sourceAccountId!, 98 sourceDate: result.sourceUser.date, 99 }; 100 }) 101 .filter((link) => link.sourceAccountId !== undefined); 102 103 await sourceAccountRepo.linkUserToAccounts(uploadId, context.did, links); 104 105 const allMatches: Array<{ 106 sourceAccountId: number; 107 atprotoDid: string; 108 atprotoHandle: string; 109 atprotoDisplayName?: string; 110 atprotoAvatar?: string; 111 atprotoDescription?: string; 112 matchScore: number; 113 postCount: number; 114 followerCount: number; 115 }> = []; 116 117 const matchedSourceAccountIds: number[] = []; 118 119 for (const result of results) { 120 const normalized = normalize(result.sourceUser.username); 121 const sourceAccountId = sourceAccountIdMap.get(normalized); 122 123 if ( 124 sourceAccountId && 125 result.atprotoMatches && 126 result.atprotoMatches.length > 0 127 ) { 128 matchedCount++; 129 matchedSourceAccountIds.push(sourceAccountId); 130 131 for (const match of result.atprotoMatches) { 132 allMatches.push({ 133 sourceAccountId, 134 atprotoDid: match.did, 135 atprotoHandle: match.handle, 136 atprotoDisplayName: match.displayName, 137 atprotoAvatar: match.avatar, 138 atprotoDescription: (match as any).description, 139 matchScore: match.matchScore, 140 postCount: match.postCount || 0, 141 followerCount: match.followerCount || 0, 142 }); 143 } 144 } 145 } 146 147 let matchIdMap = new Map<string, number>(); 148 if (allMatches.length > 0) { 149 matchIdMap = await matchRepo.bulkStoreMatches(allMatches); 150 } 151 152 if (matchedSourceAccountIds.length > 0) { 153 await sourceAccountRepo.markAsMatched(matchedSourceAccountIds); 154 } 155 156 const statuses: Array<{ 157 did: string; 158 atprotoMatchId: number; 159 sourceAccountId: number; 160 viewed: boolean; 161 }> = []; 162 163 for (const match of allMatches) { 164 const key = `${match.sourceAccountId}:${match.atprotoDid}`; 165 const matchId = matchIdMap.get(key); 166 if (matchId) { 167 statuses.push({ 168 did: context.did, 169 atprotoMatchId: matchId, 170 sourceAccountId: match.sourceAccountId, 171 viewed: true, 172 }); 173 } 174 } 175 176 if (statuses.length > 0) { 177 await matchRepo.upsertUserMatchStatus(statuses); 178 } 179 180 await uploadRepo.updateMatchCounts( 181 uploadId, 182 matchedCount, 183 results.length - matchedCount, 184 ); 185 186 return successResponse({ 187 success: true, 188 uploadId, 189 totalUsers: results.length, 190 matchedUsers: matchedCount, 191 unmatchedUsers: results.length - matchedCount, 192 }); 193}; 194 195export const handler = withAuthErrorHandling(saveResultsHandler);