ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: implement extension import in hono

byarielm.fyi e4b06075 88521a16

verified
+117
+115
packages/api/src/routes/extension.ts
··· 1 + /** 2 + * Extension Import Routes 3 + * Handles data imports from the browser extension 4 + */ 5 + 6 + import { Hono } from 'hono'; 7 + import { z } from 'zod'; 8 + import crypto from 'crypto'; 9 + import { authMiddleware } from '../middleware'; 10 + import { UploadRepository, SourceAccountRepository } from '../repositories'; 11 + import { ValidationError } from '../errors'; 12 + import type { ExtensionImportRequest, ExtensionImportResponse } from '@atlast/shared'; 13 + import type { AppEnv } from '../types/hono'; 14 + 15 + const extension = new Hono<AppEnv>(); 16 + 17 + /** 18 + * Validation schema for extension import request 19 + */ 20 + const ExtensionImportSchema = z.object({ 21 + platform: z.string(), 22 + usernames: z.array(z.string()).min(1).max(10000), 23 + metadata: z.object({ 24 + extensionVersion: z.string(), 25 + scrapedAt: z.string(), 26 + pageType: z.enum(['following', 'followers', 'list']), 27 + sourceUrl: z.string().url(), 28 + }), 29 + }); 30 + 31 + /** 32 + * POST /api/extension/import 33 + * 34 + * Import usernames scraped by the browser extension. 35 + * Creates an upload record and saves source accounts. 36 + * 37 + * @requires authentication 38 + * @body {ExtensionImportRequest} - Extension data with usernames and metadata 39 + * @returns {ExtensionImportResponse} - Import ID and redirect URL 40 + */ 41 + extension.post('/import', authMiddleware, async (c) => { 42 + const did = c.get('did'); 43 + const body = await c.req.json<ExtensionImportRequest>(); 44 + 45 + // Validate request 46 + const validation = ExtensionImportSchema.safeParse(body); 47 + if (!validation.success) { 48 + throw new ValidationError( 49 + 'Invalid request. Please provide valid platform, usernames array (1-10000), and metadata.', 50 + ); 51 + } 52 + 53 + const validatedData = validation.data; 54 + 55 + console.log('[extension-import] Received import:', { 56 + did, 57 + platform: validatedData.platform, 58 + usernameCount: validatedData.usernames.length, 59 + pageType: validatedData.metadata.pageType, 60 + extensionVersion: validatedData.metadata.extensionVersion, 61 + }); 62 + 63 + // Generate upload ID 64 + const uploadId = crypto.randomBytes(16).toString('hex'); 65 + 66 + // Create upload and save source accounts 67 + const uploadRepo = new UploadRepository(); 68 + const sourceAccountRepo = new SourceAccountRepository(); 69 + 70 + // Create upload record 71 + await uploadRepo.createUpload( 72 + uploadId, 73 + did, 74 + validatedData.platform, 75 + validatedData.usernames.length, 76 + 0, // matchedUsers - will be updated after search 77 + ); 78 + 79 + console.log(`[extension-import] Created upload ${uploadId} for user ${did}`); 80 + 81 + // Save source accounts using bulk insert and link to upload 82 + try { 83 + const sourceAccountIdMap = await sourceAccountRepo.bulkCreate( 84 + validatedData.platform, 85 + validatedData.usernames, 86 + ); 87 + console.log(`[extension-import] Saved ${validatedData.usernames.length} source accounts`); 88 + 89 + // Link source accounts to this upload 90 + const links = Array.from(sourceAccountIdMap.values()).map((sourceAccountId) => ({ 91 + sourceAccountId, 92 + sourceDate: validatedData.metadata.scrapedAt, 93 + })); 94 + 95 + await sourceAccountRepo.linkUserToAccounts(uploadId, did, links); 96 + console.log(`[extension-import] Linked ${links.length} source accounts to upload`); 97 + } catch (error) { 98 + console.error('[extension-import] Error saving source accounts:', error); 99 + // Continue anyway - upload is created, frontend can still search 100 + } 101 + 102 + // Return upload data for frontend to search 103 + const response: ExtensionImportResponse = { 104 + importId: uploadId, 105 + usernameCount: validatedData.usernames.length, 106 + redirectUrl: `/?uploadId=${uploadId}`, // Frontend will load results from uploadId param 107 + }; 108 + 109 + return c.json({ 110 + success: true, 111 + data: response, 112 + }); 113 + }); 114 + 115 + export default extension;
+2
packages/api/src/server.ts
··· 9 9 import searchRoutes from "./routes/search"; 10 10 import resultsRoutes from "./routes/results"; 11 11 import followRoutes from "./routes/follow"; 12 + import extensionRoutes from "./routes/extension"; 12 13 import { db } from "./db/client"; 13 14 import { sql } from "kysely"; 14 15 ··· 55 56 app.route("/api/search", searchRoutes); 56 57 app.route("/api/results", resultsRoutes); 57 58 app.route("/api/follow", followRoutes); 59 + app.route("/api/extension", extensionRoutes); 58 60 59 61 // Health check endpoint (Phase 3C - with database check) 60 62 app.get("/api/health", async (c) => {