Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
"dependencies": {
"@google-cloud/firestore": "^5.0.2",
"@google-cloud/pubsub": "^3.0.1",
"@types/node": "^22.6.1",
"axios": "^0.25.0",
"date-fns": "^2.30.0",
"firebase-admin": "^10",
"firebase-functions": "^3.22.0",
"firebase-functions": "^6.0.1",
"fuse.js": "6.5.3",
"handlebars": "^4.7.8",
"lodash": "^4.17.21",
Expand Down
25 changes: 13 additions & 12 deletions functions/src/analysis/updateBillTracker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { runWith } from "firebase-functions"
import { onDocumentWritten } from "firebase-functions/v2/firestore"

import { isEqual } from "lodash"
import { Bill } from "../bills/types"
import { db, Timestamp } from "../firebase"
Expand All @@ -9,19 +10,18 @@ const currentTrackerVersion = 1
export const billTrackerPath = (billId: string, court: number) =>
`/billTracker/${court}-${billId}`

export const updateBillTracker = runWith({
timeoutSeconds: 10
})
.firestore.document("/generalCourts/{court}/bills/{billId}")
.onWrite(async (change, context) => {
const params = context.params,
// export const updateBillTracker = runWith({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

export const updateBillTracker = onDocumentWritten(
"/generalCourts/{court}/bills/{billId}",
async event => {
const params = event.params,
billId = String(params.billId),
court = Number(params.court)
const previousBill = change.before.exists
? Bill.checkWithDefaults(change.before.data())
const previousBill = event.data?.before.exists
? Bill.checkWithDefaults(event.data.before.data())
: undefined
const newBill = change.after.exists
? Bill.checkWithDefaults(change.after.data())
const newBill = event.data?.after.exists
? Bill.checkWithDefaults(event.data.after.data())
: undefined

if (await shouldUpdateBillTracker(newBill, previousBill)) {
Expand All @@ -36,7 +36,8 @@ export const updateBillTracker = runWith({
}
await db.doc(billTrackerPath(billId, court)).set(tracker, { merge: true })
}
})
}
)

async function shouldUpdateBillTracker(
newBill: Bill | undefined,
Expand Down
9 changes: 5 additions & 4 deletions functions/src/auth/createFakeOrg.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import * as functions from "firebase-functions"
import { checkAdmin, checkAuth } from "../common"
import { auth, db } from "../firebase"
import { onCall } from "firebase-functions/v2/https"

// for populating admin module for testing & demonstration
//@TODO: remove

export const createFakeOrg = functions.https.onCall(async (data, context) => {
checkAuth(context, false)
checkAdmin(context)
export const createFakeOrg = onCall(async request => {
checkAuth(request, false)
checkAdmin(request)

const { uid, fullName, email } = data
const { uid, fullName, email } = request.data

const newUser = {
uid,
Expand Down
92 changes: 45 additions & 47 deletions functions/src/auth/createFakeTestimony.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,54 @@
import * as functions from "firebase-functions"
import { checkAdmin, checkAuth } from "../common"
import { auth, db } from "../firebase"
import { Testimony } from "../testimony/types"
import { Timestamp } from "../firebase"
import { onCall } from "firebase-functions/v2/https"

// for populating admin module for testing & demonstration--alert--no auth checked here.
//@TODO: remove

export const createFakeTestimony = functions.https.onCall(
async (data, context) => {
console.log("running fake testimony")
checkAuth(context, false)
checkAdmin(context)

const { uid, fullName, email } = data

const author = {
uid,
fullName,
email,
password: "password",
public: true,
role: "user"
}

await auth.createUser({ uid })

await db.doc(`profiles/${uid}`).set(author)

const id = `${uid}ttmny`

const testimony: Testimony = {
id,
authorUid: author.uid,
authorDisplayName: "none",
authorRole: "user",
billTitle: "An act",
version: 2,
billId: "H1002",
publishedAt: Timestamp.now(),
court: 192,
position: "oppose",
fullName: fullName,
content: fullName + " " + fullName + " " + fullName + " " + fullName,
public: true,
updatedAt: Timestamp.now()
}

const testRef = db.doc(`users/${uid}/publishedTestimony/${id}`)

await testRef.set(testimony)

return { uid: uid, tid: id }
export const createFakeTestimony = onCall(async request => {
console.log("running fake testimony")
checkAuth(request, false)
checkAdmin(request)

const { uid, fullName, email } = request.data

const author = {
uid,
fullName,
email,
password: "password",
public: true,
role: "user"
}

await auth.createUser({ uid })

await db.doc(`profiles/${uid}`).set(author)

const id = `${uid}ttmny`

const testimony: Testimony = {
id,
authorUid: author.uid,
authorDisplayName: "none",
authorRole: "user",
billTitle: "An act",
version: 2,
billId: "H1002",
publishedAt: Timestamp.now(),
court: 192,
position: "oppose",
fullName: fullName,
content: fullName + " " + fullName + " " + fullName + " " + fullName,
public: true,
updatedAt: Timestamp.now()
}
)

const testRef = db.doc(`users/${uid}/publishedTestimony/${id}`)

await testRef.set(testimony)

return { uid: uid, tid: id }
})
8 changes: 4 additions & 4 deletions functions/src/auth/modifyAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ const Request = z.object({
role: ZRole
})

export const modifyAccount = functions.https.onCall(async (data, context) => {
checkAuth(context, false)
checkAdmin(context)
export const modifyAccount = functions.https.onCall(async request => {
checkAuth(request, false)
checkAdmin(request)

const { uid, role } = checkRequestZod(Request, data)
const { uid, role } = checkRequestZod(Request, request.data)

await setRole({ role, auth, db, uid })
})
16 changes: 8 additions & 8 deletions functions/src/bills/BillProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { QuerySnapshot } from "@google-cloud/firestore"
import { runWith } from "firebase-functions"
import { City } from "../cities/types"
import { Committee } from "../committees/types"
import { DocUpdate } from "../common"
import { db } from "../firebase"
import { Member } from "../members/types"
import { Bill } from "./types"
import { currentGeneralCourt } from "../shared"
import { onMessagePublished } from "firebase-functions/v2/pubsub"
import { onSchedule } from "firebase-functions/v2/scheduler"

export type BillUpdates = Map<string, DocUpdate<Bill>>

Expand All @@ -23,23 +24,22 @@ export default abstract class BillProcessor {
topic: string,
timeoutSeconds = 120
) {
return runWith({ timeoutSeconds })
.pubsub.topic(topic)
.onPublish(async message => {
return onMessagePublished(
{ topic, timeoutSeconds },
async (message: any) => {
if (message.json.run !== true)
throw Error('Expected { "run": true } message')
await new Processor(message.json).run()
})
}
)
}

static scheduled(
Processor: { new (): BillProcessor },
schedule = "every 24 hours",
timeoutSeconds = 120
) {
return runWith({ timeoutSeconds })
.pubsub.schedule(schedule)
.onRun(() => new Processor().run())
return onSchedule({ schedule, timeoutSeconds }, () => new Processor().run())
}

private async run() {
Expand Down
11 changes: 6 additions & 5 deletions functions/src/committees/updateCommitteeRosters.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { runWith } from "firebase-functions"
import { DocUpdate } from "../common"
import { db } from "../firebase"
import { Member } from "../members/types"
import { Committee } from "./types"
import { currentGeneralCourt } from "../shared"
import { onSchedule } from "firebase-functions/v2/scheduler"

/** Updates the list of members in each committee. */
export const updateCommitteeRosters = runWith({ timeoutSeconds: 120 })
.pubsub.schedule("every 24 hours")
.onRun(async () => {
export const updateCommitteeRosters = onSchedule(
{ schedule: "every 24 hours", timeoutSeconds: 120 },
async () => {
const members = await db
.collection(`/generalCourts/${currentGeneralCourt}/members`)
.get()
Expand All @@ -27,7 +27,8 @@ export const updateCommitteeRosters = runWith({ timeoutSeconds: 120 })
)
})
await writer.close()
})
}
)

function computeRosters(members: Member[]) {
const rosters = new Map<string, Member[]>()
Expand Down
10 changes: 5 additions & 5 deletions functions/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ export function checkRequestZod<T extends ZodTypeAny>(

/** Return the authenticated user's id or fail if they are not authenticated. */
export function checkAuth(
context: https.CallableContext,
request: https.CallableRequest,
checkEmailVerification = false
) {
const uid = context.auth?.uid
const uid = request.auth?.uid

if (!uid) {
throw fail("unauthenticated", "Caller must be signed in")
}

if (checkEmailVerification && process.env.FUNCTIONS_EMULATOR !== "true") {
const email_verified = context.auth?.token?.email_verified
const email_verified = request.auth?.token?.email_verified

if (!email_verified) {
throw fail("permission-denied", "You must verify an account first")
Expand All @@ -61,8 +61,8 @@ export function checkAuth(
/**
* Checks that the caller is an admin.
*/
export function checkAdmin(context: https.CallableContext) {
const callerRole = context.auth?.token.role
export function checkAdmin(request: https.CallableRequest) {
const callerRole = request.auth?.token.role
if (callerRole !== "admin") {
throw fail("permission-denied", "You must be an admin")
}
Expand Down
12 changes: 8 additions & 4 deletions functions/src/events/scrapeEvents.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { runWith } from "firebase-functions"
import { DateTime } from "luxon"
import { logFetchError } from "../common"
import { db, Timestamp } from "../firebase"
Expand All @@ -15,6 +14,7 @@ import {
SpecialEventContent
} from "./types"
import { currentGeneralCourt } from "../shared"
import { onSchedule } from "firebase-functions/scheduler"

abstract class EventScraper<ListItem, Event extends BaseEvent> {
private schedule
Expand All @@ -26,9 +26,13 @@ abstract class EventScraper<ListItem, Event extends BaseEvent> {
}

get function() {
return runWith({ timeoutSeconds: this.timeout })
.pubsub.schedule(this.schedule)
.onRun(() => this.run())
return onSchedule(
{
schedule: this.schedule,
timeoutSeconds: this.timeout
},
() => this.run()
)
}

abstract listEvents(): Promise<ListItem[]>
Expand Down
11 changes: 6 additions & 5 deletions functions/src/members/createMemberSearchIndex.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { pubsub } from "firebase-functions"
import { db } from "../firebase"
import { currentGeneralCourt } from "../shared"
import { onSchedule } from "firebase-functions/scheduler"

/** Create a document that aggregates all legislative members for easier
* searching on the client. */
export const createMemberSearchIndex = pubsub
.schedule("every 24 hours")
.onRun(async () => {
export const createMemberSearchIndex = onSchedule(
"every 24 hours",
async () => {
const members = await db
.collection(`/generalCourts/${currentGeneralCourt}/members`)
.get()
Expand All @@ -24,4 +24,5 @@ export const createMemberSearchIndex = pubsub
representatives: index.filter(d => d.Branch === "House"),
senators: index.filter(d => d.Branch === "Senate")
})
})
}
)
11 changes: 6 additions & 5 deletions functions/src/notifications/cleanupNotifications.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as functions from "firebase-functions"
import * as admin from "firebase-admin"
import { Timestamp } from "../firebase"
import { onSchedule } from "firebase-functions/v2/scheduler"

// Get a reference to the Firestore database
const db = admin.firestore()

// Define the cleanupNotifications function
export const cleanupNotifications = functions.pubsub
.schedule("every 24 hours")
.onRun(async context => {
export const cleanupNotifications = onSchedule(
"every 24 hours",
async context => {
// Define the time threshold for old notifications, topic events, and email documents
const retentionPeriodDays = 60 // Adjust this value as needed
const threshold = new Timestamp(
Expand Down Expand Up @@ -46,4 +46,5 @@ export const cleanupNotifications = functions.pubsub

const deleteEmailPromises = emailsSnapshot.docs.map(doc => doc.ref.delete())
await Promise.all(deleteEmailPromises)
})
}
)
Loading