Skip to content

Commit

Permalink
Merge pull request #498 from bettersg/feat/change-calculations
Browse files Browse the repository at this point in the history
Improved Checker Ops
  • Loading branch information
sarge1989 authored Nov 2, 2024
2 parents 3d16315 + 964a807 commit 4e3ebef
Show file tree
Hide file tree
Showing 34 changed files with 1,253 additions and 132 deletions.
8 changes: 3 additions & 5 deletions checkers-app/src/components/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,7 @@ export default function Dashboard() {
name="Voting Accuracy"
img_src="/accuracy.png"
current={
programStats.accuracy === null
? 0
: programStats.accuracy * 100
programStats.accuracy === null ? 0 : programStats.accuracy * 100
}
target={programStats.accuracyTarget * 100}
isPercentageTarget={true}
Expand Down Expand Up @@ -158,8 +156,8 @@ export default function Dashboard() {
>
WhatsApp Bot
</a>
. You need to submit at least{" "}
{programStats.numReportTarget} messages.
. You need to submit at least {programStats.numReportTarget}{" "}
messages that are not eventually marked nvc-can't tell.
</>
}
/>
Expand Down
6 changes: 5 additions & 1 deletion functions/src/definitions/api/handlers/postChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const postCheckerHandler = async (req: Request, res: Response) => {
// Check request body
const {
name,
telegramUsername,
type,
isActive,
isOnboardingComplete,
Expand Down Expand Up @@ -56,6 +57,7 @@ const postCheckerHandler = async (req: Request, res: Response) => {

const newChecker: CheckerData = {
name,
telegramUsername,
type,
isActive: isActive || false,
isOnboardingComplete: isOnboardingComplete || false,
Expand All @@ -81,7 +83,8 @@ const postCheckerHandler = async (req: Request, res: Response) => {
preferredPlatform: preferredPlatform || (type === "ai" ? null : "telegram"),
lastVotedTimestamp: lastVotedTimestamp || null,
getNameMessageId: null,
certificateUrl: null, // Initialize certificateUrl as an empty string
hasCompletedProgram: false,
certificateUrl: null,
leaderboardStats: {
numVoted: 0,
numCorrectVotes: 0,
Expand All @@ -107,6 +110,7 @@ const postCheckerHandler = async (req: Request, res: Response) => {
numCorrectVotesAtProgramEnd: null,
numNonUnsureVotesAtProgramEnd: null,
},
offboardingTime: null,
}

logger.info("Creating new checker", newChecker)
Expand Down
2 changes: 2 additions & 0 deletions functions/src/definitions/api/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface updateVoteRequest {

interface createChecker {
name: string | null
telegramUsername: string | null
type: "human" | "ai"
isActive?: boolean
isOnboardingComplete?: boolean
Expand All @@ -47,6 +48,7 @@ interface createChecker {

interface updateChecker {
name?: string
telegramUsername?: string | null
type?: "human" | "ai"
isActive?: boolean
isOnboardingComplete?: boolean
Expand Down
218 changes: 161 additions & 57 deletions functions/src/definitions/batchJobs/batchJobs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as admin from "firebase-admin"
import { sendWhatsappTemplateMessage } from "../common/sendWhatsappMessage"
import {
respondToInstance,
sendInterimPrompt as sendInterimPromptImported,
Expand All @@ -14,61 +13,111 @@ import { sendTelegramTextMessage } from "../common/sendTelegramMessage"
import { AppEnv } from "../../appEnv"
import { TIME } from "../../utils/time"
import { getFullLeaderboard } from "../common/statistics"
import { getResponsesObj } from "../common/responseUtils"
import { checkCheckerActivity } from "../../services/checker/checkActivity"
import { enqueueTask } from "../common/cloudTasks"

const runtimeEnvironment = defineString(AppEnv.ENVIRONMENT)
const checkerAppHost = process.env.CHECKER_APP_HOST
const CHECKERS_GROUP_LINK = String(process.env.CHECKERS_GROUP_LINK)
const CHECKERS_CHAT_ID = String(process.env.CHECKERS_CHAT_ID)

if (!admin.apps.length) {
admin.initializeApp()
}
const db = admin.firestore()

async function deactivateAndRemind() {
async function handleInactiveCheckers() {
try {
const cutoffHours = 72
const remindAfterDays = 3
const deactivateAfterDays = 10
const remindAfter =
runtimeEnvironment.value() === "PROD"
? remindAfterDays * 24 * 60 * 60 //3 days in seconds
: 60
const deactivateAfter =
runtimeEnvironment.value() === "PROD"
? deactivateAfterDays * 24 * 60 * 60 //10 days in seconds
: 300
const activeCheckMatesSnap = await db
.collection("checkers")
.where("type", "==", "human")
.where("isActive", "==", true)
.get()
const promisesArr = activeCheckMatesSnap.docs.map(async (doc) => {
const lastVotedTimestamp =
doc.get("lastVotedTimestamp") ?? Timestamp.fromDate(new Date(0))
const factCheckerDocRef = doc.ref
const whatsappId = doc.get("whatsappId")
const telegramId = doc.get("telegramId")
const preferredPlatform = doc.get("preferredPlatform") ?? "whatsapp"
const lastVotedDate = lastVotedTimestamp.toDate()
//set cutoff to 72 hours ago
const cutoffDate = new Date(Date.now() - cutoffHours * TIME.ONE_HOUR)
const cutoffTimestamp = Timestamp.fromDate(cutoffDate)
const voteRequestsQuerySnap = await db
.collectionGroup("voteRequests")
.where("factCheckerDocRef", "==", factCheckerDocRef)
.where("createdTimestamp", "<", cutoffTimestamp)
.where("isAutoPassed", "==", true)
.get()
if (!voteRequestsQuerySnap.empty && lastVotedDate < cutoffDate) {
const deactivationCheckResponse = await checkCheckerActivity(
doc,
deactivateAfter
)
if (deactivationCheckResponse.data?.isActive === false) {
logger.log(`Checker ${doc.id}, ${doc.get("name")} set to inactive`)
if (preferredPlatform === "whatsapp") {
if (!whatsappId) {
if (preferredPlatform === "telegram") {
if (!telegramId) {
logger.error(
`No whatsappId for ${doc.id}, ${doc.get(
`No telegramId for ${doc.id}, ${doc.get(
"name"
)} despite preferred platform being whatsapp`
)} despite preferred platform being telegram`
)
return Promise.resolve()
}
const replyMarkup = {
inline_keyboard: [
[
{
text: "Reactivate Now",
callback_data: "REACTIVATE",
},
],
],
}
const responses = await getResponsesObj("factChecker")
const deactivationMessage = responses.DEACTIVATION.replace(
"{{name}}",
doc.get("name")
)
await doc.ref.update({ isActive: false })
return sendWhatsappTemplateMessage(

//enqueue reactivation message
const lastVotedTimestamp = doc.get("lastVotedTimestamp")
const onboardingTime = doc.get("onboardingTime")
const referenceTime =
lastVotedTimestamp ?? onboardingTime ?? Timestamp.now()
const secondsSinceLastVote = Math.floor(
Date.now() / 1000 - referenceTime.seconds
)
const baseDelaySeconds = 7 * 24 * 60 * 60 // 1 week in seconds
const nextAttemptPayload = {
attemptNumber: 1,
maxAttempts: 4,
baseDelaySeconds: baseDelaySeconds,
cumulativeDelaySeconds: secondsSinceLastVote + baseDelaySeconds,
checkerId: doc.id,
}
await enqueueTask(
nextAttemptPayload,
"sendCheckerReactivation",
baseDelaySeconds,
"asia-southeast1"
)
return sendTelegramTextMessage(
"factChecker",
whatsappId,
"deactivation_notification",
"en",
[doc.get("name") || "CheckMate", `${cutoffHours}`],
[`${whatsappId}`]
telegramId,
deactivationMessage,
null,
"HTML",
replyMarkup
)
} else if (preferredPlatform === "telegram") {
} else {
logger.error("Unsupported preferred platform for checker")
return
}
}
//if didn't hit 10 days, check 3 days reminder
const reminderCheckResponse = await checkCheckerActivity(doc, remindAfter)
if (reminderCheckResponse.data?.isActive === false) {
logger.log(`Reminder sent to checker ${doc.id}, ${doc.get("name")}`)
if (preferredPlatform === "telegram") {
if (!telegramId) {
logger.error(
`No telegramId for ${doc.id}, ${doc.get(
Expand All @@ -77,31 +126,24 @@ async function deactivateAndRemind() {
)
return Promise.resolve()
}
const replyMarkup = checkerAppHost
? {
inline_keyboard: [
[
{
text: "CheckMates' Portal↗️",
web_app: { url: checkerAppHost },
},
],
],
}
: null
const reactivationMessage = `Hello ${doc.get(
"name"
)}! Thanks for all your contributions so farπŸ™. We noticed that you haven't voted in 3 days! You're probably busy, and we don't want your votes to pile up, so we're temporarily stopped sending you messages to vote on.
To resume getting messages, just vote on any of your outstanding messages, which you can find by visiting the CheckMates' Portal. Remember, if you're busy, you can vote "pass" too!`
await doc.ref.update({ isActive: false })
const responses = await getResponsesObj("factChecker")
const reminderMessage = responses.REMINDER.replace(
"{{name}}",
doc.get("name")
)
.replace("{{checkers_group_link}}", CHECKERS_GROUP_LINK)
.replace("{{num_days}}", `${remindAfterDays}`)
return sendTelegramTextMessage(
"factChecker",
telegramId,
reactivationMessage,
reminderMessage,
null,
replyMarkup
"HTML",
null
)
} else {
logger.error("Unsupported preferred platform for checker")
return
}
}
})
Expand Down Expand Up @@ -173,6 +215,54 @@ async function interimPromptHandler() {
}
}

async function welcomeNewCheckers() {
// find checkers that onboarded since last week 12pm on Tuesday
try {
const lastWeek = new Date()
lastWeek.setDate(lastWeek.getDate() - 7)
lastWeek.setHours(12, 3, 0, 0)
const lastWeekTimestamp = Timestamp.fromDate(lastWeek)
const checkersQuerySnap = await db
.collection("checkers")
.where("onboardingTime", ">=", lastWeekTimestamp)
.get()
if (checkersQuerySnap.empty) {
return
}

// Create concatenated string of names
const names = checkersQuerySnap.docs
.map((doc) => {
const name = doc.get("name")
const telegramUsername = doc.get("telegramUsername")
const username = telegramUsername ? ` @${telegramUsername}` : ""
return `${name}${username}`
})
.join("\n")

// Get responses object
const responses = await getResponsesObj("factChecker")
const welcomeMessage = responses.WELCOME.replace("{{names}}", names)

// Send single welcome message to group chat
if (!CHECKERS_CHAT_ID) {
logger.error("Missing TELEGRAM_CHECKERS_GROUP_ID env var")
return
}

await sendTelegramTextMessage(
"admin",
CHECKERS_CHAT_ID,
welcomeMessage,
null,
"HTML",
null
)
} catch (error) {
logger.error("Error occured in welcomeNewCheckers:", error)
}
}

async function resetLeaderboardHandler() {
await saveLeaderboard()
try {
Expand Down Expand Up @@ -226,14 +316,20 @@ const scheduledDeactivation = onSchedule(
{
schedule: "11 20 * * *",
timeZone: "Asia/Singapore",
secrets: [
"WHATSAPP_CHECKERS_BOT_PHONE_NUMBER_ID",
"WHATSAPP_TOKEN",
"TELEGRAM_CHECKER_BOT_TOKEN",
],
secrets: ["TELEGRAM_CHECKER_BOT_TOKEN"],
region: "asia-southeast1",
},
deactivateAndRemind
handleInactiveCheckers
)

const sendCheckersWelcomeMesssage = onSchedule(
{
schedule: "3 12 * * 2",
timeZone: "Asia/Singapore",
secrets: ["TELEGRAM_ADMIN_BOT_TOKEN"],
region: "asia-southeast1",
},
welcomeNewCheckers
)

const sendInterimPrompt = onSchedule(
Expand All @@ -255,10 +351,18 @@ const resetLeaderboard = onSchedule(
resetLeaderboardHandler
)

export {
// Export scheduled cloud functions
export const batchJobs = {
checkSessionExpiring,
scheduledDeactivation,
sendCheckersWelcomeMesssage,
sendInterimPrompt,
resetLeaderboard,
}

// Export utility functions
export const utils = {
handleInactiveCheckers,
welcomeNewCheckers,
interimPromptHandler,
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@
"NOT_A_REPLY": "Sorry, did you forget to reply to a message? You need to swipe right on the message to reply to it.",
"OUTSTANDING_REMINDER": "You have *{{num_outstanding}} remaining messages* to assess. Would you like to be sent the next one in line?",
"NO_OUTSTANDING": "Great, you have no further messages to assess. Keep it up!πŸ’ͺ",
"PROGRAM_COMPLETED": "Congratulations! You have completed our CheckMate volunteers program with the following stats:\n\nNo. of messages voted on: {{num_messages}}\nAccuracy: {{accuracy}}\nNo. of new users referred: {{num_referred}}\nNo. of non-trivial messages reported: {{num_reported}}\n\nand are now a certified CheckMate!! πŸŽ‰πŸ₯³"
"REMINDER": "<b>πŸ“ We Miss You! πŸ‘€</b>\n\nHey {{name}}, it looks like you've been inactive for the past {{num_days}} days. Just a friendly reminder: if we don't see any activity for 7 days, your access as a checker will be temporarily deactivated.\n\nIf you need any help or have any questions, feel free to ask in the Q&A channel of our <a href='{{checkers_group_link}}'>Checker's Telegram group</a> - we're here to help! 😊",
"DEACTIVATION": "<b>πŸ“ Temporary Deactivation Notice</b>\n\nHi {{name}},\n\nYour access as a checker has been temporarily deactivated due to inactivity. No worries though - simply press the button below to get back to checking! πŸš€\n\nWe're excited to see you back in action soon 😊",
"MANAGE_OUT": "<b>πŸ“ CheckMate Fact-checker Programme Status</b>\n\nHi {{name}},\n\nThank you so much for your time and dedication during the CheckMate programme. While you've given it your best, it seems we haven't quite met the criteria needed to continue as a volunteer in our checking crew.\n\nWe'd love to hear about your experience - your feedback will help us improve! Please take a moment to fill out this <a href='{{survey_link}}'>quick survey</a>: \n\nAs part of this process, you'll also be removed from the Checkers' Crew Telegram chat. We appreciate your understanding and wish you all the best moving forward πŸ’›\n\nThank you for your contribution,\n\nThe CheckMate Team",
"GRADUATION": "<b>πŸ“ Congratulations, You've Graduated! πŸŽ‰</b>\n\nHi {{name}},\n\nHuge congratulations on completing the CheckMate checkers' programme! You've worked hard, and completed the following:\n\nNo. of messages voted on: {{num_messages}}\nAccuracy: {{accuracy}}\nNo. of new users referred: {{num_referred}}\nNo. of messages reported: {{num_reported}}\n\n. Your dedication has clearly paid off! πŸŽ“\n\nWe'd love to hear about your experience - please take a moment to fill out <a href='{{survey_link}}'>our survey</a>\n\nTo generate your official completion certificate, just press the button below.\n\nThank you for being part of the Checkers' Crew. We're so proud of you! πŸ’ͺ",
"WELCOME": "<b>πŸ“ Welcome to the Checkers' Crew! πŸ‘‹</b>\n\nLet's welcome our new checkers!πŸŽ‰\n\n{{names}}\n\nWe're so excited to have you join our fact-checking family. This group is your hub for connecting with fellow Checkers, sharing tips, and staying up to date with important updates.\n\nIf you ever feel unsure about anything, don't hesitate to reach out or ask in our Q&A channel. We're all here to support each other and improve our skills together πŸ€—\n\nLet's get started!"
}
11 changes: 11 additions & 0 deletions functions/src/definitions/common/parameters/nudges.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"REACTIVATION": {
"1": "<b>πŸ“ We Miss You at CheckMate! πŸ₯Ί</b>\n\nHey {{name}},\n\nIt's been {{num_days}} days since we last saw you on the CheckMate bot, and we miss having you around! We'd love to have you back, so why not pop in when you have a moment? πŸ˜„\n\nSimply press the button below to resume your checking journey. If you need any help, don't hesitate to reach out!\n\nLooking forward to seeing you again πŸ€—"
},
"ACCURACY": {
"1": "<b>πŸ“ Let's Review Together!</b>\n\nHi {{name}},\n\nIt looks like you've had some challenges with your first {{num_messages}} messages, with over {{accuracy_threshold}} of them marked incorrect. Don't worry! This is all part of the learning process. 😊\n\nWe recommend revisiting the resources which you can assess by pressing the button below. If you're still unsure about categorization, feel free to ask any questions in the <a href='{{checkers_group_link}}'>Checker's Telegram group</a>. We're here to help you improve and succeed!\n\nKeep going, you've got this πŸ’ͺ"
},
"EXTENSION": {
"1": "<b>πŸ“ You've Got Another Chance! πŸŽ“</b>\n\nHi {{name}},\n\nAlthough you haven't met the graduation criteria just yet, we've extended your time in the programme by another month! You now have extra time to give it another go. 😊\n\nTo help you prepare, we've put together a <a href='{{revision_quiz_link}}'>revision quiz</a> for you to complete. Don't hesitate to ask questions in the group chat if you need any guidance.\n\nYou're almost there, let's finish strong! πŸ’ͺ"
}
}
6 changes: 5 additions & 1 deletion functions/src/definitions/common/parameters/thresholds.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,9 @@
"volunteerProgramVotesRequirement": 50,
"volunteerProgramReferralRequirement": 0,
"volunteerProgramReportRequirement": 3,
"volunteerProgramAccuracyRequirement": 0.6
"volunteerProgramAccuracyRequirement": 0.6,
"accuracyNudgeThreshold": 0.5,
"numberBeforeAccuracyNudge": 20,
"daysBeforeFirstCompletionCheck": 60,
"daysBeforeSecondCompletionCheck": 90
}
Loading

0 comments on commit 4e3ebef

Please sign in to comment.