From 06f7e0719b8f91aa01c106513e07956502b0e9a0 Mon Sep 17 00:00:00 2001 From: astordg Date: Tue, 3 Feb 2026 11:09:40 -0500 Subject: [PATCH] Scrape Single function to firebase v2 Implemented a v2 version of the scrape single hearing function. Changed the frontend to call the v2 version. Updated the check auth and check admin functions to be able to take a request object for both v1 and v2 firebase functino --- components/moderation/ScrapeHearing.tsx | 7 +++- functions/src/common.ts | 5 ++- functions/src/events/index.ts | 1 + functions/src/events/scrapeEvents.ts | 51 ++++++++++++++++++++++++- functions/src/index.ts | 3 +- 5 files changed, 61 insertions(+), 6 deletions(-) diff --git a/components/moderation/ScrapeHearing.tsx b/components/moderation/ScrapeHearing.tsx index bfb6c648a..ab97503f4 100644 --- a/components/moderation/ScrapeHearing.tsx +++ b/components/moderation/ScrapeHearing.tsx @@ -16,6 +16,11 @@ const scrapeSingleHearing = httpsCallable< ScrapeHearingResponse >(functions, "scrapeSingleHearing") +const scrapeSingleHearingv2 = httpsCallable< + ScrapeHearingRequest, + ScrapeHearingResponse +>(functions, "scrapeSingleHearingv2") + export const ScrapeHearingForm = () => { const [eventId, setEventId] = useState("") const [loading, setLoading] = useState(false) @@ -39,7 +44,7 @@ export const ScrapeHearingForm = () => { setLoading(true) try { - const response = await scrapeSingleHearing({ eventId: parsedEventId }) + const response = await scrapeSingleHearingv2({ eventId: parsedEventId }) setResult({ type: "success", message: `${response.data.message} (ID: ${response.data.hearingId})` diff --git a/functions/src/common.ts b/functions/src/common.ts index aed0ab6d5..7838ada3f 100644 --- a/functions/src/common.ts +++ b/functions/src/common.ts @@ -1,6 +1,7 @@ import { FieldValue } from "@google-cloud/firestore" import axios from "axios" import { https, logger } from "firebase-functions" +import { CallableRequest } from "firebase-functions/v2/https" import { Null, Nullish, @@ -38,7 +39,7 @@ export function checkRequestZod( /** Return the authenticated user's id or fail if they are not authenticated. */ export function checkAuth( - context: https.CallableContext, + context: https.CallableContext | CallableRequest, checkEmailVerification = false ) { const uid = context.auth?.uid @@ -61,7 +62,7 @@ export function checkAuth( /** * Checks that the caller is an admin. */ -export function checkAdmin(context: https.CallableContext) { +export function checkAdmin(context: https.CallableContext | CallableRequest) { const callerRole = context.auth?.token.role if (callerRole !== "admin") { throw fail("permission-denied", "You must be an admin") diff --git a/functions/src/events/index.ts b/functions/src/events/index.ts index 9a7a84fad..96ff5307d 100644 --- a/functions/src/events/index.ts +++ b/functions/src/events/index.ts @@ -1,2 +1,3 @@ export * from "./scrapeEvents" export { scrapeSingleHearing } from "./scrapeEvents" +export { scrapeSingleHearingv2 } from "./scrapeEvents" diff --git a/functions/src/events/scrapeEvents.ts b/functions/src/events/scrapeEvents.ts index 38a1da7ba..4c989f16c 100644 --- a/functions/src/events/scrapeEvents.ts +++ b/functions/src/events/scrapeEvents.ts @@ -1,5 +1,6 @@ -import * as functions from "firebase-functions" -import { RuntimeOptions, runWith } from "firebase-functions" +import * as functions from "firebase-functions/v1" +import { RuntimeOptions, runWith } from "firebase-functions/v1" +import { onCall, CallableRequest } from "firebase-functions/v2/https" import { DateTime } from "luxon" import { JSDOM } from "jsdom" import { AssemblyAI } from "assemblyai" @@ -476,6 +477,52 @@ export const scrapeSingleHearing = functions } }) +export const scrapeSingleHearingv2 = onCall( + { timeoutSeconds: 480, memory: "4GiB", secrets: ["ASSEMBLY_API_KEY"] }, + async (request: CallableRequest) => { + // Require admin authentication + // Check how to integrate the new object with these helper functions + checkAuth(request, false) + checkAdmin(request) + + const { eventId } = request.data + + if (!eventId || typeof eventId !== "number") { + throw new functions.https.HttpsError( + "invalid-argument", + "The function must be called with a valid eventId (number)." + ) + } + + try { + // Create a temporary scraper instance to reuse the existing logic + const scraper = new HearingScraper() + const hearing = await scraper.getEvent( + { EventId: eventId }, + { ignoreCutoff: true } + ) + + // Save the hearing to Firestore + await db.doc(`/events/${hearing.id}`).set(hearing, { merge: true }) + + console.log(`Successfully scraped hearing ${eventId}`, hearing) + + return { + status: "success", + message: `Successfully scraped hearing ${eventId}`, + hearingId: hearing.id + } + } catch (error: any) { + console.error(`Failed to scrape hearing ${eventId}:`, error) + throw new functions.https.HttpsError( + "internal", + `Failed to scrape hearing ${eventId}`, + { details: error.message } + ) + } + } +) + export const scrapeSpecialEvents = new SpecialEventsScraper().function export const scrapeSessions = new SessionScraper().function export const scrapeHearings = new HearingScraper().function diff --git a/functions/src/index.ts b/functions/src/index.ts index 51343963d..d3effd4be 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -19,7 +19,8 @@ export { scrapeHearings, scrapeSessions, scrapeSpecialEvents, - scrapeSingleHearing + scrapeSingleHearing, + scrapeSingleHearingv2 } from "./events" export { syncHearingToSearchIndex,