From 15abc4d9165f4a214031675c094bf95796074c7e Mon Sep 17 00:00:00 2001 From: Bing Wen Tan Date: Sun, 19 Nov 2023 16:24:28 +0800 Subject: [PATCH 1/8] calling from cloud run --- functions/package-lock.json | 1 + functions/package.json | 1 + .../machineLearningServer/operations.ts | 33 +++++++++++++++++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index 826799f1..015f6831 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -14,6 +14,7 @@ "firebase": "^9.15.0", "firebase-admin": "^11.4.1", "firebase-functions": "^4.5.0", + "google-auth-library": "^9.2.0", "googleapis": "^112.0.0", "hashids": "^2.3.0", "image-hash": "^5.3.1", diff --git a/functions/package.json b/functions/package.json index 7fa348e2..423c4031 100644 --- a/functions/package.json +++ b/functions/package.json @@ -26,6 +26,7 @@ "firebase": "^9.15.0", "firebase-admin": "^11.4.1", "firebase-functions": "^4.5.0", + "google-auth-library": "^9.2.0", "googleapis": "^112.0.0", "hashids": "^2.3.0", "image-hash": "^5.3.1", diff --git a/functions/src/definitions/common/machineLearningServer/operations.ts b/functions/src/definitions/common/machineLearningServer/operations.ts index 88840c9c..9d67024d 100644 --- a/functions/src/definitions/common/machineLearningServer/operations.ts +++ b/functions/src/definitions/common/machineLearningServer/operations.ts @@ -1,8 +1,10 @@ import { defineString } from "firebase-functions/params" import axios, { AxiosError } from "axios" import * as functions from "firebase-functions" +import { GoogleAuth } from "google-auth-library" const embedderHost = defineString("EMBEDDER_HOST") +const env = process.env.ENVIRONMENT interface EmbedResponse { embedding: number[] @@ -93,15 +95,42 @@ async function performOCR(url: string): Promise { return camelCaseResponse } +async function getGoogleIdentityToken(audience: string) { + try { + const auth = new GoogleAuth() + const client = await auth.getIdTokenClient(audience) + const idToken = await client.idTokenProvider.fetchIdToken(audience) + return idToken + } catch (error) { + if (env !== "PROD") { + functions.logger.log( + "Unable to get Google identity token in nonprod environment" + ) + return "" + } else { + if (error instanceof AxiosError) { + functions.logger.error(error.message) + } else { + functions.logger.error(error) + } + throw new Error("Unable to get Google identity token in prod environment") + } + } +} + async function callAPI(endpoint: string, data: object) { try { + const hostName = embedderHost.value() + // Fetch identity token + const identityToken = await getGoogleIdentityToken(hostName) const response = await axios({ method: "POST", // Required, HTTP method, a string, e.g. POST, GET - url: `${embedderHost.value()}/${endpoint}`, + url: `${hostName}/${endpoint}`, data: data, headers: { "Content-Type": "application/json", - apikey: process.env.ML_SERVER_TOKEN ?? "", + Authorization: `Bearer ${identityToken}`, + //apikey: process.env.ML_SERVER_TOKEN ?? "", }, }) return response From 92613d2e856d4f01776d542d572bd1e9709089d8 Mon Sep 17 00:00:00 2001 From: Bing Wen Tan Date: Sun, 19 Nov 2023 17:25:49 +0800 Subject: [PATCH 2/8] test --- functions/src/definitions/common/pubsub.ts | 1 + functions/src/definitions/webhookHandlers/handler.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/functions/src/definitions/common/pubsub.ts b/functions/src/definitions/common/pubsub.ts index cf1e212f..37ae0648 100644 --- a/functions/src/definitions/common/pubsub.ts +++ b/functions/src/definitions/common/pubsub.ts @@ -18,6 +18,7 @@ async function publishToTopic( messageData: object, source: string ) { + functions.logger.log(`Publishing to topic ${topicName}`) if (env !== "PROD") { const [exists] = await pubsub.topic(topicName).exists() //Doesn't seem to autocreate in emulator if (!exists) { diff --git a/functions/src/definitions/webhookHandlers/handler.ts b/functions/src/definitions/webhookHandlers/handler.ts index 5062051b..cca0fe5d 100644 --- a/functions/src/definitions/webhookHandlers/handler.ts +++ b/functions/src/definitions/webhookHandlers/handler.ts @@ -48,6 +48,7 @@ const getHandlerWhatsapp = async (req: Request, res: Response) => { } const postHandlerWhatsapp = async (req: Request, res: Response) => { + functions.logger.log("postHandlerWhatsapp called") if (req.body.object) { if (req?.body?.entry?.[0]?.changes?.[0]?.value) { let value = req.body.entry[0].changes[0].value @@ -67,6 +68,7 @@ const postHandlerWhatsapp = async (req: Request, res: Response) => { (phoneNumberId === checkerPhoneNumberId && wabaID === checkerWabaId) || (phoneNumberId === userPhoneNumberId && wabaID === userWabaId) ) { + functions.logger.log(`message received from ${phoneNumberId}`) if (value?.messages?.[0]) { let message = value.messages[0] let type = message.type @@ -79,6 +81,7 @@ const postHandlerWhatsapp = async (req: Request, res: Response) => { await handleSpecialCommands(message) } else { if (message?.id) { + functions.logger.log("not special command") //if message has been processed before, don't even put it in queue. if (await checkMessageId(message.id)) { functions.logger.warn(`message ${message.id} already processed`) @@ -98,6 +101,7 @@ const postHandlerWhatsapp = async (req: Request, res: Response) => { await publishToTopic("checkerEvents", message, "whatsapp") } if (phoneNumberId === userPhoneNumberId) { + functions.logger.log("putting into user queue") //put into user queue await publishToTopic("userEvents", message, "whatsapp") } From 3a5d093d2889d4086c57683a0deb27f514fca865 Mon Sep 17 00:00:00 2001 From: Bing Wen Tan Date: Sun, 19 Nov 2023 18:24:03 +0800 Subject: [PATCH 3/8] removed comments and changed pubsub to nonUAT --- functions/src/definitions/common/pubsub.ts | 3 +-- functions/src/definitions/webhookHandlers/handler.ts | 4 ---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/functions/src/definitions/common/pubsub.ts b/functions/src/definitions/common/pubsub.ts index 37ae0648..d7d72f19 100644 --- a/functions/src/definitions/common/pubsub.ts +++ b/functions/src/definitions/common/pubsub.ts @@ -5,7 +5,7 @@ const env = process.env.ENVIRONMENT let pubsub: PubSub -if (env === "PROD") { +if (env === "PROD" || env === "UAT") { pubsub = new PubSub() } else { pubsub = new PubSub({ @@ -18,7 +18,6 @@ async function publishToTopic( messageData: object, source: string ) { - functions.logger.log(`Publishing to topic ${topicName}`) if (env !== "PROD") { const [exists] = await pubsub.topic(topicName).exists() //Doesn't seem to autocreate in emulator if (!exists) { diff --git a/functions/src/definitions/webhookHandlers/handler.ts b/functions/src/definitions/webhookHandlers/handler.ts index cca0fe5d..5062051b 100644 --- a/functions/src/definitions/webhookHandlers/handler.ts +++ b/functions/src/definitions/webhookHandlers/handler.ts @@ -48,7 +48,6 @@ const getHandlerWhatsapp = async (req: Request, res: Response) => { } const postHandlerWhatsapp = async (req: Request, res: Response) => { - functions.logger.log("postHandlerWhatsapp called") if (req.body.object) { if (req?.body?.entry?.[0]?.changes?.[0]?.value) { let value = req.body.entry[0].changes[0].value @@ -68,7 +67,6 @@ const postHandlerWhatsapp = async (req: Request, res: Response) => { (phoneNumberId === checkerPhoneNumberId && wabaID === checkerWabaId) || (phoneNumberId === userPhoneNumberId && wabaID === userWabaId) ) { - functions.logger.log(`message received from ${phoneNumberId}`) if (value?.messages?.[0]) { let message = value.messages[0] let type = message.type @@ -81,7 +79,6 @@ const postHandlerWhatsapp = async (req: Request, res: Response) => { await handleSpecialCommands(message) } else { if (message?.id) { - functions.logger.log("not special command") //if message has been processed before, don't even put it in queue. if (await checkMessageId(message.id)) { functions.logger.warn(`message ${message.id} already processed`) @@ -101,7 +98,6 @@ const postHandlerWhatsapp = async (req: Request, res: Response) => { await publishToTopic("checkerEvents", message, "whatsapp") } if (phoneNumberId === userPhoneNumberId) { - functions.logger.log("putting into user queue") //put into user queue await publishToTopic("userEvents", message, "whatsapp") } From 8833f962264cbafa3c87576b8dc3d0cb725be31d Mon Sep 17 00:00:00 2001 From: Bing Wen Tan Date: Sun, 19 Nov 2023 21:15:02 +0800 Subject: [PATCH 4/8] only not throw error in SIT and DEV --- .../definitions/common/machineLearningServer/operations.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/src/definitions/common/machineLearningServer/operations.ts b/functions/src/definitions/common/machineLearningServer/operations.ts index 9d67024d..5a6dbb78 100644 --- a/functions/src/definitions/common/machineLearningServer/operations.ts +++ b/functions/src/definitions/common/machineLearningServer/operations.ts @@ -102,9 +102,9 @@ async function getGoogleIdentityToken(audience: string) { const idToken = await client.idTokenProvider.fetchIdToken(audience) return idToken } catch (error) { - if (env !== "PROD") { + if (env === "SIT" || env === "DEV") { functions.logger.log( - "Unable to get Google identity token in nonprod environment" + "Unable to get Google identity token in lower environments" ) return "" } else { From aa07e056a7394dff1f07652ca2ac07b8da4ea7ff Mon Sep 17 00:00:00 2001 From: Bing Wen Tan Date: Sun, 19 Nov 2023 21:24:15 +0800 Subject: [PATCH 5/8] changed wording --- functions/src/definitions/common/parameters/userResponses.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/definitions/common/parameters/userResponses.json b/functions/src/definitions/common/parameters/userResponses.json index 578dff47..34088c7c 100644 --- a/functions/src/definitions/common/parameters/userResponses.json +++ b/functions/src/definitions/common/parameters/userResponses.json @@ -88,7 +88,7 @@ "cn": "目前,{%voted}}%的查哥查妹已经对这条短信进行投票评估。但是,他们无法达成一致,或缺乏足够的信息进行评估。\n\n如果您能提供更多信息,例如发信人电话号码或截图,将有助于我们更好地评估。" }, "INTERIM_PROMPT": { - "en": "Thanks for waiting! We are currently still pending the assessment from some of our network of trusted CheckMate volunteers and will only be able to provide a credible final result once enough votes have come in. \n\nYou may press the button below *to get an interim update of the preliminary result*. However, do note that there may be discrepancies between the preliminary and the final result, and *the preliminary result should be interpreted with caution*. We appreciate your patience and hope to deliver the final result to you soon! 💪🏼", + "en": "Thanks for waiting! Our network of trusted CheckMate volunteers is still assessing your message.\n\nYou may press the button below to see how our volunteers have rated your message so far. However, *do interpret this preliminary rating with caution* as the final result may differ. \n\nWe appreciate your patience and hope to deliver the final result soon!", "cn": "感谢您的耐心等待!查哥查妹正在对您提交的短信进行投票评估。我们将在足够多的查哥查妹投票后,为您提供最终结果。\n\n在最终结果发布之前,您可以点击“获取初步结果”。请注意,初步结果和最终结果可能存在差异,应谨慎解读初步结果。我们感谢您的耐心,并会尽快提供最终结果!💪🏼" }, "ALREADY_REPLIED": { From 131f503e62b7fa98762542993ba501ef6d23c3d3 Mon Sep 17 00:00:00 2001 From: Bing Wen Tan Date: Sun, 19 Nov 2023 21:26:44 +0800 Subject: [PATCH 6/8] changed spaces --- functions/src/definitions/common/parameters/userResponses.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/definitions/common/parameters/userResponses.json b/functions/src/definitions/common/parameters/userResponses.json index 34088c7c..7526e3bb 100644 --- a/functions/src/definitions/common/parameters/userResponses.json +++ b/functions/src/definitions/common/parameters/userResponses.json @@ -88,7 +88,7 @@ "cn": "目前,{%voted}}%的查哥查妹已经对这条短信进行投票评估。但是,他们无法达成一致,或缺乏足够的信息进行评估。\n\n如果您能提供更多信息,例如发信人电话号码或截图,将有助于我们更好地评估。" }, "INTERIM_PROMPT": { - "en": "Thanks for waiting! Our network of trusted CheckMate volunteers is still assessing your message.\n\nYou may press the button below to see how our volunteers have rated your message so far. However, *do interpret this preliminary rating with caution* as the final result may differ. \n\nWe appreciate your patience and hope to deliver the final result soon!", + "en": "Thanks for waiting! Our network of trusted CheckMate volunteers is still assessing your message.\n\nYou may press the button below to see how our volunteers have rated your message so far. However, *do interpret this preliminary rating with caution* as the final result may differ. \n\nWe appreciate your patience and hope to deliver the final result soon!", "cn": "感谢您的耐心等待!查哥查妹正在对您提交的短信进行投票评估。我们将在足够多的查哥查妹投票后,为您提供最终结果。\n\n在最终结果发布之前,您可以点击“获取初步结果”。请注意,初步结果和最终结果可能存在差异,应谨慎解读初步结果。我们感谢您的耐心,并会尽快提供最终结果!💪🏼" }, "ALREADY_REPLIED": { From f6e812166dcfc0ee471273bd5196e8ea8836cb26 Mon Sep 17 00:00:00 2001 From: Bing Wen Tan Date: Sun, 26 Nov 2023 19:24:43 +0800 Subject: [PATCH 7/8] added message blast capability --- documentation/dataSchema.md | 17 + .../common/parameters/userResponses.json | 42 +- .../src/definitions/common/responseUtils.ts | 122 ++- .../definitions/eventHandlers/userHandlers.ts | 31 +- .../webhookHandlers/specialCommands.ts | 28 + .../checkmate.postman_collection.json | 928 ++++++++++++++---- integration-tests/env.json | 134 ++- 7 files changed, 1090 insertions(+), 212 deletions(-) diff --git a/documentation/dataSchema.md b/documentation/dataSchema.md index b631112c..fd6e2c0e 100644 --- a/documentation/dataSchema.md +++ b/documentation/dataSchema.md @@ -115,8 +115,25 @@ erDiagram number referralCount map utm "map containing utm parameters, source, medium, content, campaign, term" string language "en or cn, users preferred language" + boolean isSubscribedUpdates "whether to blast msgs to this user" } + blasts { + string type "image or text" + string text "text or caption (if image)" + string storageUrl "image storage url if applicable" + boolean isActive "the one that should be sent" + timestamp createdDate "is active" + timestamp blastDate "" + collection recipients + } + + recipients { + string id PK "phone number" + string feebackCategory "positive, negative or neutral" + timestamp sentTimestamp "when blast was sent to user" + } + message ||--|{ instance: has user ||--|{ instance: sends message ||--o{ voteRequest: triggers diff --git a/functions/src/definitions/common/parameters/userResponses.json b/functions/src/definitions/common/parameters/userResponses.json index 7526e3bb..3a64be88 100644 --- a/functions/src/definitions/common/parameters/userResponses.json +++ b/functions/src/definitions/common/parameters/userResponses.json @@ -151,6 +151,14 @@ "en": "Here's our contact! Do add us to your contact list so you can find us in future. 😊", "cn": "这是我们的联系方式!请将我们添加到您的联系人列表中,以便您以后能找到我们。😊" }, + "UNSUBSCRIBE": { + "en": "Sorry to see you go.😞 If you change your mind, you can always come back to the menu to subscribe again!", + "cn": "很遗憾看到您离开。😞 如果您改变主意,您可以随时回到菜单重新订阅!" + }, + "SUBSCRIBE": { + "en": "Thanks for subscribing!🙏🏻 CheckMate will now send you content occasionally. Hope you find it useful!", + "cn": "感谢您的订阅!查友将偶尔向您发送内容。希望您觉得它有用!" + }, "REFERRAL": { "en": "Have you started checking and reporting suspicious messages using CheckMate yet? Sign up by clicking this link and sending in the pre-loaded message!! {{link}}", "cn": "您是否已经开始使用查友?点击此链接进行注册!{{link}}" @@ -183,7 +191,7 @@ "en": "*This is an experimental feature powered by generative AI*. Do let us know if it was useful below!\n \n{{rationalisation}}", "cn": "这是一项由AI技术提供的实验性功能。请在下方告诉我们它是否对您有所帮助!\n\n{{rationalisation}}" }, - "RATIONALISATION_USEFUL": { + "FEEDBACK_THANKS": { "en": "Thanks for your valuable feedback!", "cn": "感谢您的反馈!" }, @@ -191,6 +199,10 @@ "en": "Sorry to hear that, but thanks anyway for your valuable feedback!", "cn": "很遗憾此功能对您来说不是很有用。 查友已将您的反馈传递给我们的产品团队,以便进一步增强此功能。感谢您的反馈!" }, + "BLAST_FEEDBACK": { + "en": "Did you find this content useful?\n\nIf you wish to unsubscribe, type \"menu\" and select the \"Unsubscribe\" option", + "cn": "是否认为这份内容对您有帮助?\n\n如果您希望取消订阅,请输入“菜单”并选择“取消订阅”选项。" + }, "MENU_BUTTON": { "en": "Menu", "cn": "菜单" @@ -259,6 +271,22 @@ "en": "Select Language / 选择语言", "cn": "Select Language / 选择语言" }, + "MENU_TITLE_UNSUB": { + "en": "Unsubscribe", + "cn": "取消订阅" + }, + "MENU_DESCRIPTION_UNSUB": { + "en": "Unsubscribe from receiving content from CheckMate", + "cn": "取消订阅查友不时内容" + }, + "MENU_TITLE_SUB": { + "en": "Subscribe", + "cn": "订阅" + }, + "MENU_DESCRIPTION_SUB": { + "en": "Subscribe to occasional content from CheckMate", + "cn": "订阅查友不时内容" + }, "MENU_DESCRIPTION_NPS_LIKELY": { "en": "Extremely likely 🤩", "cn": "一定会🤩" @@ -307,6 +335,18 @@ "en": "How'd we tell?", "cn": "我们是如何判断的?" }, + "BUTTON_SHIOK": { + "en": "😍", + "cn": "😍" + }, + "BUTTON_MEH": { + "en": "😐", + "cn": "😐" + }, + "BUTTON_BOO": { + "en": "😞", + "cn": "😞" + }, "PLACEHOLDER_SCAM": { "en": "a scam🚫", "cn": "是诈骗信息🚫" diff --git a/functions/src/definitions/common/responseUtils.ts b/functions/src/definitions/common/responseUtils.ts index 886fe861..2d95a070 100644 --- a/functions/src/definitions/common/responseUtils.ts +++ b/functions/src/definitions/common/responseUtils.ts @@ -4,11 +4,13 @@ import USER_BOT_RESPONSES from "./parameters/userResponses.json" import CHECKER_BOT_RESPONSES from "./parameters/checkerResponses.json" import { sendWhatsappButtonMessage, + sendWhatsappImageMessage, sendWhatsappTextListMessage, sendWhatsappTextMessage, } from "./sendWhatsappMessage" import { DocumentSnapshot, Timestamp } from "firebase-admin/firestore" import { getThresholds, sleep } from "./utils" +import { getSignedUrl } from "./mediaUtils" import { sendTextMessage } from "./sendMessage" import { getCount } from "./counters" @@ -92,7 +94,7 @@ async function respondToRationalisationFeedback( let response switch (isUseful) { case "yes": - response = responses?.RATIONALISATION_USEFUL + response = responses?.FEEDBACK_THANKS await instanceRef.update({ isRationalisationUseful: true }) break default: @@ -104,6 +106,19 @@ async function respondToRationalisationFeedback( await sendWhatsappTextMessage("user", from, response) } +async function respondToBlastFeedback( + blastPath: string, + feedbackCategory: string, + from: string +) { + const blastFeedbackRef = db.doc(blastPath).collection("recipients").doc(from) + const responses = await getResponsesObj("user", from) + blastFeedbackRef.update({ + feebackCategory: feedbackCategory, + }) + await sendWhatsappTextMessage("user", from, responses.FEEDBACK_THANKS) +} + async function sendMenuMessage( to: string, prefixName: string, @@ -111,6 +126,8 @@ async function sendMenuMessage( replyMessageId: string | null = null, disputedInstancePath: string | null = null ) { + const userSnap = await db.collection("users").doc(to).get() + const isSubscribedUpdates = userSnap.get("isSubscribedUpdates") ?? false const responses = await getResponsesObj("user", to) if (!(prefixName in responses)) { functions.logger.error(`prefixName ${prefixName} not found in responses`) @@ -162,6 +179,17 @@ async function sendMenuMessage( title: responses.MENU_TITLE_CONTACT, description: responses.MENU_DESCRIPTION_CONTACT, }, + { + id: isSubscribedUpdates + ? `${type}_unsubscribeUpdates` + : `${type}_subscribeUpdates`, + title: isSubscribedUpdates + ? responses.MENU_TITLE_UNSUB + : responses.MENU_TITLE_SUB, + description: isSubscribedUpdates + ? responses.MENU_DESCRIPTION_UNSUB + : responses.MENU_DESCRIPTION_SUB, + }, //TODO: Implement these next time // { @@ -845,6 +873,96 @@ async function sendLanguageSelection(user: string, newUser: boolean) { await sendWhatsappButtonMessage("user", user, response, buttons) } +async function sendBlast(user: string) { + const blastQuerySnap = await db + .collection("blasts") + .where("isActive", "==", true) + .orderBy("createdDate", "desc") // Order by createdDate in descending order + .limit(1) // Limit to 1 document + .get() + const responses = await getResponsesObj("user", user) + if (blastQuerySnap.empty) { + functions.logger.warn( + `No active blast found when attempting to send blast to user ${user}` + ) + await sendTextMessage("user", user, responses.GENERIC_ERROR) + return + } + const blastSnap = blastQuerySnap.docs[0] + const blastData = blastSnap.data() + switch (blastData.type) { + case "image": + if (!blastData.storageUrl) { + functions.logger.error( + `No image url found for blast ${blastSnap.ref.path}` + ) + await sendTextMessage("user", user, responses.GENERIC_ERROR) + return + } else { + //send image to user + const signedUrl = await getSignedUrl(blastData.storageUrl) + await sendWhatsappImageMessage( + "user", + user, + null, + signedUrl, + blastData.text ?? null, + null + ) + } + break + case "text": + if (!blastData.text) { + functions.logger.error(`No text found for blast ${blastSnap.ref.path}`) + await sendTextMessage("user", user, responses.GENERIC_ERROR) + return + } else { + //send text to user + await sendTextMessage("user", user, blastData.text) + } + break + } + const buttons = [ + { + type: "reply", + reply: { + id: `feedbackBlast_${blastSnap.ref.path}_negative`, + title: responses.BUTTON_BOO, + }, + }, + { + type: "reply", + reply: { + id: `feedbackBlast_${blastSnap.ref.path}_neutral`, + title: responses.BUTTON_MEH, + }, + }, + { + type: "reply", + reply: { + id: `feedbackBlast_${blastSnap.ref.path}_positive`, + title: responses.BUTTON_SHIOK, + }, + }, + ] + await blastSnap.ref + .collection("recipients") + .doc(user) + .set( + { + feebackCategory: null, + sentTimestamp: Timestamp.fromDate(new Date()), + }, + { merge: true } + ) + await sendWhatsappButtonMessage( + "user", + user, + responses.BLAST_FEEDBACK, + buttons + ) +} + export { getResponsesObj, respondToInstance, @@ -857,4 +975,6 @@ export { respondToRationalisationFeedback, updateLanguageAndSendMenu, sendLanguageSelection, + sendBlast, + respondToBlastFeedback, } diff --git a/functions/src/definitions/eventHandlers/userHandlers.ts b/functions/src/definitions/eventHandlers/userHandlers.ts index 69a5cede..2fff640a 100644 --- a/functions/src/definitions/eventHandlers/userHandlers.ts +++ b/functions/src/definitions/eventHandlers/userHandlers.ts @@ -24,6 +24,8 @@ import { respondToRationalisationFeedback, updateLanguageAndSendMenu, sendLanguageSelection, + sendBlast, + respondToBlastFeedback, } from "../common/responseUtils" import { downloadWhatsappMedia, @@ -38,7 +40,6 @@ import { classifyText } from "../common/classifier" import { FieldValue } from "@google-cloud/firestore" import Hashids from "hashids" import { Message } from "../../types" -import { user } from "firebase-functions/v1/auth" const runtimeEnvironment = defineString("ENVIRONMENT") const similarityThreshold = defineString("SIMILARITY_THRESHOLD") @@ -217,10 +218,10 @@ const userHandlerWhatsapp = async function (message: Message) { const button = message.button switch (button.text) { case "Get Latest Update": - //TODO + await sendBlast(from) break case "Unsubscribe": - //TODO + await toggleUserSubscription(from, false) break case "Get Referral Message": await sendReferralMessage(from) @@ -725,10 +726,7 @@ async function onButtonReply(messageObj: Message, platform = "whatsapp") { const from = messageObj.from const responses = await getResponsesObj("user", from) const [type, ...rest] = buttonId.split("_") - let instancePath, - selection, - instanceRef, - updateObj: { scamShieldConsent?: boolean } + let instancePath, selection, instanceRef, blastPath switch (type) { case "scamshieldDecline": ;[instancePath] = rest @@ -763,6 +761,10 @@ async function onButtonReply(messageObj: Message, platform = "whatsapp") { ;[instancePath, selection] = rest await respondToRationalisationFeedback(instancePath, selection) break + case "feedbackBlast": + ;[blastPath, selection] = rest + await respondToBlastFeedback(blastPath, selection, from) + break case "languageSelection": ;[selection] = rest await updateLanguageAndSendMenu(from, selection) @@ -846,6 +848,14 @@ async function onTextListReceipt(messageObj: Message, platform = "whatsapp") { ) response = responses.DISPUTE break + case "unsubscribeUpdates": + await toggleUserSubscription(from, false) + response = responses.UNSUBSCRIBE + break + case "subscribeUpdates": + await toggleUserSubscription(from, true) + response = responses.SUBSCRIBE + break } break case "satisfactionSurvey": @@ -903,6 +913,12 @@ function checkMenu(text: string) { return menuKeywords.includes(text.toLowerCase()) } +async function toggleUserSubscription(userId: string, toSubscribe: boolean) { + db.collection("users").doc(userId).update({ + isSubscribedUpdates: toSubscribe, + }) +} + async function createNewUser( userRef: admin.firestore.DocumentReference, messageTimestamp: Timestamp @@ -919,6 +935,7 @@ async function createNewUser( referralId: referralId, referralCount: 0, language: "en", + isSubscribedUpdates: true, }) } diff --git a/functions/src/definitions/webhookHandlers/specialCommands.ts b/functions/src/definitions/webhookHandlers/specialCommands.ts index 43897e4b..e7582532 100644 --- a/functions/src/definitions/webhookHandlers/specialCommands.ts +++ b/functions/src/definitions/webhookHandlers/specialCommands.ts @@ -7,6 +7,8 @@ import USER_BOT_RESPONSES from "../common/parameters/userResponses.json" import CHECKER_BOT_RESPONSES from "../common/parameters/checkerResponses.json" import thresholds from "../common/parameters/thresholds.json" import { interimPromptHandler } from "../batchJobs/batchJobs" +import { sendBlast } from "../common/responseUtils" +import { Timestamp } from "firebase-admin/firestore" const runtimeEnvironment = defineString("ENVIRONMENT") const checker1PhoneNumber = defineString("CHECKER1_PHONE_NUMBER") @@ -31,6 +33,10 @@ const handleSpecialCommands = async function (messageObj: WhatsappMessage) { return case "/interim": await interimPromptHandler() + return + case "/blast": + await sendBlast(messageObj.from) + return } } } @@ -86,6 +92,28 @@ const mockDb = async function () { }, { merge: true } ) + /* + string type "image or text" + string text "text or caption (if image)" + string storageUrl "image storage url if applicable" + boolean isActive "the one that should be sent" + timestamp createdDate "is active" + timestamp blastDate "" + */ + await db + .collection("blasts") + .doc() + .set( + { + type: "text", + text: "This is a test blast", + storageUrl: null, + isActive: true, + createdDate: Timestamp.fromDate(new Date()), + blastDate: Timestamp.fromDate(new Date()), + }, + { merge: true } + ) } functions.logger.log("mocked") } diff --git a/integration-tests/checkmate.postman_collection.json b/integration-tests/checkmate.postman_collection.json index e89c4e58..826a4800 100644 --- a/integration-tests/checkmate.postman_collection.json +++ b/integration-tests/checkmate.postman_collection.json @@ -120,8 +120,7 @@ "exec": [ "pm.test(\"Check number of collections /mockdb command creates in Firestore\", function () {\r", " const jsonData = pm.response.json();\r", - " \r", - " pm.expect(jsonData?.collectionIds?.length).to.eql(2);\r", + " pm.expect(jsonData?.collectionIds?.length).to.eql(3);\r", "});" ], "type": "text/javascript" @@ -1148,6 +1147,8 @@ " const MENU_DESCRIPTION_LANGUAGE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_LANGUAGE.en\")\r", " const MENU_TITLE_CONTACT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_CONTACT.en\");\r", " const MENU_DESCRIPTION_CONTACT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_CONTACT.en\");\r", + " const MENU_TITLE_UNSUB = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_UNSUB.en\")\r", + " const MENU_DESCRIPTION_UNSUB = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_UNSUB.en\")\r", " const MENU_TITLE_DISPUTE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_DISPUTE.en\")\r", " const MENU_DESCRIPTION_DISPUTE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_DISPUTE.en\")\r", " \r", @@ -1205,7 +1206,12 @@ " \"id\": \"menu_contact\",\r", " \"title\": MENU_TITLE_CONTACT,\r", " \"description\": MENU_DESCRIPTION_CONTACT\r", - " }\r", + " },\r", + " {\r", + " \"id\": \"menu_unsubscribeUpdates\",\r", + " \"title\": MENU_TITLE_UNSUB,\r", + " \"description\": MENU_DESCRIPTION_UNSUB\r", + " },\r", " ]\r", " }\r", " ]\r", @@ -1387,6 +1393,8 @@ " const MENU_DESCRIPTION_LANGUAGE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_LANGUAGE.en\")\r", " const MENU_TITLE_CONTACT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_CONTACT.en\");\r", " const MENU_DESCRIPTION_CONTACT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_CONTACT.en\");\r", + " const MENU_TITLE_UNSUB = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_UNSUB.en\")\r", + " const MENU_DESCRIPTION_UNSUB = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_UNSUB.en\")\r", " const MENU_TITLE_DISPUTE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_DISPUTE.en\")\r", " const MENU_DESCRIPTION_DISPUTE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_DISPUTE.en\")\r", " const wamid = pm.variables.get(\"whatsapp_id_9\")\r", @@ -1450,6 +1458,11 @@ " \"id\": \"menu_contact\",\r", " \"title\": MENU_TITLE_CONTACT,\r", " \"description\": MENU_DESCRIPTION_CONTACT\r", + " },\r", + " {\r", + " \"id\": \"menu_unsubscribeUpdates\",\r", + " \"title\": MENU_TITLE_UNSUB,\r", + " \"description\": MENU_DESCRIPTION_UNSUB\r", " }\r", " ]\r", " }\r", @@ -1491,7 +1504,7 @@ "script": { "exec": [ "// Allow time for firestore onUpdate event to complete\r", - "setTimeout(() => {}, 6000);" + "setTimeout(() => {}, 8000);" ], "type": "text/javascript" } @@ -10482,7 +10495,7 @@ "listen": "test", "script": { "exec": [ - "pm.test(\"Your test name\", function () {\r", + "pm.test(\"Check that generic menu is sent\", function () {\r", " const MENU = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU.en\");\r", " const PREFIX = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_PREFIX.en\");\r", " const MENU_TEXT = MENU.replace(\"{{prefix}}\",PREFIX);\r", @@ -10501,6 +10514,8 @@ " const MENU_DESCRIPTION_LANGUAGE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_LANGUAGE.en\")\r", " const MENU_TITLE_CONTACT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_CONTACT.en\");\r", " const MENU_DESCRIPTION_CONTACT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_CONTACT.en\");\r", + " const MENU_TITLE_UNSUB = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_UNSUB.en\")\r", + " const MENU_DESCRIPTION_UNSUB = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_UNSUB.en\")\r", " const MENU_TITLE_DISPUTE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_DISPUTE.en\")\r", " const MENU_DESCRIPTION_DISPUTE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_DISPUTE.en\")\r", " \r", @@ -10558,6 +10573,11 @@ " \"id\": \"menu_contact\",\r", " \"title\": MENU_TITLE_CONTACT,\r", " \"description\": MENU_DESCRIPTION_CONTACT\r", + " },\r", + " {\r", + " \"id\": \"menu_unsubscribeUpdates\",\r", + " \"title\": MENU_TITLE_UNSUB,\r", + " \"description\": MENU_DESCRIPTION_UNSUB\r", " }\r", " ]\r", " }\r", @@ -11294,6 +11314,8 @@ " const MENU_DESCRIPTION_LANGUAGE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_LANGUAGE.en\")\r", " const MENU_TITLE_CONTACT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_CONTACT.en\");\r", " const MENU_DESCRIPTION_CONTACT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_CONTACT.en\");\r", + " const MENU_TITLE_UNSUB = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_UNSUB.en\")\r", + " const MENU_DESCRIPTION_UNSUB = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_UNSUB.en\")\r", " const MENU_TITLE_DISPUTE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_DISPUTE.en\")\r", " const MENU_DESCRIPTION_DISPUTE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_DISPUTE.en\")\r", " \r", @@ -11351,6 +11373,11 @@ " \"id\": \"menu_contact\",\r", " \"title\": MENU_TITLE_CONTACT,\r", " \"description\": MENU_DESCRIPTION_CONTACT\r", + " },\r", + " {\r", + " \"id\": \"menu_unsubscribeUpdates\",\r", + " \"title\": MENU_TITLE_UNSUB,\r", + " \"description\": MENU_DESCRIPTION_UNSUB\r", " }\r", " ]\r", " }\r", @@ -11690,14 +11717,9 @@ } }, "response": [] - } - ] - }, - { - "name": "009_Scam_Autocategorise", - "item": [ + }, { - "name": "User sends scam message", + "name": "Make Menu Selection (\"Unsubscribe\")", "event": [ { "listen": "test", @@ -11715,7 +11737,7 @@ "script": { "exec": [ "// 1. Retrieve the current messageCounter and increment it.\r", - "let messageCounter = 37\r", + "let messageCounter = 36\r", "\r", "const whatsappId = \"wamid.HBgKNjU5Njg4MDMyMBUCABIYIEY4MDAwNTlEODQyMDZDMkNDOEU1NEVEQjc1MTNCMjlFA1==\";\r", "\r", @@ -11736,7 +11758,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"object\": \"whatsapp_business_account\",\r\n \"entry\": [\r\n {\r\n \"id\": \"WHATSAPP_TEST_USER_WABA_ID\",\r\n \"changes\": [\r\n {\r\n \"value\": {\r\n \"messaging_product\": \"whatsapp\",\r\n \"metadata\": {\r\n \"display_phone_number\": \"15550933685\",\r\n \"phone_number_id\": \"WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID\"\r\n },\r\n \"contacts\": [\r\n { \"profile\": { \"name\": \"{{USER_1_NAME}}\" }, \"wa_id\": \"{{USER_1_NUMBER}}\" }\r\n ],\r\n \"messages\": [\r\n {\r\n \"from\": \"{{USER_1_NUMBER}}\",\r\n \"id\": \"{{whatsapp_id_37}}\",\r\n \"timestamp\": {{$timestamp}},\r\n \"text\": { \"body\": \"This is a scam message\" },\r\n \"type\": \"text\"\r\n }\r\n ]\r\n },\r\n \"field\": \"messages\"\r\n }\r\n ]\r\n }\r\n ]\r\n}\r\n", + "raw": "{\r\n \"object\": \"whatsapp_business_account\",\r\n \"entry\": [\r\n {\r\n \"id\": \"WHATSAPP_TEST_USER_WABA_ID\",\r\n \"changes\": [\r\n {\r\n \"value\": {\r\n \"messaging_product\": \"whatsapp\",\r\n \"metadata\": {\r\n \"display_phone_number\": \"15550933685\",\r\n \"phone_number_id\": \"WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID\"\r\n },\r\n \"contacts\": [\r\n {\r\n \"profile\": {\r\n \"name\": \"{{USER_1_NAME}}\"\r\n },\r\n \"wa_id\": \"{{USER_1_NUMBER}}\"\r\n }\r\n ],\r\n \"messages\": [\r\n {\r\n \"context\": {\r\n \"from\": \"15550933685\",\r\n \"id\": \"wamid.HBgKNjU5Njg4MDMyMBUCABIYIEY4MDAwNTlEODQyMDZDMkNDOEU1NEVEQjc1MTNCMjlFAA==\"\r\n },\r\n \"from\": \"{{USER_1_NUMBER}}\",\r\n \"id\": \"{{whatsapp_id_36}}\",\r\n \"timestamp\": {{$timestamp}},\r\n \"type\": \"interactive\",\r\n \"interactive\": {\r\n \"type\": \"list_reply\",\r\n \"list_reply\": {\r\n \"id\": \"menu_unsubscribeUpdates\",\r\n \"title\": \"{{__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_UNSUB.en}}\",\r\n \"description\": \"{{__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_UNSUB.en}}\"\r\n }\r\n }\r\n }\r\n ]\r\n },\r\n \"field\": \"messages\"\r\n }\r\n ]\r\n }\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -11759,79 +11781,29 @@ "response": [] }, { - "name": "Get last message (SCAM, IMMEDIATE, NOT MATCHED, AUTO)", + "name": "Get last message (Unsubscribe)", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test(\"scam immediate autocategorised no matched response\", function () {\r", - " const TEMPLATE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.SCAM.en\");\r", - " const THANKS_IMMEDIATE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.THANKS_IMMEDIATE.en\")\r", - " const THANKS_DELAYED = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.THANKS_DELAYED.en\")\r", - " const METHODOLOGY_HUMAN = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.METHODOLOGY_HUMAN.en\")\r", - " const METHODOLOGY_AUTO = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.METHODOLOGY_AUTO.en\")\r", - " const VOTE_RESULTS_SUFFIX = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.VOTE_RESULTS_SUFFIX.en\")\r", - " const BUTTON_RESULTS = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_RESULTS.en\")\r", - " const BUTTON_DECLINE_REPORT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_DECLINE_REPORT.en\")\r", - " const BUTTON_RATIONALISATION = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_RATIONALISATION.en\")\r", - " const wamid = pm.variables.get(\"whatsapp_id_37\")\r", - " const BODY_TEXT = TEMPLATE.replace(\"{{thanks}}\",THANKS_IMMEDIATE).replace(\"{{methodology}}\",METHODOLOGY_AUTO).replace(\"{{matched}}\",\"\").replace(\"{{results}}\",\"\").replace(\"{{image_caveat}}\",\"\")\r", - " const USER_1_NAME = pm.variables.get(\"USER_1_NAME\")\r", + "pm.test(\"Your test name\", function () {\r", + " const BODY_TEXT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.UNSUBSCRIBE.en\");\r", " const USER_1_NUMBER = pm.variables.get(\"USER_1_NUMBER\")\r", " const expected = {\r", - " \"hostname\": \"resultserver\",\r", - " \"path\": \"/v15.0/WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID/messages\",\r", - " \"body\": {\r", - " \"messaging_product\": \"whatsapp\",\r", - " \"recipient_type\": \"individual\",\r", - " \"to\": USER_1_NUMBER,\r", - " \"type\": \"interactive\",\r", - " \"interactive\": {\r", - " \"type\": \"button\",\r", - " \"body\": {\r", - " \"text\": BODY_TEXT\r", - " },\r", - " \"action\": {\r", - " \"buttons\": [\r", - " {\r", - " \"type\": \"reply\",\r", - " \"reply\": {\r", - " \"id\": \"ID\",\r", - " \"title\": BUTTON_RATIONALISATION\r", - " }\r", - " },\r", - " {\r", - " \"type\": \"reply\",\r", - " \"reply\": {\r", - " \"id\": \"ID\",\r", - " \"title\": BUTTON_DECLINE_REPORT\r", - " }\r", - " }\r", - " ]\r", - " }\r", - " },\r", - " \"context\": {\r", - " \"message_id\": wamid\r", - " }\r", + " \"hostname\": \"resultserver\",\r", + " \"path\": \"/v15.0/WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID/messages\",\r", + " \"body\": {\r", + " \"text\": {\r", + " \"body\": BODY_TEXT,\r", + " \"preview_url\": true\r", " },\r", - " \"method\": \"POST\"\r", - " }\r", - "\r", + " \"to\": USER_1_NUMBER,\r", + " \"messaging_product\": \"whatsapp\",\r", + " },\r", + " \"method\": \"POST\"\r", + "}\r", " var jsonData = pm.response.json();\r", - "\r", - " let reply_id_rationalisation = jsonData.body.interactive.action.buttons[0].reply.id;\r", - " let reply_id_decline = jsonData.body.interactive.action.buttons[1].reply.id;\r", - " let [, scamInstancePath , ] = reply_id_rationalisation.split(\"_\")\r", - " \r", - " // check if the id matches the pattern \"menu_dispute_messages/*/instances/*\"\r", - " pm.expect(reply_id_rationalisation).to.match(/rationalisation_messages\\/\\w+\\/instances\\/\\w+/);\r", - " pm.expect(reply_id_decline).to.match(/scamshieldDecline_messages\\/\\w+\\/instances\\/\\w+/);\r", - " pm.collectionVariables.set(\"scamInstancePath\", scamInstancePath);\r", - "\r", - " jsonData.body.interactive.action.buttons[0].reply.id = \"ID\"\r", - " jsonData.body.interactive.action.buttons[1].reply.id = \"ID\"\r", - "\r", " pm.expect(jsonData).to.eql(expected);\r", "});" ], @@ -11842,7 +11814,7 @@ "listen": "prerequest", "script": { "exec": [ - "setTimeout(() => {}, 3000);" + "setTimeout(() => {}, 2000);" ], "type": "text/javascript" } @@ -11862,14 +11834,55 @@ } }, "response": [] - } - ] - }, - { - "name": "010_Spam_Autocategorise", - "item": [ + }, { - "name": "User sends spam message", + "name": "[DB CALL] Check isSubscribedUpdates", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Check that has isSubscribedUpdates is false\", function () {\r", + "\r", + " var jsonData = pm.response.json();\r", + " const isSubscribedUpdates = jsonData.fields.isSubscribedUpdates.booleanValue\r", + " pm.expect(isSubscribedUpdates).to.be.false;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer owner", + "type": "text" + } + ], + "url": { + "raw": "{{FIRESTORE_URL}}/v1/projects/{{PROJECT_ID}}/databases/(default)/documents/users/{{USER_1_NUMBER}}", + "host": [ + "{{FIRESTORE_URL}}" + ], + "path": [ + "v1", + "projects", + "{{PROJECT_ID}}", + "databases", + "(default)", + "documents", + "users", + "{{USER_1_NUMBER}}" + ] + } + }, + "response": [] + }, + { + "name": "Send WhatApp message (\"Menu\")", "event": [ { "listen": "test", @@ -11887,7 +11900,7 @@ "script": { "exec": [ "// 1. Retrieve the current messageCounter and increment it.\r", - "let messageCounter = 38\r", + "let messageCounter = 30\r", "\r", "const whatsappId = \"wamid.HBgKNjU5Njg4MDMyMBUCABIYIEY4MDAwNTlEODQyMDZDMkNDOEU1NEVEQjc1MTNCMjlFA1==\";\r", "\r", @@ -11908,7 +11921,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"object\": \"whatsapp_business_account\",\r\n \"entry\": [\r\n {\r\n \"id\": \"WHATSAPP_TEST_USER_WABA_ID\",\r\n \"changes\": [\r\n {\r\n \"value\": {\r\n \"messaging_product\": \"whatsapp\",\r\n \"metadata\": {\r\n \"display_phone_number\": \"15550933685\",\r\n \"phone_number_id\": \"WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID\"\r\n },\r\n \"contacts\": [\r\n { \"profile\": { \"name\": \"{{USER_1_NAME}}\" }, \"wa_id\": \"{{USER_1_NUMBER}}\" }\r\n ],\r\n \"messages\": [\r\n {\r\n \"from\": \"{{USER_1_NUMBER}}\",\r\n \"id\": \"{{whatsapp_id_38}}\",\r\n \"timestamp\": {{$timestamp}},\r\n \"text\": { \"body\": \"This is a spam message\" },\r\n \"type\": \"text\"\r\n }\r\n ]\r\n },\r\n \"field\": \"messages\"\r\n }\r\n ]\r\n }\r\n ]\r\n}\r\n", + "raw": "{\r\n \"object\": \"whatsapp_business_account\",\r\n \"entry\": [\r\n {\r\n \"id\": \"WHATSAPP_TEST_USER_WABA_ID\",\r\n \"changes\": [\r\n {\r\n \"value\": {\r\n \"messaging_product\": \"whatsapp\",\r\n \"metadata\": {\r\n \"display_phone_number\": \"15550933685\",\r\n \"phone_number_id\": \"WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID\"\r\n },\r\n \"contacts\": [\r\n {\r\n \"profile\": {\r\n \"name\": \"{{USER_1_NAME}}\"\r\n },\r\n \"wa_id\": \"{{USER_1_NUMBER}}\"\r\n }\r\n ],\r\n \"messages\": [\r\n {\r\n \"from\": \"{{USER_1_NUMBER}}\",\r\n \"id\": \"{{whatsapp_id_30}}\",\r\n \"timestamp\": {{$timestamp}},\r\n \"text\": {\r\n \"body\": \"Menu\"\r\n },\r\n \"type\": \"text\"\r\n }\r\n ]\r\n },\r\n \"field\": \"messages\"\r\n }\r\n ]\r\n }\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -11931,109 +11944,642 @@ "response": [] }, { - "name": "Get last message (SPAM, IMMEDIATE, NOT MATCHED, AUTO)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"spam immediate autocategorised no matched response\", function () {\r", - " const TEMPLATE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.SPAM.en\");\r", - " const THANKS_IMMEDIATE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.THANKS_IMMEDIATE.en\")\r", - " const METHODOLOGY_AUTO = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.METHODOLOGY_AUTO.en\")\r", - " const BUTTON_RESULTS = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_RESULTS.en\")\r", - " const BUTTON_DECLINE_REPORT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_DECLINE_REPORT.en\")\r", - " const BUTTON_RATIONALISATION = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_RATIONALISATION.en\")\r", - " const BODY_TEXT = TEMPLATE.replace(\"{{thanks}}\",THANKS_IMMEDIATE).replace(\"{{methodology}}\",METHODOLOGY_AUTO).replace(\"{{matched}}\",\"\").replace(\"{{image_caveat}}\",\"\")\r", - " const wamid = pm.variables.get(\"whatsapp_id_38\")\r", - " const USER_1_NUMBER = pm.variables.get(\"USER_1_NUMBER\")\r", - " const expected = {\r", - " \"hostname\": \"resultserver\",\r", - " \"path\": \"/v15.0/WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID/messages\",\r", - " \"body\": {\r", - " \"text\": {\r", - " \"body\": BODY_TEXT,\r", - " \"preview_url\": false\r", - " },\r", - " \"to\": USER_1_NUMBER,\r", - " \"messaging_product\": \"whatsapp\",\r", - " \"context\": {\r", - " \"message_id\": wamid\r", - " }\r", - " },\r", - " \"method\": \"POST\"\r", - "}\r", - " var jsonData = pm.response.json();\r", - " pm.expect(jsonData).to.eql(expected);\r", - "});" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(() => {}, 3000);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{RESULT_SERVER_URL}}/testresultdata", - "host": [ - "{{RESULT_SERVER_URL}}" - ], - "path": [ - "testresultdata" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "011_Check_Steps_logging", - "item": [ - { - "name": "[DB CALL] Check steps", + "name": "Get last message (GENERIC_MENU)", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test(\"Check that instance data updated correctly\", function () {\r", - "\r", - " var jsonData = pm.response.json();\r", - " const initialJourney = jsonData.fields.initialJourney.mapValue.fields\r", - " const actions = Object.values(initialJourney).map((obj) => obj.stringValue)\r", - " const expected = [\r", - " \"text_machine_irrelevant_length\",\r", - " \"languageSelection_en\",\r", - " \"text_machine_irrelevant_length\",\r", - " \"menu_dispute\",\r", - " \"text_machine_unsure\",\r", - " \"sendInterim\",\r", - " \"sendInterim\",\r", - " \"sendInterim\",\r", - " \"satisfactionSurvey_10\",\r", - " \"sendInterim\",\r", - " \"votingResults\",\r", - " \"text_machine_info\",\r", - " \"sendInterim\",\r", - " \"votingResults\",\r", - " \"text_machine_unsure\",\r", - " \"sendInterim\",\r", - " \"sendInterim\",\r", - " \"sendInterim\",\r", - " \"votingResults\",\r", - " \"text_menu\",\r", - " \"menu_check\",\r", + "pm.test(\"Check that menu now has subscribe button\", function () {\r", + " const MENU = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU.en\");\r", + " const PREFIX = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_PREFIX.en\");\r", + " const MENU_TEXT = MENU.replace(\"{{prefix}}\",PREFIX);\r", + " const MENU_BUTTON = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_BUTTON.en\");\r", + " const MENU_TITLE_CHECK = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_CHECK.en\");\r", + " const MENU_DESCRIPTION_CHECK = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_CHECK.en\");\r", + " const MENU_TITLE_REFERRAL = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_REFERRAL.en\");\r", + " const MENU_DESCRIPTION_REFERRAL = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_REFERRAL.en\");\r", + " const MENU_TITLE_ABOUT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_ABOUT.en\");\r", + " const MENU_DESCRIPTION_ABOUT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_ABOUT.en\");\r", + " const MENU_TITLE_HELP = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_HELP.en\");\r", + " const MENU_DESCRIPTION_HELP = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_HELP.en\");\r", + " const MENU_TITLE_FEEDBACK = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_FEEDBACK.en\");\r", + " const MENU_DESCRIPTION_FEEDBACK = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_FEEDBACK.en\");\r", + " const MENU_TITLE_LANGUAGE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_LANGUAGE.en\")\r", + " const MENU_DESCRIPTION_LANGUAGE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_LANGUAGE.en\")\r", + " const MENU_TITLE_CONTACT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_CONTACT.en\");\r", + " const MENU_DESCRIPTION_CONTACT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_CONTACT.en\");\r", + " const MENU_TITLE_SUB = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_SUB.en\")\r", + " const MENU_DESCRIPTION_SUB = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_SUB.en\")\r", + " const MENU_TITLE_DISPUTE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_DISPUTE.en\")\r", + " const MENU_DESCRIPTION_DISPUTE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_DISPUTE.en\")\r", + " \r", + " const USER_1_NAME = pm.variables.get(\"USER_1_NAME\")\r", + " const USER_1_NUMBER = pm.variables.get(\"USER_1_NUMBER\")\r", + " const expected = {\r", + " \"hostname\": \"resultserver\",\r", + " \"path\": \"/v15.0/WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID/messages\",\r", + " \"body\": {\r", + " \"messaging_product\": \"whatsapp\",\r", + " \"recipient_type\": \"individual\",\r", + " \"to\": USER_1_NUMBER,\r", + " \"type\": \"interactive\",\r", + " \"interactive\": {\r", + " \"type\": \"list\",\r", + " \"body\": {\r", + " \"text\": MENU_TEXT\r", + " },\r", + " \"action\": {\r", + " \"button\": MENU_BUTTON,\r", + " \"sections\": [\r", + " {\r", + " \"rows\": [\r", + " {\r", + " \"id\": \"menu_check\",\r", + " \"title\": MENU_TITLE_CHECK,\r", + " \"description\": MENU_DESCRIPTION_CHECK\r", + " },\r", + " {\r", + " \"id\": \"menu_referral\",\r", + " \"title\": MENU_TITLE_REFERRAL,\r", + " \"description\": MENU_DESCRIPTION_REFERRAL\r", + " },\r", + " {\r", + " \"id\": \"menu_help\",\r", + " \"title\": MENU_TITLE_HELP,\r", + " \"description\": MENU_DESCRIPTION_HELP\r", + " },\r", + " {\r", + " \"id\": \"menu_about\",\r", + " \"title\": MENU_TITLE_ABOUT,\r", + " \"description\": MENU_DESCRIPTION_ABOUT\r", + " },\r", + " {\r", + " \"id\": \"menu_feedback\",\r", + " \"title\": MENU_TITLE_FEEDBACK,\r", + " \"description\": MENU_DESCRIPTION_FEEDBACK\r", + " },\r", + " {\r", + " \"id\": \"menu_language\",\r", + " \"title\": MENU_TITLE_LANGUAGE,\r", + " \"description\": MENU_DESCRIPTION_LANGUAGE\r", + " },\r", + " {\r", + " \"id\": \"menu_contact\",\r", + " \"title\": MENU_TITLE_CONTACT,\r", + " \"description\": MENU_DESCRIPTION_CONTACT\r", + " },\r", + " {\r", + " \"id\": \"menu_subscribeUpdates\",\r", + " \"title\": MENU_TITLE_SUB,\r", + " \"description\": MENU_DESCRIPTION_SUB\r", + " },\r", + " ]\r", + " }\r", + " ]\r", + " }\r", + " }\r", + " },\r", + " \"method\": \"POST\"\r", + " }\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.eql(expected);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(() => {}, 2000);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{RESULT_SERVER_URL}}/testresultdata", + "host": [ + "{{RESULT_SERVER_URL}}" + ], + "path": [ + "testresultdata" + ] + } + }, + "response": [] + }, + { + "name": "Make Menu Selection (\"Subscribe\")", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "// 1. Retrieve the current messageCounter and increment it.\r", + "let messageCounter = 36\r", + "\r", + "const whatsappId = \"wamid.HBgKNjU5Njg4MDMyMBUCABIYIEY4MDAwNTlEODQyMDZDMkNDOEU1NEVEQjc1MTNCMjlFA1==\";\r", + "\r", + "// 3. Replace the last n characters (excluding the == at the end) of whatsappId with the new messageCounter\r", + "let n = messageCounter.toString().length;\r", + "let basePart = whatsappId.substring(0, whatsappId.length - n - 2);\r", + "let newId = basePart + messageCounter + \"==\";\r", + "\r", + "pm.collectionVariables.set(`whatsapp_id_${messageCounter}`, newId)\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"object\": \"whatsapp_business_account\",\r\n \"entry\": [\r\n {\r\n \"id\": \"WHATSAPP_TEST_USER_WABA_ID\",\r\n \"changes\": [\r\n {\r\n \"value\": {\r\n \"messaging_product\": \"whatsapp\",\r\n \"metadata\": {\r\n \"display_phone_number\": \"15550933685\",\r\n \"phone_number_id\": \"WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID\"\r\n },\r\n \"contacts\": [\r\n {\r\n \"profile\": {\r\n \"name\": \"{{USER_1_NAME}}\"\r\n },\r\n \"wa_id\": \"{{USER_1_NUMBER}}\"\r\n }\r\n ],\r\n \"messages\": [\r\n {\r\n \"context\": {\r\n \"from\": \"15550933685\",\r\n \"id\": \"wamid.HBgKNjU5Njg4MDMyMBUCABIYIEY4MDAwNTlEODQyMDZDMkNDOEU1NEVEQjc1MTNCMjlFAA==\"\r\n },\r\n \"from\": \"{{USER_1_NUMBER}}\",\r\n \"id\": \"{{whatsapp_id_36}}\",\r\n \"timestamp\": {{$timestamp}},\r\n \"type\": \"interactive\",\r\n \"interactive\": {\r\n \"type\": \"list_reply\",\r\n \"list_reply\": {\r\n \"id\": \"menu_subscribeUpdates\",\r\n \"title\": \"{{__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_SUB.en}}\",\r\n \"description\": \"{{__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_SUB.en}}\"\r\n }\r\n }\r\n }\r\n ]\r\n },\r\n \"field\": \"messages\"\r\n }\r\n ]\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{FUNCTIONS_URL}}/{{PROJECT_ID}}/asia-southeast1/webhookHandlerV2/{{WEBHOOK_PATH_WHATSAPP}}", + "host": [ + "{{FUNCTIONS_URL}}" + ], + "path": [ + "{{PROJECT_ID}}", + "asia-southeast1", + "webhookHandlerV2", + "{{WEBHOOK_PATH_WHATSAPP}}" + ] + } + }, + "response": [] + }, + { + "name": "Get last message (Subscribe)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Your test name\", function () {\r", + " const BODY_TEXT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.SUBSCRIBE.en\");\r", + " const USER_1_NUMBER = pm.variables.get(\"USER_1_NUMBER\")\r", + " const expected = {\r", + " \"hostname\": \"resultserver\",\r", + " \"path\": \"/v15.0/WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID/messages\",\r", + " \"body\": {\r", + " \"text\": {\r", + " \"body\": BODY_TEXT,\r", + " \"preview_url\": true\r", + " },\r", + " \"to\": USER_1_NUMBER,\r", + " \"messaging_product\": \"whatsapp\",\r", + " },\r", + " \"method\": \"POST\"\r", + "}\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.eql(expected);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(() => {}, 2000);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{RESULT_SERVER_URL}}/testresultdata", + "host": [ + "{{RESULT_SERVER_URL}}" + ], + "path": [ + "testresultdata" + ] + } + }, + "response": [] + }, + { + "name": "[DB CALL] Check isSubscribedUpdates true", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Check that has isSubscribedUpdates is false\", function () {\r", + "\r", + " var jsonData = pm.response.json();\r", + " const isSubscribedUpdates = jsonData.fields.isSubscribedUpdates.booleanValue\r", + " pm.expect(isSubscribedUpdates).to.be.true;\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer owner", + "type": "text" + } + ], + "url": { + "raw": "{{FIRESTORE_URL}}/v1/projects/{{PROJECT_ID}}/databases/(default)/documents/users/{{USER_1_NUMBER}}", + "host": [ + "{{FIRESTORE_URL}}" + ], + "path": [ + "v1", + "projects", + "{{PROJECT_ID}}", + "databases", + "(default)", + "documents", + "users", + "{{USER_1_NUMBER}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "009_Scam_Autocategorise", + "item": [ + { + "name": "User sends scam message", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "// 1. Retrieve the current messageCounter and increment it.\r", + "let messageCounter = 37\r", + "\r", + "const whatsappId = \"wamid.HBgKNjU5Njg4MDMyMBUCABIYIEY4MDAwNTlEODQyMDZDMkNDOEU1NEVEQjc1MTNCMjlFA1==\";\r", + "\r", + "// 3. Replace the last n characters (excluding the == at the end) of whatsappId with the new messageCounter\r", + "let n = messageCounter.toString().length;\r", + "let basePart = whatsappId.substring(0, whatsappId.length - n - 2);\r", + "let newId = basePart + messageCounter + \"==\";\r", + "\r", + "pm.collectionVariables.set(`whatsapp_id_${messageCounter}`, newId)\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"object\": \"whatsapp_business_account\",\r\n \"entry\": [\r\n {\r\n \"id\": \"WHATSAPP_TEST_USER_WABA_ID\",\r\n \"changes\": [\r\n {\r\n \"value\": {\r\n \"messaging_product\": \"whatsapp\",\r\n \"metadata\": {\r\n \"display_phone_number\": \"15550933685\",\r\n \"phone_number_id\": \"WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID\"\r\n },\r\n \"contacts\": [\r\n { \"profile\": { \"name\": \"{{USER_1_NAME}}\" }, \"wa_id\": \"{{USER_1_NUMBER}}\" }\r\n ],\r\n \"messages\": [\r\n {\r\n \"from\": \"{{USER_1_NUMBER}}\",\r\n \"id\": \"{{whatsapp_id_37}}\",\r\n \"timestamp\": {{$timestamp}},\r\n \"text\": { \"body\": \"This is a scam message\" },\r\n \"type\": \"text\"\r\n }\r\n ]\r\n },\r\n \"field\": \"messages\"\r\n }\r\n ]\r\n }\r\n ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{FUNCTIONS_URL}}/{{PROJECT_ID}}/asia-southeast1/webhookHandlerV2/{{WEBHOOK_PATH_WHATSAPP}}", + "host": [ + "{{FUNCTIONS_URL}}" + ], + "path": [ + "{{PROJECT_ID}}", + "asia-southeast1", + "webhookHandlerV2", + "{{WEBHOOK_PATH_WHATSAPP}}" + ] + } + }, + "response": [] + }, + { + "name": "Get last message (SCAM, IMMEDIATE, NOT MATCHED, AUTO)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"scam immediate autocategorised no matched response\", function () {\r", + " const TEMPLATE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.SCAM.en\");\r", + " const THANKS_IMMEDIATE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.THANKS_IMMEDIATE.en\")\r", + " const THANKS_DELAYED = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.THANKS_DELAYED.en\")\r", + " const METHODOLOGY_HUMAN = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.METHODOLOGY_HUMAN.en\")\r", + " const METHODOLOGY_AUTO = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.METHODOLOGY_AUTO.en\")\r", + " const VOTE_RESULTS_SUFFIX = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.VOTE_RESULTS_SUFFIX.en\")\r", + " const BUTTON_RESULTS = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_RESULTS.en\")\r", + " const BUTTON_DECLINE_REPORT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_DECLINE_REPORT.en\")\r", + " const BUTTON_RATIONALISATION = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_RATIONALISATION.en\")\r", + " const wamid = pm.variables.get(\"whatsapp_id_37\")\r", + " const BODY_TEXT = TEMPLATE.replace(\"{{thanks}}\",THANKS_IMMEDIATE).replace(\"{{methodology}}\",METHODOLOGY_AUTO).replace(\"{{matched}}\",\"\").replace(\"{{results}}\",\"\").replace(\"{{image_caveat}}\",\"\")\r", + " const USER_1_NAME = pm.variables.get(\"USER_1_NAME\")\r", + " const USER_1_NUMBER = pm.variables.get(\"USER_1_NUMBER\")\r", + " const expected = {\r", + " \"hostname\": \"resultserver\",\r", + " \"path\": \"/v15.0/WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID/messages\",\r", + " \"body\": {\r", + " \"messaging_product\": \"whatsapp\",\r", + " \"recipient_type\": \"individual\",\r", + " \"to\": USER_1_NUMBER,\r", + " \"type\": \"interactive\",\r", + " \"interactive\": {\r", + " \"type\": \"button\",\r", + " \"body\": {\r", + " \"text\": BODY_TEXT\r", + " },\r", + " \"action\": {\r", + " \"buttons\": [\r", + " {\r", + " \"type\": \"reply\",\r", + " \"reply\": {\r", + " \"id\": \"ID\",\r", + " \"title\": BUTTON_RATIONALISATION\r", + " }\r", + " },\r", + " {\r", + " \"type\": \"reply\",\r", + " \"reply\": {\r", + " \"id\": \"ID\",\r", + " \"title\": BUTTON_DECLINE_REPORT\r", + " }\r", + " }\r", + " ]\r", + " }\r", + " },\r", + " \"context\": {\r", + " \"message_id\": wamid\r", + " }\r", + " },\r", + " \"method\": \"POST\"\r", + " }\r", + "\r", + " var jsonData = pm.response.json();\r", + "\r", + " let reply_id_rationalisation = jsonData.body.interactive.action.buttons[0].reply.id;\r", + " let reply_id_decline = jsonData.body.interactive.action.buttons[1].reply.id;\r", + " let [, scamInstancePath , ] = reply_id_rationalisation.split(\"_\")\r", + " \r", + " // check if the id matches the pattern \"menu_dispute_messages/*/instances/*\"\r", + " pm.expect(reply_id_rationalisation).to.match(/rationalisation_messages\\/\\w+\\/instances\\/\\w+/);\r", + " pm.expect(reply_id_decline).to.match(/scamshieldDecline_messages\\/\\w+\\/instances\\/\\w+/);\r", + " pm.collectionVariables.set(\"scamInstancePath\", scamInstancePath);\r", + "\r", + " jsonData.body.interactive.action.buttons[0].reply.id = \"ID\"\r", + " jsonData.body.interactive.action.buttons[1].reply.id = \"ID\"\r", + "\r", + " pm.expect(jsonData).to.eql(expected);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(() => {}, 5000);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{RESULT_SERVER_URL}}/testresultdata", + "host": [ + "{{RESULT_SERVER_URL}}" + ], + "path": [ + "testresultdata" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "010_Spam_Autocategorise", + "item": [ + { + "name": "User sends spam message", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "// 1. Retrieve the current messageCounter and increment it.\r", + "let messageCounter = 38\r", + "\r", + "const whatsappId = \"wamid.HBgKNjU5Njg4MDMyMBUCABIYIEY4MDAwNTlEODQyMDZDMkNDOEU1NEVEQjc1MTNCMjlFA1==\";\r", + "\r", + "// 3. Replace the last n characters (excluding the == at the end) of whatsappId with the new messageCounter\r", + "let n = messageCounter.toString().length;\r", + "let basePart = whatsappId.substring(0, whatsappId.length - n - 2);\r", + "let newId = basePart + messageCounter + \"==\";\r", + "\r", + "pm.collectionVariables.set(`whatsapp_id_${messageCounter}`, newId)\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"object\": \"whatsapp_business_account\",\r\n \"entry\": [\r\n {\r\n \"id\": \"WHATSAPP_TEST_USER_WABA_ID\",\r\n \"changes\": [\r\n {\r\n \"value\": {\r\n \"messaging_product\": \"whatsapp\",\r\n \"metadata\": {\r\n \"display_phone_number\": \"15550933685\",\r\n \"phone_number_id\": \"WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID\"\r\n },\r\n \"contacts\": [\r\n { \"profile\": { \"name\": \"{{USER_1_NAME}}\" }, \"wa_id\": \"{{USER_1_NUMBER}}\" }\r\n ],\r\n \"messages\": [\r\n {\r\n \"from\": \"{{USER_1_NUMBER}}\",\r\n \"id\": \"{{whatsapp_id_38}}\",\r\n \"timestamp\": {{$timestamp}},\r\n \"text\": { \"body\": \"This is a spam message\" },\r\n \"type\": \"text\"\r\n }\r\n ]\r\n },\r\n \"field\": \"messages\"\r\n }\r\n ]\r\n }\r\n ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{FUNCTIONS_URL}}/{{PROJECT_ID}}/asia-southeast1/webhookHandlerV2/{{WEBHOOK_PATH_WHATSAPP}}", + "host": [ + "{{FUNCTIONS_URL}}" + ], + "path": [ + "{{PROJECT_ID}}", + "asia-southeast1", + "webhookHandlerV2", + "{{WEBHOOK_PATH_WHATSAPP}}" + ] + } + }, + "response": [] + }, + { + "name": "Get last message (SPAM, IMMEDIATE, NOT MATCHED, AUTO)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"spam immediate autocategorised no matched response\", function () {\r", + " const TEMPLATE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.SPAM.en\");\r", + " const THANKS_IMMEDIATE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.THANKS_IMMEDIATE.en\")\r", + " const METHODOLOGY_AUTO = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.METHODOLOGY_AUTO.en\")\r", + " const BUTTON_RESULTS = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_RESULTS.en\")\r", + " const BUTTON_DECLINE_REPORT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_DECLINE_REPORT.en\")\r", + " const BUTTON_RATIONALISATION = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_RATIONALISATION.en\")\r", + " const BODY_TEXT = TEMPLATE.replace(\"{{thanks}}\",THANKS_IMMEDIATE).replace(\"{{methodology}}\",METHODOLOGY_AUTO).replace(\"{{matched}}\",\"\").replace(\"{{image_caveat}}\",\"\")\r", + " const wamid = pm.variables.get(\"whatsapp_id_38\")\r", + " const USER_1_NUMBER = pm.variables.get(\"USER_1_NUMBER\")\r", + " const expected = {\r", + " \"hostname\": \"resultserver\",\r", + " \"path\": \"/v15.0/WHATSAPP_TEST_USER_BOT_PHONE_NUMBER_ID/messages\",\r", + " \"body\": {\r", + " \"text\": {\r", + " \"body\": BODY_TEXT,\r", + " \"preview_url\": false\r", + " },\r", + " \"to\": USER_1_NUMBER,\r", + " \"messaging_product\": \"whatsapp\",\r", + " \"context\": {\r", + " \"message_id\": wamid\r", + " }\r", + " },\r", + " \"method\": \"POST\"\r", + "}\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.eql(expected);\r", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(() => {}, 5000);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{RESULT_SERVER_URL}}/testresultdata", + "host": [ + "{{RESULT_SERVER_URL}}" + ], + "path": [ + "testresultdata" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "011_Check_Steps_logging", + "item": [ + { + "name": "[DB CALL] Check steps", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Check that instance data updated correctly\", function () {\r", + "\r", + " var jsonData = pm.response.json();\r", + " const initialJourney = jsonData.fields.initialJourney.mapValue.fields\r", + " const actions = Object.values(initialJourney).map((obj) => obj.stringValue)\r", + " const expected = [\r", + " \"text_machine_irrelevant_length\",\r", + " \"languageSelection_en\",\r", + " \"text_machine_irrelevant_length\",\r", + " \"menu_dispute\",\r", + " \"text_machine_unsure\",\r", + " \"sendInterim\",\r", + " \"sendInterim\",\r", + " \"sendInterim\",\r", + " \"satisfactionSurvey_10\",\r", + " \"sendInterim\",\r", + " \"votingResults\",\r", + " \"text_machine_info\",\r", + " \"sendInterim\",\r", + " \"votingResults\",\r", + " \"text_machine_unsure\",\r", + " \"sendInterim\",\r", + " \"sendInterim\",\r", + " \"sendInterim\",\r", + " \"votingResults\",\r", + " \"text_menu\",\r", + " \"menu_check\",\r", " \"menu_help\",\r", " \"menu_about\",\r", " \"menu_feedback\",\r", @@ -12041,6 +12587,9 @@ " \"languageSelection_en\",\r", " \"menu_contact\",\r", " \"menu_referral\",\r", + " \"menu_unsubscribeUpdates\",\r", + " \"text_menu\",\r", + " \"menu_subscribeUpdates\",\r", " \"text_machine_scam\",\r", " \"text_machine_spam\",\r", " ]\r", @@ -12312,6 +12861,8 @@ " const MENU_DESCRIPTION_LANGUAGE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_LANGUAGE.en\")\r", " const MENU_TITLE_CONTACT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_CONTACT.en\");\r", " const MENU_DESCRIPTION_CONTACT = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_CONTACT.en\");\r", + " const MENU_TITLE_UNSUB = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_UNSUB.en\")\r", + " const MENU_DESCRIPTION_UNSUB = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_UNSUB.en\")\r", " const MENU_TITLE_DISPUTE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_DISPUTE.en\")\r", " const MENU_DESCRIPTION_DISPUTE = pm.variables.get(\"__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_DISPUTE.en\")\r", " \r", @@ -12369,6 +12920,11 @@ " \"id\": \"menu_contact\",\r", " \"title\": MENU_TITLE_CONTACT,\r", " \"description\": MENU_DESCRIPTION_CONTACT\r", + " },\r", + " {\r", + " \"id\": \"menu_unsubscribeUpdates\",\r", + " \"title\": MENU_TITLE_UNSUB,\r", + " \"description\": MENU_DESCRIPTION_UNSUB\r", " }\r", " ]\r", " }\r", diff --git a/integration-tests/env.json b/integration-tests/env.json index 3921b826..d63f2f47 100644 --- a/integration-tests/env.json +++ b/integration-tests/env.json @@ -165,7 +165,7 @@ }, { "key": "__CONSTANTS__.USER_BOT_RESPONSES.UNTRUE.cn", - "value": "{thanks}}{{matched}}{{methodology}}不属实。❌{{image_caveat}}\n\n请不要转发⛔️⛔️\n\n感谢您对新加坡网络安全的支持和贡献!\n", + "value": "{{thanks}}{{matched}}{{methodology}}不属实。❌{{image_caveat}}\n\n请不要转发⛔️⛔️\n\n感谢您对新加坡网络安全的支持和贡献!\n", "enabled": true }, { @@ -260,7 +260,7 @@ }, { "key": "__CONSTANTS__.USER_BOT_RESPONSES.THANKS_IMMEDIATE.en", - "value": "Thanks for sending this in!", + "value": "Thanks for sending this in! ", "enabled": true }, { @@ -270,7 +270,7 @@ }, { "key": "__CONSTANTS__.USER_BOT_RESPONSES.THANKS_DELAYED.en", - "value": "Thanks for waiting!", + "value": "Thanks for waiting! ", "enabled": true }, { @@ -280,7 +280,7 @@ }, { "key": "__CONSTANTS__.USER_BOT_RESPONSES.METHODOLOGY_HUMAN.en", - "value": "Our CheckMates have reviewed this message and think it's", + "value": "Our CheckMates have reviewed this message and think it's ", "enabled": true }, { @@ -340,12 +340,12 @@ }, { "key": "__CONSTANTS__.USER_BOT_RESPONSES.INTERIM_PROMPT.en", - "value": "Thanks for waiting! We are currently still pending the assessment from some of our network of trusted CheckMate volunteers and will only be able to provide a credible final result once enough votes have come in. \n\nYou may press the button below *to get an interim update of the preliminary result*. However, do note that there may be discrepancies between the preliminary and the final result, and *the preliminary result should be interpreted with caution*. We appreciate your patience and hope to deliver the final result to you soon! 💪🏼", + "value": "Thanks for waiting! Our network of trusted CheckMate volunteers is still assessing your message.\n\nYou may press the button below to see how our volunteers have rated your message so far. However, *do interpret this preliminary rating with caution* as the final result may differ. \n\nWe appreciate your patience and hope to deliver the final result soon!", "enabled": true }, { "key": "__CONSTANTS__.USER_BOT_RESPONSES.INTERIM_PROMPT.cn", - "value": "感谢您的耐心等待!查哥查妹正在对您提交的短信进行投票评估。我们将在足够多的查哥查妹票后,为您提供最终结果。\n\n在最终结果发布之前,您可以点击“获取初步结果”。请注意,初步结果和最终结果可能存在差异,应谨慎解读初步结果。我们感谢您的耐心,并会尽快提供最终结果!💪🏼", + "value": "感谢您的耐心等待!查哥查妹正在对您提交的短信进行投票评估。我们将在足够多的查哥查妹投票后,为您提供最终结果。\n\n在最终结果发布之前,您可以点击“获取初步结果”。请注意,初步结果和最终结果可能存在差异,应谨慎解读初步结果。我们感谢您的耐心,并会尽快提供最终结果!💪🏼", "enabled": true }, { @@ -548,16 +548,6 @@ "value": "这是一项由AI技术提供的实验性功能。请在下方告诉我们它是否对您有所帮助!\n\n{{rationalisation}}", "enabled": true }, - { - "key": "__CONSTANTS__.USER_BOT_RESPONSES.RATIONALISATION_USEFUL.en", - "value": "Thanks for your valuable feedback!", - "enabled": true - }, - { - "key": "__CONSTANTS__.USER_BOT_RESPONSES.RATIONALISATION_USEFUL.cn", - "value": "感谢您的反馈!", - "enabled": true - }, { "key": "__CONSTANTS__.USER_BOT_RESPONSES.RATIONALISATION_NOT_USEFUL.en", "value": "Sorry to hear that, but thanks anyway for your valuable feedback!", @@ -930,7 +920,7 @@ }, { "key": "__CONSTANTS__.USER_BOT_RESPONSES.MENU.en", - "value": "{{prefix}}\n\nIf you know what to do, please go ahead! Else, select \"Menu\" below to see what CheckMate can do! 👈\n\nDo note that CheckMate *is designed to check dubious messages you send in. It cannot converse freely with you*.\n\nAnytime you need a refresher on what CheckMate can do, type \"menu\" to get here again! 😊", + "value": "{{prefix}}\n\nSelect \"Menu\" below to see what CheckMate can do! 👈\n\nDo note that CheckMate *is designed to check dubious messages you send in. It cannot converse freely with you*.\n\nAnytime you need a refresher on what CheckMate can do, type \"menu\" to get here again! 😊", "enabled": true }, { @@ -1017,6 +1007,116 @@ "key": "__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_CHINESE.cn", "value": "华语", "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.UNSUBSCRIBE.en", + "value": "Sorry to see you go.😞 If you change your mind, you can always come back to the menu to subscribe again!", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.UNSUBSCRIBE.cn", + "value": "很遗憾看到您离开。😞 如果您改变主意,您可以随时回到菜单重新订阅!", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.SUBSCRIBE.en", + "value": "Thanks for subscribing!🙏🏻 CheckMate will now send you content occasionally. Hope you find it useful!", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.SUBSCRIBE.cn", + "value": "感谢您的订阅!查友将偶尔向您发送内容。希望您觉得它有用!", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.FEEDBACK_THANKS.en", + "value": "Thanks for your valuable feedback!", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.FEEDBACK_THANKS.cn", + "value": "感谢您的反馈!", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.BLAST_FEEDBACK.en", + "value": "Did you find this content useful?\n\nIf you wish to unsubscribe, type \"menu\" and select the \"Unsubscribe\" option", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.BLAST_FEEDBACK.cn", + "value": "是否认为这份内容对您有帮助?\n\n如果您希望取消订阅,请输入“菜单”并选择“取消订阅”选项。", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_UNSUB.en", + "value": "Unsubscribe", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_UNSUB.cn", + "value": "取消订阅", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_UNSUB.en", + "value": "Unsubscribe from receiving content from CheckMate", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_UNSUB.cn", + "value": "取消订阅查友不时内容", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_SUB.en", + "value": "Subscribe", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.MENU_TITLE_SUB.cn", + "value": "订阅", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_SUB.en", + "value": "Subscribe to occasional content from CheckMate", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.MENU_DESCRIPTION_SUB.cn", + "value": "订阅查友不时内容", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_SHIOK.en", + "value": "😍", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_SHIOK.cn", + "value": "😍", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_MEH.en", + "value": "😐", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_MEH.cn", + "value": "😐", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_BOO.en", + "value": "😞", + "enabled": true + }, + { + "key": "__CONSTANTS__.USER_BOT_RESPONSES.BUTTON_BOO.cn", + "value": "😞", + "enabled": true } ], "_postman_variable_scope": "environment", From 6fcbcacb64a6ea8230ad99e416ddf6acbe44e8c8 Mon Sep 17 00:00:00 2001 From: Bing Wen Tan Date: Sat, 2 Dec 2023 09:58:49 +0800 Subject: [PATCH 8/8] fixed anonymise null text bug --- functions/src/definitions/eventHandlers/onMessageUpdate.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/functions/src/definitions/eventHandlers/onMessageUpdate.ts b/functions/src/definitions/eventHandlers/onMessageUpdate.ts index c8354994..11e58326 100644 --- a/functions/src/definitions/eventHandlers/onMessageUpdate.ts +++ b/functions/src/definitions/eventHandlers/onMessageUpdate.ts @@ -60,7 +60,8 @@ const onMessageUpdate = functions } if ( before.data().primaryCategory !== primaryCategory && - primaryCategory === "legitimate" + primaryCategory === "legitimate" && + text ) { const anonymisedText = await anonymiseMessage(text, false) await after.ref.update({