ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
at master 3.3 kB view raw
1import { AuthenticatedHandler } from './core/types'; 2import type { ExtensionImportRequest, ExtensionImportResponse } from '@atlast/shared'; 3import { z } from 'zod'; 4import crypto from 'crypto'; 5import { withAuthErrorHandling } from './core/middleware'; 6import { ValidationError } from './core/errors'; 7import { UploadRepository, SourceAccountRepository } from './repositories'; 8import { normalize } from './utils/string.utils'; 9import { successResponse } from './utils'; 10 11/** 12 * Validation schema for extension import request 13 */ 14const ExtensionImportSchema = z.object({ 15 platform: z.string(), 16 usernames: z.array(z.string()).min(1).max(10000), 17 metadata: z.object({ 18 extensionVersion: z.string(), 19 scrapedAt: z.string(), 20 pageType: z.enum(['following', 'followers', 'list']), 21 sourceUrl: z.string().url() 22 }) 23}); 24 25/** 26 * Extension import endpoint 27 * POST /extension-import 28 * 29 * Requires authentication. Creates upload and saves usernames immediately. 30 */ 31const extensionImportHandler: AuthenticatedHandler = async (context) => { 32 const body: ExtensionImportRequest = JSON.parse(context.event.body || '{}'); 33 34 // Validate request 35 const validatedData = ExtensionImportSchema.parse(body); 36 37 console.log('[extension-import] Received import:', { 38 did: context.did, 39 platform: validatedData.platform, 40 usernameCount: validatedData.usernames.length, 41 pageType: validatedData.metadata.pageType, 42 extensionVersion: validatedData.metadata.extensionVersion 43 }); 44 45 // Generate upload ID 46 const uploadId = crypto.randomBytes(16).toString('hex'); 47 48 // Create upload and save source accounts 49 const uploadRepo = new UploadRepository(); 50 const sourceAccountRepo = new SourceAccountRepository(); 51 52 // Create upload record 53 await uploadRepo.createUpload( 54 uploadId, 55 context.did, 56 validatedData.platform, 57 validatedData.usernames.length, 58 0 // matchedUsers - will be updated after search 59 ); 60 61 console.log(`[extension-import] Created upload ${uploadId} for user ${context.did}`); 62 63 // Save source accounts using bulk insert and link to upload 64 try { 65 const sourceAccountIdMap = await sourceAccountRepo.bulkCreate( 66 validatedData.platform, 67 validatedData.usernames 68 ); 69 console.log(`[extension-import] Saved ${validatedData.usernames.length} source accounts`); 70 71 // Link source accounts to this upload 72 const links = Array.from(sourceAccountIdMap.values()).map(sourceAccountId => ({ 73 sourceAccountId, 74 sourceDate: validatedData.metadata.scrapedAt 75 })); 76 77 await sourceAccountRepo.linkUserToAccounts(uploadId, context.did, links); 78 console.log(`[extension-import] Linked ${links.length} source accounts to upload`); 79 } catch (error) { 80 console.error('[extension-import] Error saving source accounts:', error); 81 // Continue anyway - upload is created, frontend can still search 82 } 83 84 // Return upload data for frontend to search 85 const response: ExtensionImportResponse = { 86 importId: uploadId, 87 usernameCount: validatedData.usernames.length, 88 redirectUrl: `/?uploadId=${uploadId}` // Frontend will load results from uploadId param 89 }; 90 91 return successResponse(response); 92}; 93 94export const handler = withAuthErrorHandling(extensionImportHandler);