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 // 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);