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