Skip to content

Commit

Permalink
implementing volunteer program
Browse files Browse the repository at this point in the history
  • Loading branch information
bwsarge committed May 26, 2024
1 parent 4def8c9 commit d6cc827
Show file tree
Hide file tree
Showing 15 changed files with 662 additions and 171 deletions.
12 changes: 12 additions & 0 deletions checkers-app/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,15 @@ export const sendWhatsappTestMessage = async (
} as postWhatsappTestMessage)
).data;
};

export const resetCheckerProgram = async (checkerId: string) => {
if (!checkerId) {
throw new Error("Checker ID missing.");
}
const checkerUpdateData: updateChecker = {
programData: "reset",
};
return (
await axiosInstance.patch(`/api/checkers/${checkerId}`, checkerUpdateData)
).data;
};
25 changes: 23 additions & 2 deletions functions/src/definitions/api/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import express from "express"
import * as crypto from "crypto"
import { onRequest } from "firebase-functions/v2/https"
import { logger } from "firebase-functions"
import { Checker } from "../../types"
import { CheckerData } from "../../types"
import { defineString } from "firebase-functions/params"
import { AppEnv } from "../../appEnv"
import { getThresholds } from "../common/utils"
import { Timestamp } from "firebase-admin/firestore"

if (!admin.apps.length) {
admin.initializeApp()
Expand Down Expand Up @@ -119,7 +121,9 @@ app.post("/", async (req, res) => {
//from telegram but not yet a user in database
functions.logger.info("Creating new user")

const checkerObject: Checker = {
const thresholds = await getThresholds()

const checkerObject: CheckerData = {
name: "",
type: "human",
isActive: false,
Expand All @@ -133,7 +137,10 @@ app.post("/", async (req, res) => {
experience: 0,
tier: "beginner",
numVoted: 0,
numReferred: 0,
numReported: 0,
numCorrectVotes: 0,
numNonUnsureVotes: 0,
numVerifiedLinks: 0,
preferredPlatform: "telegram",
lastVotedTimestamp: null,
Expand All @@ -144,6 +151,20 @@ app.post("/", async (req, res) => {
totalTimeTaken: 0,
score: 0,
},
programData: {
isOnProgram: true,
programStart: Timestamp.fromDate(new Date()),
programEnd: null,
numVotesTarget: thresholds.volunteerProgramVotesRequirement ?? 0, //target number of messages voted on to complete program
numReferralTarget: thresholds.volunteerProgramReferralRequirement ?? 0, //target number of referrals made to complete program
numReportTarget: thresholds.volunteerProgramReportRequirement ?? 0, //number of non-trivial messages sent in to complete program
accuracyTarget: thresholds.volunteerProgramAccuracyRequirement ?? 0, //target accuracy of non-unsure votes
numVotesAtProgramStart: 0,
numReferralsAtProgramStart: 0,
numReportsAtProgramStart: 0,
numCorrectVotesAtProgramStart: 0,
numNonUnsureVotesAtProgramStart: 0,
},
}

try {
Expand Down
146 changes: 45 additions & 101 deletions functions/src/definitions/api/handlers/getChecker.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Request, Response } from "express"
import { Checker } from "../interfaces"
import { CheckerData } from "../../../types"
import * as admin from "firebase-admin"
import { Timestamp } from "firebase-admin/firestore"
import { logger } from "firebase-functions/v2"
import { checkAccuracy } from "../../common/statistics"
import { TIME } from "../../../utils/time"
import {
computeLast30DaysStats,
computeProgramStats,
} from "../../common/statistics"
if (!admin.apps.length) {
admin.initializeApp()
}
Expand All @@ -25,7 +27,7 @@ const getCheckerHandler = async (req: Request, res: Response) => {
return res.status(404).send(`Checker with id ${checkerId} not found`)
}

const checkerData = checkerSnap.data()
const checkerData = checkerSnap.data() as CheckerData

if (!checkerData) {
return res.status(500).send("Checker data not found")
Expand All @@ -38,115 +40,57 @@ const getCheckerHandler = async (req: Request, res: Response) => {
const pendingVoteSnap = await pendingVoteQuery.count().get()
const pendingVoteCount = pendingVoteSnap.data().count

const cutoffTimestamp = Timestamp.fromDate(
new Date(Date.now() - TIME.THIRTY_DAYS)
)

const last30DaysQuery = db
.collectionGroup("voteRequests")
.where("factCheckerDocRef", "==", checkerRef)
.where("createdTimestamp", ">", cutoffTimestamp)

const last30DaysSnap = await last30DaysQuery.get()

//filter client side for category != null, since firestore doesn't support inequality on 2 fields
const last30DaysData = last30DaysSnap.docs.filter(
(doc) => doc.get("category") !== null && doc.get("category") !== "pass"
)

const totalVoted = last30DaysData.length

// Map each document to a promise to fetch the parent message and count instances
const fetchDataPromises = last30DaysData.map((doc) => {
const parentMessageRef = doc.ref.parent.parent // Assuming this is how you get the reference
if (!parentMessageRef) {
logger.error(`Vote request ${doc.id} has no parent message`)
return null
}

// You can fetch the parent message and count instances in parallel for each doc
return Promise.all([
parentMessageRef.get(),
parentMessageRef.collection("instances").count().get(),
])
.then(([parentMessageSnap, instanceCountResult]) => {
if (!parentMessageSnap.exists) {
logger.error(`Parent message not found for vote request ${doc.id}`)
return null
}
const instanceCount = instanceCountResult.data().count ?? 0
const isAccurate = checkAccuracy(parentMessageSnap, doc)
const isAssessed = parentMessageSnap.get("isAssessed") ?? false
const votedTimestamp = doc.get("votedTimestamp") ?? null
const createdTimestamp = doc.get("createdTimestamp") ?? null

// You may adjust what you return based on your needs
return {
votedTimestamp,
createdTimestamp,
isAccurate,
isAssessed,
instanceCount,
}
})
.catch((error) => {
logger.error(
`Error fetching data for vote request ${doc.id}: ${error}`
)
return null // Handle errors as appropriate for your use case
})
})

// Wait for all fetches to complete
const results = await Promise.all(fetchDataPromises)
//calculate accuracy
const accurateCount = results.filter(
(d) => d !== null && d.isAccurate
).length
const totalAssessedAndNonUnsureCount = results.filter(
(d) => d !== null && d.isAssessed && d.isAccurate !== null
).length
const totalCount = results.filter((d) => d !== null).length
//calculate people helped
const peopleHelped = results.reduce(
(acc, d) => acc + (d !== null ? d.instanceCount : 0),
0
)
//calculate average response time, given data has a createdTimestamp and a votedTimestamp
const totalResponseTime = results.reduce((acc, d) => {
if (d === null) {
return acc
}
if (d.createdTimestamp && d.votedTimestamp) {
const responseTimeMinutes =
(d.votedTimestamp.toMillis() - d.createdTimestamp.toMillis()) / 60000
return acc + responseTimeMinutes
}
return acc
}, 0)
const averageResponseTime = totalResponseTime / (totalCount || 1)

const accuracyRate =
totalAssessedAndNonUnsureCount === 0
? null
: accurateCount / totalAssessedAndNonUnsureCount
const returnData: Checker = {
name: checkerData.name,
type: checkerData.type,
isActive: checkerData.isActive,
tier: checkerData.tier,
isAdmin: checkerData.isAdmin,
isOnboardingComplete: checkerData.isOnboardingComplete,
isOnProgram: checkerData.programData.isOnProgram ?? false,
pendingVoteCount: pendingVoteCount,
last30days: {
achievements: null,
level: 0, //TODO,check
experience: 0, //TOD0
}

if (checkerData.programData.isOnProgram) {
try {
const {
numVotes,
numReferrals,
numReports,
accuracy,
isProgramCompleted,
} = await computeProgramStats(checkerSnap)

returnData.programStats = {
//TODO
numVotes: numVotes,
numVotesTarget: checkerData.programData.numVotesTarget,
numReferrals: numReferrals,
numReferralTarget: checkerData.programData.numReferralTarget,
numReports: numReports,
numReportTarget: checkerData.programData.numReportTarget,
accuracy: accuracy,
accuracyTarget: checkerData.programData.accuracyTarget,
isProgramCompleted: isProgramCompleted,
}
} catch {
logger.error("Error fetching program stats")
return res.status(500).send("Error fetching program stats")
}
//calculate and send back program statistics
} else {
//calculate and send back last 30 day statistics
const { totalVoted, accuracyRate, averageResponseTime, peopleHelped } =
await computeLast30DaysStats(checkerSnap)
returnData.last30days = {
totalVoted: totalVoted,
accuracyRate: accuracyRate,
averageResponseTime: averageResponseTime,
peopleHelped: peopleHelped,
},
achievements: null,
level: 0, //TODO,
experience: 0, //TOD0
}
}
res.status(200).send(returnData)
} catch (error) {
Expand Down
36 changes: 33 additions & 3 deletions functions/src/definitions/api/handlers/patchChecker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Request, Response } from "express"
import { Checker } from "../../../types"
import { CheckerData } from "../../../types"
import * as admin from "firebase-admin"
import { logger } from "firebase-functions/v2"
import { Timestamp } from "firebase-admin/firestore"
import { getThresholds } from "../../common/utils"
if (!admin.apps.length) {
admin.initializeApp()
}
Expand All @@ -26,7 +28,7 @@ const patchCheckerHandler = async (req: Request, res: Response) => {
//check keys in request body, make sure they are defined in checker type
const body = req.body
const keys = Object.keys(body)
const checker = checkerSnap.data() as Checker
const checker = checkerSnap.data() as CheckerData
const checkerKeys = Object.keys(checker)
const validKeys = keys.every((key) => checkerKeys.includes(key))

Expand All @@ -50,11 +52,39 @@ const patchCheckerHandler = async (req: Request, res: Response) => {
return res.status(400).send("tier cannot be updated")
}

if (keys.includes("programData")) {
if (
typeof body.programData !== "string" ||
body.programData !== "reset"
) {
return res
.status(400)
.send("programData will only work with the value 'reset'")
} else {
const thresholds = await getThresholds()
body.programData = {
isOnProgram: true,
programStart: Timestamp.fromDate(new Date()),
programEnd: null,
numVotesTarget: thresholds.volunteerProgramVotesRequirement ?? 0, //target number of messages voted on to complete program
numReferralTarget:
thresholds.volunteerProgramReferralRequirement ?? 0, //target number of referrals made to complete program
numReportTarget: thresholds.volunteerProgramReportRequirement ?? 0, //number of non-trivial messages sent in to complete program
accuracyTarget: thresholds.volunteerProgramAccuracyRequirement ?? 0, //target accuracy of non-unsure votes
numVotesAtProgramStart: checker.numVoted ?? 0,
numReferralsAtProgramStart: checker.numReferred ?? 0,
numReportsAtProgramStart: checker.numReported ?? 0,
numCorrectVotesAtProgramStart: checker.numCorrectVotes ?? 0,
numNonUnsureVotesAtProgramStart: checker.numNonUnsureVotes ?? 0,
}
}
}

//update checker
await checkerRef.update(body)

const updatedCheckerSnap = await checkerRef.get()
const updatedChecker = updatedCheckerSnap.data() as Checker
const updatedChecker = updatedCheckerSnap.data() as CheckerData

res.status(200).send(updatedChecker)
} catch (error) {
Expand Down
29 changes: 27 additions & 2 deletions functions/src/definitions/api/handlers/postChecker.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Request, Response } from "express"
import { createChecker } from "../interfaces"
import { Checker } from "../../../types"
import { CheckerData } from "../../../types"
import * as admin from "firebase-admin"
import { logger } from "firebase-functions/v2"
import { Timestamp } from "firebase-admin/firestore"
import { getThresholds } from "../../common/utils"

if (!admin.apps.length) {
admin.initializeApp()
Expand All @@ -23,7 +25,10 @@ const postCheckerHandler = async (req: Request, res: Response) => {
level,
experience,
numVoted,
numReferred,
numReported,
numCorrectVotes,
numNonUnsureVotes,
numVerifiedLinks,
preferredPlatform,
lastVotedTimestamp,
Expand All @@ -44,7 +49,10 @@ const postCheckerHandler = async (req: Request, res: Response) => {
return res.status(409).send("Checker agent name already exists")
}
}
const newChecker: Checker = {

const thresholds = await getThresholds()

const newChecker: CheckerData = {
name,
type,
isActive: isActive || false,
Expand All @@ -58,7 +66,10 @@ const postCheckerHandler = async (req: Request, res: Response) => {
experience: experience || 0,
tier: "beginner",
numVoted: numVoted || 0,
numReferred: numReferred || 0,
numReported: numReported || 0,
numCorrectVotes: numCorrectVotes || 0,
numNonUnsureVotes: numNonUnsureVotes || 0,
numVerifiedLinks: numVerifiedLinks || 0,
preferredPlatform: preferredPlatform || type === "ai" ? null : "telegram",
lastVotedTimestamp: lastVotedTimestamp || null,
Expand All @@ -69,6 +80,20 @@ const postCheckerHandler = async (req: Request, res: Response) => {
totalTimeTaken: 0,
score: 0,
},
programData: {
isOnProgram: type === "human" ? true : false,
programStart: type === "human" ? Timestamp.fromDate(new Date()) : null,
programEnd: null,
numVotesTarget: thresholds.volunteerProgramVotesRequirement ?? 0, //target number of messages voted on to complete program
numReferralTarget: thresholds.volunteerProgramReferralRequirement ?? 0, //target number of referrals made to complete program
numReportTarget: thresholds.volunteerProgramReportRequirement ?? 0, //number of non-trivial messages sent in to complete program
accuracyTarget: thresholds.volunteerProgramAccuracyRequirement ?? 0, //target accuracy of non-unsure votes
numVotesAtProgramStart: 0,
numReferralsAtProgramStart: 0,
numReportsAtProgramStart: 0,
numCorrectVotesAtProgramStart: 0,
numNonUnsureVotesAtProgramStart: 0,
},
}

logger.info("Creating new checker", newChecker)
Expand Down
Loading

0 comments on commit d6cc827

Please sign in to comment.