From 11332df7919d6ab487d6e8caaf8eb1fee30008e7 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 13 Nov 2023 01:36:53 +0800 Subject: [PATCH 1/2] refactor(webhook): move selectedArticleId from context to choosingReply action payload --- src/types/chatbotState.ts | 2 -- .../choosingArticle.test.ts.snap | 6 ---- .../__snapshots__/choosingReply.test.ts.snap | 2 -- .../handlers/__tests__/choosingReply.test.ts | 20 ++++++++----- src/webhook/handlers/choosingArticle.ts | 10 +++++-- src/webhook/handlers/choosingReply.ts | 30 ++++++++++++------- 6 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/types/chatbotState.ts b/src/types/chatbotState.ts index cd96d502..da20bdaa 100644 --- a/src/types/chatbotState.ts +++ b/src/types/chatbotState.ts @@ -12,8 +12,6 @@ export type ChatbotState = export type Context = { /** Used to differientiate different search sessions (searched text or media) */ sessionId: number; - /** User selected article in DB */ - selectedArticleId?: string; } & ( | { /** Searched multi-media message that started this search session */ diff --git a/src/webhook/handlers/__tests__/__snapshots__/choosingArticle.test.ts.snap b/src/webhook/handlers/__tests__/__snapshots__/choosingArticle.test.ts.snap index e2115804..cf3e7b35 100644 --- a/src/webhook/handlers/__tests__/__snapshots__/choosingArticle.test.ts.snap +++ b/src/webhook/handlers/__tests__/__snapshots__/choosingArticle.test.ts.snap @@ -200,7 +200,6 @@ exports[`should select article and choose the only one reply for user 1`] = ` Object { "data": Object { "searchedText": "Just One Reply Just One Reply Just One Reply Just One Reply Just One Reply", - "selectedArticleId": "article-id", "sessionId": 0, }, "replies": Array [ @@ -328,7 +327,6 @@ exports[`should select article and have OPINIONATED and NOT_ARTICLE replies 1`] Object { "data": Object { "searchedText": "老榮民九成存款全部捐給慈濟,如今窮了卻得不到慈濟醫院社工的幫忙,竟翻臉不認人", - "selectedArticleId": "article-id", "sessionId": 1497994017447, }, "replies": Array [ @@ -675,7 +673,6 @@ exports[`should select article and slice replies when over 10 1`] = ` Object { "data": Object { "searchedText": "老榮民九成存款全部捐給慈濟,如今窮了卻得不到慈濟醫院社工的幫忙,竟翻臉不認人", - "selectedArticleId": "article-id", "sessionId": 0, }, "replies": Array [ @@ -1500,7 +1497,6 @@ Object { 因為最近已經發現- 好多病人因為吃了生魚片,胃壁附著《海獸胃腺蟲》,大小隻不一定,有的病人甚至胃壁上滿滿都是無法夾出來,驅蟲藥也很難根治,罹患機率每個國家的人都一樣。 尤其;鮭魚的含蟲量最高、最可怕! 請傳給朋友,讓他們有所警惕!", - "selectedArticleId": "article-id", "sessionId": 0, }, "replies": Array [ @@ -1689,7 +1685,6 @@ exports[`should select article with no replies: has AI reply 1`] = ` Object { "data": Object { "searchedText": "老司機車裡總備一塊香皂,知道內情的新手默默也準備了一塊", - "selectedArticleId": "article-id", "sessionId": 0, }, "replies": Array [ @@ -1869,7 +1864,6 @@ exports[`should select article with no replies: has no AI reply 1`] = ` Object { "data": Object { "searchedText": "老司機車裡總備一塊香皂,知道內情的新手默默也準備了一塊", - "selectedArticleId": "article-id", "sessionId": 0, }, "replies": Array [ diff --git a/src/webhook/handlers/__tests__/__snapshots__/choosingReply.test.ts.snap b/src/webhook/handlers/__tests__/__snapshots__/choosingReply.test.ts.snap index d2d5596b..4d116b7f 100644 --- a/src/webhook/handlers/__tests__/__snapshots__/choosingReply.test.ts.snap +++ b/src/webhook/handlers/__tests__/__snapshots__/choosingReply.test.ts.snap @@ -245,7 +245,6 @@ exports[`should select reply by replyId should handle the case with just one rep Object { "data": Object { "searchedText": "貼圖", - "selectedArticleId": "AWDZYXxAyCdS-nWhumlz", "sessionId": 0, }, "replies": Array [ @@ -373,7 +372,6 @@ exports[`should select reply by replyId should handle the case with multiple rep Object { "data": Object { "searchedText": "貼圖", - "selectedArticleId": "AWDZYXxAyCdS-nWhumlz", "sessionId": 0, }, "replies": Array [ diff --git a/src/webhook/handlers/__tests__/choosingReply.test.ts b/src/webhook/handlers/__tests__/choosingReply.test.ts index cac6fe52..60372b96 100644 --- a/src/webhook/handlers/__tests__/choosingReply.test.ts +++ b/src/webhook/handlers/__tests__/choosingReply.test.ts @@ -2,7 +2,7 @@ jest.mock('src/lib/gql'); jest.mock('src/lib/ga'); import MockDate from 'mockdate'; -import choosingReply from '../choosingReply'; +import choosingReply, { Input } from '../choosingReply'; import * as apiResult from '../__fixtures__/choosingReply'; import UserSettings from 'src/database/models/userSettings'; import originalGql from 'src/lib/gql'; @@ -25,15 +25,19 @@ afterEach(() => { }); describe('should select reply by replyId', () => { + const input: Input = { + a: 'AWDZYXxAyCdS-nWhumlz', + r: 'AWDZeeV0yCdS-nWhuml8', + }; + const params: ChatbotPostbackHandlerParams = { data: { sessionId: 0, searchedText: '貼圖', - selectedArticleId: 'AWDZYXxAyCdS-nWhumlz', }, postbackData: { sessionId: 0, - input: 'AWDZeeV0yCdS-nWhuml8', + input, state: 'CHOOSING_REPLY', }, userId: 'Uaddc74df8a3a176b901d9d648b0fc4fe', @@ -100,12 +104,11 @@ it('should block invalid postback input', async () => { data: { sessionId: 0, searchedText: '貼圖', - selectedArticleId: 'AWDZYXxAyCdS-nWhumlz', }, postbackData: { sessionId: 0, state: 'CHOOSING_REPLY', - input: undefined, + input: 'Some wrong string', }, userId: 'Uaddc74df8a3a176b901d9d648b0fc4fe', }; @@ -117,16 +120,19 @@ it('should block invalid postback input', async () => { it('should handle graphql error gracefully', async () => { gql.__push({ errors: [] }); + const input: Input = { + a: 'AWDZYXxAyCdS-nWhumlz', + r: 'AWDZeeV0yCdS-nWhuml8', + }; const params: ChatbotPostbackHandlerParams = { data: { sessionId: 0, searchedText: '貼圖', - selectedArticleId: 'AWDZYXxAyCdS-nWhumlz', }, postbackData: { sessionId: 0, - input: 'AWDZeeV0yCdS-nWhuml8', + input, state: 'CHOOSING_REPLY', }, userId: 'Uaddc74df8a3a176b901d9d648b0fc4fe', diff --git a/src/webhook/handlers/choosingArticle.ts b/src/webhook/handlers/choosingArticle.ts index 048b69f0..4527061c 100644 --- a/src/webhook/handlers/choosingArticle.ts +++ b/src/webhook/handlers/choosingArticle.ts @@ -27,6 +27,7 @@ import { import UserArticleLink from '../../database/models/userArticleLink'; import choosingReply from './choosingReply'; +import type { Input as ChoosingReplyInput } from './choosingReply'; import { ChatbotPostbackHandler } from 'src/types/chatbotState'; import { FlexBubble, Message } from '@line/bot-sdk'; @@ -116,7 +117,7 @@ const choosingArticle: ChatbotPostbackHandler = async (params) => { }; } - const selectedArticleId = (data.selectedArticleId = input); + const selectedArticleId = input; await UserArticleLink.createOrUpdateByUserIdAndArticleId( userId, @@ -171,13 +172,18 @@ const choosingArticle: ChatbotPostbackHandler = async (params) => { if (articleReplies.length === 1) { visitor.send(); + const input: ChoosingReplyInput = { + a: selectedArticleId, + r: articleReplies[0].reply?.id ?? '', + }; + // choose reply for user return await choosingReply({ data, postbackData: { sessionId, state: 'CHOOSING_REPLY', - input: articleReplies[0].reply?.id ?? '', + input, }, userId, }); diff --git a/src/webhook/handlers/choosingReply.ts b/src/webhook/handlers/choosingReply.ts index 7b6487ca..1b81dda0 100644 --- a/src/webhook/handlers/choosingReply.ts +++ b/src/webhook/handlers/choosingReply.ts @@ -1,4 +1,5 @@ import { t } from 'ttag'; +import { z } from 'zod'; import gql from 'src/lib/gql'; import { ManipulationError, @@ -17,6 +18,14 @@ import { ReplyTypeEnum, } from 'typegen/graphql'; +const inputSchema = z.object({ + a: z.string().describe('Article ID'), + r: z.string().describe('Reply ID'), +}); + +/** Postback input type for CHOOSING_REPLY state handler */ +export type Input = z.infer; + /** * @param {string} articleId - Article ID of the article-reply to feedback * @param {string} replyId - Reply ID of the article-reply to feedback @@ -133,13 +142,17 @@ function createShareBubble( const choosingReply: ChatbotPostbackHandler = async ({ data, userId, - postbackData: { input, state }, + postbackData: { input: postbackInput, state }, }) => { - if (typeof input !== 'string') { + let input: Input; + try { + input = inputSchema.parse(postbackInput); + } catch (e) { + console.error('[choosingReply]', e); throw new ManipulationError(t`Please choose from provided options.`); } - const selectedReplyId = input; + const { a: selectedArticleId, r: selectedReplyId } = input; const { data: getReplyData, errors } = await gql` query GetReplyRelatedData($id: String!, $articleId: String!) { @@ -156,7 +169,7 @@ const choosingReply: ChatbotPostbackHandler = async ({ } `({ id: selectedReplyId, - articleId: data.selectedArticleId ?? '', + articleId: selectedArticleId, }); /* istanbul ignore if */ @@ -177,17 +190,14 @@ const choosingReply: ChatbotPostbackHandler = async ({ ); const replies: Message[] = [ - ...createReplyMessages(GetReply, GetArticle, data.selectedArticleId ?? ''), + ...createReplyMessages(GetReply, GetArticle, selectedArticleId), { type: 'flex', altText: t`Is the reply helpful?`, contents: { type: 'carousel', contents: [ - createAskReplyFeedbackBubble( - data.selectedArticleId ?? '', - selectedReplyId - ), + createAskReplyFeedbackBubble(selectedArticleId, selectedReplyId), // Ask user to turn on notification if the user did not turn it on process.env.NOTIFY_METHOD && @@ -195,7 +205,7 @@ const choosingReply: ChatbotPostbackHandler = async ({ createNotificationSettingsBubble(), createShareBubble( - data.selectedArticleId ?? '', + selectedArticleId, getReplyData.GetArticle.text ?? '', GetReply.type ), From bf77c4d024545c7753d8e7baef64d0e16a1705fa Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 13 Nov 2023 01:47:56 +0800 Subject: [PATCH 2/2] fix(webhook): update snapshots --- .../handlers/__tests__/__snapshots__/initState.test.ts.snap | 1 - .../handlers/__tests__/__snapshots__/processMedia.test.js.snap | 1 - 2 files changed, 2 deletions(-) diff --git a/src/webhook/handlers/__tests__/__snapshots__/initState.test.ts.snap b/src/webhook/handlers/__tests__/__snapshots__/initState.test.ts.snap index 785fcfa9..34258911 100644 --- a/src/webhook/handlers/__tests__/__snapshots__/initState.test.ts.snap +++ b/src/webhook/handlers/__tests__/__snapshots__/initState.test.ts.snap @@ -463,7 +463,6 @@ exports[`only one article found with high similarity and choose for user 1`] = ` Object { "data": Object { "searchedText": "YouTube · 寻找健康人生", - "selectedArticleId": "AVvY-yizyCdS-nWhuYWx", "sessionId": 1497994017447, }, "replies": Array [ diff --git a/src/webhook/handlers/__tests__/__snapshots__/processMedia.test.js.snap b/src/webhook/handlers/__tests__/__snapshots__/processMedia.test.js.snap index 264e531f..515312e9 100644 --- a/src/webhook/handlers/__tests__/__snapshots__/processMedia.test.js.snap +++ b/src/webhook/handlers/__tests__/__snapshots__/processMedia.test.js.snap @@ -136,7 +136,6 @@ Object { "messageId": "6270464463537", "messageType": "image", "searchedText": "", - "selectedArticleId": "image-article-1", "sessionId": 1577836800000, }, },