diff --git a/src/types/chatbotState.ts b/src/types/chatbotState.ts index d35a74f6..84db9b1c 100644 --- a/src/types/chatbotState.ts +++ b/src/types/chatbotState.ts @@ -9,20 +9,31 @@ export type ChatbotState = | 'ASKING_ARTICLE_SUBMISSION_CONSENT' | 'Error'; +export type LegacyContext = { + data: { + /** Used to differientiate different search sessions (searched text or media) */ + sessionId: number; + } & ( + | { + /** Searched multi-media message that started this search session */ + messageId: MessageEvent['message']['id']; + messageType: Extract< + MessageEvent['message']['type'], + 'audio' | 'video' | 'image' + >; + } + | { + /** Searched text that started this search session */ + searchedText: string; + } + ); +}; + export type Context = { /** Used to differientiate different search sessions (searched text or media) */ sessionId: number; -} & ( - | { - /** Searched multi-media message that started this search session */ - messageId: MessageEvent['message']['id']; - messageType: MessageEvent['message']['type']; - } - | { - /** Searched text that started this search session */ - searchedText: string; - } -); + msgs: ReadonlyArray; +}; /** A single messages in the same co-occurrence */ export type CooccurredMessage = { @@ -41,8 +52,12 @@ export type CooccurredMessage = { } ); -export type ChatbotStateHandlerReturnType = { - data: Context; +/** Result of handler or processors */ +export type Result = { + /** The new context to set after processing the event */ + context: Context; + + /** The messages to send to the user as reply */ replies: Message[]; }; @@ -56,8 +71,8 @@ export type PostbackActionData = { }; export type ChatbotPostbackHandlerParams = { - /** Data stored in Chatbot context */ - data: Context; + /** Chatbot context */ + context: Context; /** Data in postback payload */ postbackData: PostbackActionData; userId: string; @@ -68,4 +83,4 @@ export type ChatbotPostbackHandlerParams = { */ export type ChatbotPostbackHandler = ( params: ChatbotPostbackHandlerParams -) => Promise; +) => Promise; diff --git a/src/types/result.ts b/src/types/result.ts deleted file mode 100644 index fbf8663f..00000000 --- a/src/types/result.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Message } from '@line/bot-sdk'; -import { Context } from './chatbotState'; - -export declare type Result = { - context: { - data: Partial; - }; - replies: Message[]; -}; diff --git a/src/webhook/handlers/__tests__/__snapshots__/askingArticleSubmissionConsent.test.ts.snap b/src/webhook/handlers/__tests__/__snapshots__/askingArticleSubmissionConsent.test.ts.snap index 960b03d3..f5b4fbaf 100644 --- a/src/webhook/handlers/__tests__/__snapshots__/askingArticleSubmissionConsent.test.ts.snap +++ b/src/webhook/handlers/__tests__/__snapshots__/askingArticleSubmissionConsent.test.ts.snap @@ -147,8 +147,14 @@ Array [ exports[`should submit article if user agrees to submit: has AI reply 1`] = ` Object { - "data": Object { - "searchedText": "Some text forwarded by the user", + "context": Object { + "msgs": Array [ + Object { + "id": "foo", + "text": "Some text forwarded by the user", + "type": "text", + }, + ], "sessionId": 1577923200000, }, "replies": Array [ @@ -325,8 +331,14 @@ Object { exports[`should submit article if user agrees to submit: has no AI reply 1`] = ` Object { - "data": Object { - "searchedText": "Some text forwarded by the user", + "context": Object { + "msgs": Array [ + Object { + "id": "foo", + "text": "Some text forwarded by the user", + "type": "text", + }, + ], "sessionId": 1577923200000, }, "replies": Array [ @@ -475,10 +487,13 @@ Object { exports[`should submit image article if user agrees to submit 1`] = ` Object { - "data": Object { - "messageId": "6530038889933", - "messageType": "image", - "searchedText": "", + "context": Object { + "msgs": Array [ + Object { + "id": "6530038889933", + "type": "image", + }, + ], "sessionId": 1577923200000, }, "replies": Array [ diff --git a/src/webhook/handlers/__tests__/__snapshots__/choosingArticle.test.ts.snap b/src/webhook/handlers/__tests__/__snapshots__/choosingArticle.test.ts.snap index 63df0a79..37388e31 100644 --- a/src/webhook/handlers/__tests__/__snapshots__/choosingArticle.test.ts.snap +++ b/src/webhook/handlers/__tests__/__snapshots__/choosingArticle.test.ts.snap @@ -2,8 +2,14 @@ exports[`should ask users if they want to submit article when user say not found 1`] = ` Object { - "data": Object { - "searchedText": "這一篇文章確實是一個轉傳文章,他夠長,看起來很轉傳,但是使用者覺得資料庫裡沒有。", + "context": Object { + "msgs": Array [ + Object { + "id": "foo", + "text": "這一篇文章確實是一個轉傳文章,他夠長,看起來很轉傳,但是使用者覺得資料庫裡沒有。", + "type": "text", + }, + ], "sessionId": 0, }, "replies": Array [ @@ -85,9 +91,13 @@ May I ask you a quick question?", exports[`should ask users if they want to submit image article when user say not found 1`] = ` Object { - "data": Object { - "messageId": "6530038889933", - "messageType": "image", + "context": Object { + "msgs": Array [ + Object { + "id": "6530038889933", + "type": "image", + }, + ], "sessionId": 0, }, "replies": Array [ @@ -198,8 +208,14 @@ Array [ 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", + "context": Object { + "msgs": Array [ + Object { + "id": "foo", + "text": "Just One Reply Just One Reply Just One Reply Just One Reply Just One Reply", + "type": "text", + }, + ], "sessionId": 0, }, "replies": Array [ @@ -325,8 +341,14 @@ https://dev.cofacts.tw/article/article-id", exports[`should select article and have OPINIONATED and NOT_ARTICLE replies 1`] = ` Object { - "data": Object { - "searchedText": "老榮民九成存款全部捐給慈濟,如今窮了卻得不到慈濟醫院社工的幫忙,竟翻臉不認人", + "context": Object { + "msgs": Array [ + Object { + "id": "foo", + "text": "老榮民九成存款全部捐給慈濟,如今窮了卻得不到慈濟醫院社工的幫忙,竟翻臉不認人", + "type": "text", + }, + ], "sessionId": 1497994017447, }, "replies": Array [ @@ -671,8 +693,14 @@ Object { exports[`should select article and slice replies when over 10 1`] = ` Object { - "data": Object { - "searchedText": "老榮民九成存款全部捐給慈濟,如今窮了卻得不到慈濟醫院社工的幫忙,竟翻臉不認人", + "context": Object { + "msgs": Array [ + Object { + "id": "foo", + "text": "老榮民九成存款全部捐給慈濟,如今窮了卻得不到慈濟醫院社工的幫忙,竟翻臉不認人", + "type": "text", + }, + ], "sessionId": 0, }, "replies": Array [ @@ -1490,13 +1518,19 @@ Object { exports[`should select article by articleId 1`] = ` Object { - "data": Object { - "searchedText": "《緊急通知》 + "context": Object { + "msgs": Array [ + Object { + "id": "foo", + "text": "《緊急通知》 台北馬偕醫院傳來訊息: 資深醫生(林清風)傳來:「請大家以後千萬不要再吃生魚片了!」 因為最近已經發現- 好多病人因為吃了生魚片,胃壁附著《海獸胃腺蟲》,大小隻不一定,有的病人甚至胃壁上滿滿都是無法夾出來,驅蟲藥也很難根治,罹患機率每個國家的人都一樣。 尤其;鮭魚的含蟲量最高、最可怕! 請傳給朋友,讓他們有所警惕!", + "type": "text", + }, + ], "sessionId": 0, }, "replies": Array [ @@ -1683,8 +1717,14 @@ Object { exports[`should select article with no replies: has AI reply 1`] = ` Object { - "data": Object { - "searchedText": "老司機車裡總備一塊香皂,知道內情的新手默默也準備了一塊", + "context": Object { + "msgs": Array [ + Object { + "id": "foo", + "text": "老司機車裡總備一塊香皂,知道內情的新手默默也準備了一塊", + "type": "text", + }, + ], "sessionId": 0, }, "replies": Array [ @@ -1862,8 +1902,14 @@ Don’t trust the message just yet!", exports[`should select article with no replies: has no AI reply 1`] = ` Object { - "data": Object { - "searchedText": "老司機車裡總備一塊香皂,知道內情的新手默默也準備了一塊", + "context": Object { + "msgs": Array [ + Object { + "id": "foo", + "text": "老司機車裡總備一塊香皂,知道內情的新手默默也準備了一塊", + "type": "text", + }, + ], "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 4d116b7f..5914c47b 100644 --- a/src/webhook/handlers/__tests__/__snapshots__/choosingReply.test.ts.snap +++ b/src/webhook/handlers/__tests__/__snapshots__/choosingReply.test.ts.snap @@ -243,8 +243,14 @@ Array [ exports[`should select reply by replyId should handle the case with just one reply 1`] = ` Object { - "data": Object { - "searchedText": "貼圖", + "context": Object { + "msgs": Array [ + Object { + "id": "foo", + "text": "貼圖", + "type": "text", + }, + ], "sessionId": 0, }, "replies": Array [ @@ -370,8 +376,14 @@ https://dev.cofacts.tw/article/AWDZYXxAyCdS-nWhumlz", exports[`should select reply by replyId should handle the case with multiple replies 1`] = ` Object { - "data": Object { - "searchedText": "貼圖", + "context": Object { + "msgs": Array [ + Object { + "id": "foo", + "text": "貼圖", + "type": "text", + }, + ], "sessionId": 0, }, "replies": Array [ diff --git a/src/webhook/handlers/__tests__/__snapshots__/initState.test.ts.snap b/src/webhook/handlers/__tests__/__snapshots__/initState.test.ts.snap index 34258911..37fb4946 100644 --- a/src/webhook/handlers/__tests__/__snapshots__/initState.test.ts.snap +++ b/src/webhook/handlers/__tests__/__snapshots__/initState.test.ts.snap @@ -2,8 +2,14 @@ exports[`article found 1`] = ` Object { - "data": Object { - "searchedText": "計程車上有裝悠遊卡感應器,老人悠悠卡可以享受優惠部分由政府補助,不影響司機收入", + "context": Object { + "msgs": Array [ + Object { + "id": "abc", + "text": "計程車上有裝悠遊卡感應器,老人悠悠卡可以享受優惠部分由政府補助,不影響司機收入", + "type": "text", + }, + ], "sessionId": 1497994017447, }, "replies": Array [ @@ -161,8 +167,14 @@ Please choose the version that looks the most similar👇", exports[`articles found with high similarity 1`] = ` Object { - "data": Object { - "searchedText": "YouTube · 寻找健康人生", + "context": Object { + "msgs": Array [ + Object { + "id": "abc", + "text": "YouTube · 寻找健康人生", + "type": "text", + }, + ], "sessionId": 1497994017447, }, "replies": Array [ @@ -348,8 +360,14 @@ Please choose the version that looks the most similar👇", exports[`input matches dialogflow intent search article when input length > 10 and intentDetectionConfidence != 1 1`] = ` Object { - "data": Object { - "searchedText": "零一二三四五六七八九十", + "context": Object { + "msgs": Array [ + Object { + "id": "abc", + "text": "零一二三四五六七八九十", + "type": "text", + }, + ], "sessionId": 1497994017447, }, "replies": Array [ @@ -431,8 +449,14 @@ May I ask you a quick question?", exports[`input matches dialogflow intent uses dialogflow response when input length < 10 1`] = ` Object { - "data": Object { - "searchedText": "你好", + "context": Object { + "msgs": Array [ + Object { + "id": "abc", + "text": "你好", + "type": "text", + }, + ], "sessionId": 1497994017447, }, "replies": Array [ @@ -446,8 +470,14 @@ Object { exports[`input matches dialogflow intent uses dialogflow response when input length > 10 and intentDetectionConfidence = 1 1`] = ` Object { - "data": Object { - "searchedText": "零一二三四五六七八九十", + "context": Object { + "msgs": Array [ + Object { + "id": "abc", + "text": "零一二三四五六七八九十", + "type": "text", + }, + ], "sessionId": 1497994017447, }, "replies": Array [ @@ -461,8 +491,14 @@ Object { exports[`only one article found with high similarity and choose for user 1`] = ` Object { - "data": Object { - "searchedText": "YouTube · 寻找健康人生", + "context": Object { + "msgs": Array [ + Object { + "id": "abc", + "text": "YouTube · 寻找健康人生", + "type": "text", + }, + ], "sessionId": 1497994017447, }, "replies": Array [ @@ -613,8 +649,14 @@ Don’t trust the message just yet!", exports[`should handle message matches only hyperlinks 1`] = ` Object { - "data": Object { - "searchedText": "YouTube · 寻找健康人生", + "context": Object { + "msgs": Array [ + Object { + "id": "abc", + "text": "YouTube · 寻找健康人生", + "type": "text", + }, + ], "sessionId": 1497994017447, }, "replies": Array [ @@ -920,8 +962,14 @@ title2 title2 title2 ", exports[`should handle text not found 1`] = ` Object { - "data": Object { - "searchedText": "YouTube · 寻找健康人生 驚!大批香蕉受到愛滋血污染!這種香蕉千萬不要吃!吃到可能會被 ...", + "context": Object { + "msgs": Array [ + Object { + "id": "abc", + "text": "YouTube · 寻找健康人生 驚!大批香蕉受到愛滋血污染!這種香蕉千萬不要吃!吃到可能會被 ...", + "type": "text", + }, + ], "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 672051ae..d1696f94 100644 --- a/src/webhook/handlers/__tests__/__snapshots__/processMedia.test.js.snap +++ b/src/webhook/handlers/__tests__/__snapshots__/processMedia.test.js.snap @@ -3,12 +3,13 @@ exports[`one article found (not identical) 1`] = ` Object { "context": Object { - "data": Object { - "messageId": "6270464463537", - "messageType": "image", - "searchedText": "", - "sessionId": 1577836800000, - }, + "msgs": Array [ + Object { + "id": "6270464463537", + "type": "image", + }, + ], + "sessionId": 1577836800000, }, "replies": Array [ Object { @@ -132,12 +133,13 @@ Please choose the version that looks the most similar👇", exports[`one identical article found and choose for user 1`] = ` Object { "context": Object { - "data": Object { - "messageId": "6270464463537", - "messageType": "image", - "searchedText": "", - "sessionId": 1577836800000, - }, + "msgs": Array [ + Object { + "id": "6270464463537", + "type": "image", + }, + ], + "sessionId": 1577836800000, }, "replies": Array [ Object { @@ -324,12 +326,13 @@ Object { exports[`one identical image and similar text found 1`] = ` Object { "context": Object { - "data": Object { - "messageId": "6270464463537", - "messageType": "image", - "searchedText": "", - "sessionId": 1577836800000, - }, + "msgs": Array [ + Object { + "id": "6270464463537", + "type": "image", + }, + ], + "sessionId": 1577836800000, }, "replies": Array [ Object { @@ -466,12 +469,13 @@ Please choose the version that looks the most similar👇", exports[`should handle image not found 1`] = ` Object { "context": Object { - "data": Object { - "messageId": "6530038889933", - "messageType": "image", - "searchedText": "", - "sessionId": 1577836800000, - }, + "msgs": Array [ + Object { + "id": "6530038889933", + "type": "image", + }, + ], + "sessionId": 1577836800000, }, "replies": Array [ Object { @@ -570,12 +574,13 @@ Do you want someone to fact-check this message?", exports[`twelve articles found 1`] = ` Object { "context": Object { - "data": Object { - "messageId": "6530038889933", - "messageType": "image", - "searchedText": "", - "sessionId": 1577836800000, - }, + "msgs": Array [ + Object { + "id": "6530038889933", + "type": "image", + }, + ], + "sessionId": 1577836800000, }, "replies": Array [ Object { diff --git a/src/webhook/handlers/__tests__/__snapshots__/tutorial.test.ts.snap b/src/webhook/handlers/__tests__/__snapshots__/tutorial.test.ts.snap index fd679420..c81b7363 100644 --- a/src/webhook/handlers/__tests__/__snapshots__/tutorial.test.ts.snap +++ b/src/webhook/handlers/__tests__/__snapshots__/tutorial.test.ts.snap @@ -257,8 +257,8 @@ Object { exports[`should handle EXPLAN_CHATBOT_FLOW_AND_PROVIDE_PERMISSION_SETUP 1`] = ` Object { - "data": Object { - "searchedText": "", + "context": Object { + "msgs": Array [], "sessionId": 1497994017447, }, "replies": Array [ @@ -354,8 +354,8 @@ Object { exports[`should handle PROVIDE_PERMISSION_SETUP 1`] = ` Object { - "data": Object { - "searchedText": "", + "context": Object { + "msgs": Array [], "sessionId": 1497994017447, }, "replies": Array [ @@ -445,8 +445,8 @@ Object { exports[`should handle PROVIDE_PERMISSION_SETUP_WITH_EXPLANATION 1`] = ` Object { - "data": Object { - "searchedText": "", + "context": Object { + "msgs": Array [], "sessionId": 1497994017447, }, "replies": Array [ @@ -528,8 +528,8 @@ You can still use Cofacts without granting me this permission. When we ask for f exports[`should handle RICH_MENU 1`] = ` Object { - "data": Object { - "searchedText": "", + "context": Object { + "msgs": Array [], "sessionId": 1497994017447, }, "replies": Array [ @@ -740,8 +740,8 @@ Object { exports[`should handle SETUP_DONE 1`] = ` Object { - "data": Object { - "searchedText": "", + "context": Object { + "msgs": Array [], "sessionId": 1497994017447, }, "replies": Array [ @@ -799,8 +799,8 @@ Object { exports[`should handle SETUP_LATER 1`] = ` Object { - "data": Object { - "searchedText": "", + "context": Object { + "msgs": Array [], "sessionId": 1497994017447, }, "replies": Array [ @@ -862,8 +862,8 @@ Object { exports[`should handle SIMULATE_FORWARDING_MESSAGE 1`] = ` Object { - "data": Object { - "searchedText": "", + "context": Object { + "msgs": Array [], "sessionId": 1497994017447, }, "replies": Array [ diff --git a/src/webhook/handlers/__tests__/askingArticleSource.test.ts b/src/webhook/handlers/__tests__/askingArticleSource.test.ts index 8399bdb9..8917d7fb 100644 --- a/src/webhook/handlers/__tests__/askingArticleSource.test.ts +++ b/src/webhook/handlers/__tests__/askingArticleSource.test.ts @@ -14,7 +14,7 @@ beforeEach(() => { it('throws on incorrect input', async () => { const incorrectParam: ChatbotPostbackHandlerParams = { - data: { sessionId: 0, searchedText: 'foo' }, + context: { sessionId: 0, msgs: [{ id: 'foo', type: 'text', text: 'foo' }] }, postbackData: { sessionId: 0, state: 'ASKING_ARTICLE_SOURCE', @@ -30,7 +30,7 @@ it('throws on incorrect input', async () => { it('returns instructions if user did not forward the whole message', async () => { const didNotForwardParam: ChatbotPostbackHandlerParams = { - data: { searchedText: 'foo', sessionId: 0 }, + context: { sessionId: 0, msgs: [{ id: 'foo', type: 'text', text: 'foo' }] }, postbackData: { sessionId: 0, state: 'ASKING_ARTICLE_SOURCE', @@ -212,7 +212,7 @@ it('returns instructions if user did not forward the whole message', async () => it('sends user submission consent if user forwarded the whole message', async () => { const didForwardParam: ChatbotPostbackHandlerParams = { - data: { searchedText: 'foo', sessionId: 0 }, + context: { sessionId: 0, msgs: [{ id: 'foo', type: 'text', text: 'foo' }] }, postbackData: { sessionId: 0, state: 'ASKING_ARTICLE_SOURCE', diff --git a/src/webhook/handlers/__tests__/askingArticleSubmissionConsent.test.ts b/src/webhook/handlers/__tests__/askingArticleSubmissionConsent.test.ts index 4f79ff15..5fedeef2 100644 --- a/src/webhook/handlers/__tests__/askingArticleSubmissionConsent.test.ts +++ b/src/webhook/handlers/__tests__/askingArticleSubmissionConsent.test.ts @@ -28,7 +28,7 @@ beforeEach(() => { it('throws on incorrect input', async () => { const incorrectParam: ChatbotPostbackHandlerParams = { - data: { sessionId: 0, searchedText: 'foo' }, + context: { sessionId: 0, msgs: [] }, postbackData: { sessionId: 0, state: 'ASKING_ARTICLE_SUBMISSION_CONSENT', @@ -47,9 +47,11 @@ it('throws on incorrect input', async () => { it('should thank the user if user does not agree to submit', async () => { const inputSession = new Date('2020-01-01T18:10:18.314Z').getTime(); const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: inputSession, - searchedText: 'Some text forwarded by the user', + msgs: [ + { id: 'foo', type: 'text', text: 'Some text forwarded by the user' }, + ], }, postbackData: { sessionId: inputSession, @@ -87,9 +89,11 @@ it('should thank the user if user does not agree to submit', async () => { it('should submit article if user agrees to submit', async () => { const inputSession = new Date('2020-01-01T18:10:18.314Z').getTime(); const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: inputSession, - searchedText: 'Some text forwarded by the user', + msgs: [ + { id: 'foo', type: 'text', text: 'Some text forwarded by the user' }, + ], }, postbackData: { sessionId: inputSession, @@ -108,7 +112,7 @@ it('should submit article if user agrees to submit', async () => { expect(gql.__finished()).toBe(true); expect(result).toMatchSnapshot('has AI reply'); - expect(result.data.sessionId).not.toEqual(inputSession); + expect(result.context.sessionId).not.toEqual(inputSession); expect(ga.eventMock.mock.calls).toMatchInlineSnapshot(` Array [ Array [ @@ -137,11 +141,9 @@ it('should submit article if user agrees to submit', async () => { it('should submit image article if user agrees to submit', async () => { const inputSession = new Date('2020-01-01T18:10:18.314Z').getTime(); const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: inputSession, - searchedText: '', - messageId: '6530038889933', - messageType: 'image', + msgs: [{ id: '6530038889933', type: 'image' }], }, postbackData: { sessionId: inputSession, @@ -158,7 +160,7 @@ it('should submit image article if user agrees to submit', async () => { expect(gql.__finished()).toBe(true); expect(result).toMatchSnapshot(); - expect(result.data.sessionId).not.toEqual(inputSession); + expect(result.context.sessionId).not.toEqual(inputSession); expect(ga.eventMock.mock.calls).toMatchInlineSnapshot(` Array [ Array [ @@ -176,9 +178,11 @@ it('should submit image article if user agrees to submit', async () => { it('should create a UserArticleLink when creating a Article', async () => { const userId = 'user-id-0'; const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 0, - searchedText: 'Some text forwarded by the user', + msgs: [ + { id: 'foo', type: 'text', text: 'Some text forwarded by the user' }, + ], }, postbackData: { sessionId: 0, @@ -201,9 +205,11 @@ it('should create a UserArticleLink when creating a Article', async () => { it('should ask user to turn on notification settings if they did not turn it on after creating an Article', async () => { const userId = 'user-id-0'; const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 0, - searchedText: 'Some text forwarded by the user', + msgs: [ + { id: 'foo', type: 'text', text: 'Some text forwarded by the user' }, + ], }, postbackData: { sessionId: 0, diff --git a/src/webhook/handlers/__tests__/choosingArticle.test.ts b/src/webhook/handlers/__tests__/choosingArticle.test.ts index 1402a309..d39e10a7 100644 --- a/src/webhook/handlers/__tests__/choosingArticle.test.ts +++ b/src/webhook/handlers/__tests__/choosingArticle.test.ts @@ -32,10 +32,15 @@ it('should select article by articleId', async () => { gql.__push(apiGetArticleResult.selectedArticleId); const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 0, - searchedText: - '《緊急通知》\n台北馬偕醫院傳來訊息:\n資深醫生(林清風)傳來:「請大家以後千萬不要再吃生魚片了!」\n因為最近已經發現- 好多病人因為吃了生魚片,胃壁附著《海獸胃腺蟲》,大小隻不一定,有的病人甚至胃壁上滿滿都是無法夾出來,驅蟲藥也很難根治,罹患機率每個國家的人都一樣。\n尤其;鮭魚的含蟲量最高、最可怕!\n請傳給朋友,讓他們有所警惕!', + msgs: [ + { + id: 'foo', + type: 'text', + text: '《緊急通知》\n台北馬偕醫院傳來訊息:\n資深醫生(林清風)傳來:「請大家以後千萬不要再吃生魚片了!」\n因為最近已經發現- 好多病人因為吃了生魚片,胃壁附著《海獸胃腺蟲》,大小隻不一定,有的病人甚至胃壁上滿滿都是無法夾出來,驅蟲藥也很難根治,罹患機率每個國家的人都一樣。\n尤其;鮭魚的含蟲量最高、最可怕!\n請傳給朋友,讓他們有所警惕!', + }, + ], }, postbackData: { sessionId: 0, @@ -81,7 +86,7 @@ it('throws ManipulationError when articleId is not valid', async () => { gql.__push({ data: { GetArticle: null } }); const params: ChatbotPostbackHandlerParams = { - data: { sessionId: 0, searchedText: '' }, + context: { sessionId: 0, msgs: [{ type: 'text', text: 'foo', id: 'id' }] }, postbackData: { sessionId: 0, state: 'CHOOSING_ARTICLE', @@ -100,10 +105,15 @@ it('should select article and have OPINIONATED and NOT_ARTICLE replies', async ( gql.__push(apiGetArticleResult.multipleReplies); const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 1497994017447, - searchedText: - '老榮民九成存款全部捐給慈濟,如今窮了卻得不到慈濟醫院社工的幫忙,竟翻臉不認人', + msgs: [ + { + id: 'foo', + type: 'text', + text: '老榮民九成存款全部捐給慈濟,如今窮了卻得不到慈濟醫院社工的幫忙,竟翻臉不認人', + }, + ], }, postbackData: { input: 'article-id', @@ -168,9 +178,15 @@ it('should select article with no replies', async () => { gql.__push(apiGetArticleResult.createOrUpdateReplyRequest); const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 0, - searchedText: '老司機車裡總備一塊香皂,知道內情的新手默默也準備了一塊', + msgs: [ + { + id: 'foo', + type: 'text', + text: '老司機車裡總備一塊香皂,知道內情的新手默默也準備了一塊', + }, + ], }, postbackData: { input: 'article-id', @@ -223,10 +239,15 @@ it('should select article and choose the only one reply for user', async () => { gql.__push(apiGetReplyResult.oneReply2); const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 0, - searchedText: - 'Just One Reply Just One Reply Just One Reply Just One Reply Just One Reply', + msgs: [ + { + id: 'foo', + type: 'text', + text: 'Just One Reply Just One Reply Just One Reply Just One Reply Just One Reply', + }, + ], }, postbackData: { input: 'article-id', @@ -269,10 +290,15 @@ it('should select article and choose the only one reply for user', async () => { it('should block incorrect interactions', async () => { const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 0, - searchedText: - 'Just One Reply Just One Reply Just One Reply Just One Reply Just One Reply', + msgs: [ + { + id: 'foo', + type: 'text', + text: 'Just One Reply Just One Reply Just One Reply Just One Reply Just One Reply', + }, + ], }, postbackData: { sessionId: 0, @@ -291,10 +317,15 @@ it('should select article and slice replies when over 10', async () => { gql.__push(apiGetArticleResult.elevenReplies); const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 0, - searchedText: - '老榮民九成存款全部捐給慈濟,如今窮了卻得不到慈濟醫院社工的幫忙,竟翻臉不認人', + msgs: [ + { + id: 'foo', + type: 'text', + text: '老榮民九成存款全部捐給慈濟,如今窮了卻得不到慈濟醫院社工的幫忙,竟翻臉不認人', + }, + ], }, postbackData: { input: 'article-id', @@ -310,10 +341,15 @@ it('should select article and slice replies when over 10', async () => { it('should ask users if they want to submit article when user say not found', async () => { const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 0, - searchedText: - '這一篇文章確實是一個轉傳文章,他夠長,看起來很轉傳,但是使用者覺得資料庫裡沒有。', + msgs: [ + { + id: 'foo', + type: 'text', + text: '這一篇文章確實是一個轉傳文章,他夠長,看起來很轉傳,但是使用者覺得資料庫裡沒有。', + }, + ], }, postbackData: { input: POSTBACK_NO_ARTICLE_FOUND, @@ -344,10 +380,14 @@ it('should ask users if they want to submit article when user say not found', as it('should ask users if they want to submit image article when user say not found', async () => { const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 0, - messageType: 'image', - messageId: '6530038889933', + msgs: [ + { + id: '6530038889933', + type: 'image', + }, + ], }, postbackData: { input: POSTBACK_NO_ARTICLE_FOUND, @@ -379,9 +419,15 @@ it('should ask users if they want to submit image article when user say not foun it('should create a UserArticleLink when selecting a article', async () => { const userId = 'user-id-0'; const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 0, - searchedText: '《緊急通知》', + msgs: [ + { + id: 'foo', + type: 'text', + text: '《緊急通知》', + }, + ], }, postbackData: { input: 'article-id', diff --git a/src/webhook/handlers/__tests__/choosingReply.test.ts b/src/webhook/handlers/__tests__/choosingReply.test.ts index 60372b96..b1b17b1c 100644 --- a/src/webhook/handlers/__tests__/choosingReply.test.ts +++ b/src/webhook/handlers/__tests__/choosingReply.test.ts @@ -31,9 +31,9 @@ describe('should select reply by replyId', () => { }; const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 0, - searchedText: '貼圖', + msgs: [{ id: 'foo', type: 'text', text: '貼圖' }], }, postbackData: { sessionId: 0, @@ -101,9 +101,9 @@ describe('should select reply by replyId', () => { it('should block invalid postback input', async () => { const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 0, - searchedText: '貼圖', + msgs: [{ id: 'foo', type: 'text', text: '貼圖' }], }, postbackData: { sessionId: 0, @@ -126,9 +126,9 @@ it('should handle graphql error gracefully', async () => { }; const params: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 0, - searchedText: '貼圖', + msgs: [{ id: 'foo', type: 'text', text: '貼圖' }], }, postbackData: { sessionId: 0, diff --git a/src/webhook/handlers/__tests__/handlePostback.test.ts b/src/webhook/handlers/__tests__/handlePostback.test.ts index ceb6bda8..7abb981b 100644 --- a/src/webhook/handlers/__tests__/handlePostback.test.ts +++ b/src/webhook/handlers/__tests__/handlePostback.test.ts @@ -7,7 +7,7 @@ import originalAskingArticleSource from '../askingArticleSource'; import originalAskingArticleSubmissionConsent from '../askingArticleSubmissionConsent'; import originalTutorial from '../tutorial'; import originalDefaultState from '../defaultState'; -import { ChatbotStateHandlerReturnType, Context } from 'src/types/chatbotState'; +import { Result, Context } from 'src/types/chatbotState'; jest.mock('../choosingArticle'); jest.mock('../choosingReply'); @@ -56,9 +56,9 @@ afterEach(() => { }); it('invokes state handler specified by event.postbackHandlerState', async () => { - const data: Context = { + const context: Context = { sessionId: FIXED_DATE, - searchedText: '', + msgs: [], }; for (const { postbackState, expectedHandler } of [ @@ -82,13 +82,13 @@ it('invokes state handler specified by event.postbackHandlerState', async () => ] as const) { expectedHandler.mockImplementationOnce(() => { return Promise.resolve({ - data: { sessionId: 0, searchedText: '' }, + context: { sessionId: 0, msgs: [] }, replies: [], - } as ChatbotStateHandlerReturnType); + } as Result); }); await handlePostback( - data, + context, { sessionId: FIXED_DATE, state: postbackState, @@ -105,17 +105,17 @@ it('invokes state handler specified by event.postbackHandlerState', async () => describe('defaultState', () => { it('handles unimplemented state', async () => { - const data: Context = { sessionId: FIXED_DATE, searchedText: '' }; + const context: Context = { sessionId: FIXED_DATE, msgs: [] }; defaultState.mockImplementationOnce(() => { return { - data: { sessionId: 0, searchedText: '' }, + context: { sessionId: 0, msgs: [] }, replies: [], }; }); await expect( handlePostback( - data, + context, { sessionId: FIXED_DATE, input: 'foo', @@ -126,10 +126,8 @@ describe('defaultState', () => { ).resolves.toMatchInlineSnapshot(` Object { "context": Object { - "data": Object { - "searchedText": "", - "sessionId": 0, - }, + "msgs": Array [], + "sessionId": 0, }, "replies": Array [], } @@ -139,7 +137,7 @@ describe('defaultState', () => { }); it('handles ManipulationError fired in handlers', async () => { - const data: Context = { sessionId: FIXED_DATE, searchedText: '' }; + const context: Context = { sessionId: FIXED_DATE, msgs: [] }; choosingArticle.mockImplementationOnce(() => Promise.reject(new ManipulationError('Foo error')) @@ -147,7 +145,7 @@ it('handles ManipulationError fired in handlers', async () => { await expect( handlePostback( - data, + context, { sessionId: FIXED_DATE, state: 'CHOOSING_ARTICLE', @@ -158,10 +156,8 @@ it('handles ManipulationError fired in handlers', async () => { ).resolves.toMatchInlineSnapshot(` Object { "context": Object { - "data": Object { - "searchedText": "", - "sessionId": 612964800000, - }, + "msgs": Array [], + "sessionId": 612964800000, }, "replies": Array [ Object { @@ -205,14 +201,14 @@ it('handles ManipulationError fired in handlers', async () => { }); it('throws on unknown error', async () => { - const data: Context = { sessionId: FIXED_DATE, searchedText: '' }; + const context: Context = { sessionId: FIXED_DATE, msgs: [] }; choosingArticle.mockImplementationOnce(() => Promise.reject(new Error('Unknown error')) ); await expect( handlePostback( - data, + context, { sessionId: FIXED_DATE, state: 'CHOOSING_ARTICLE', input: '' }, 'user-id' ) @@ -223,12 +219,12 @@ describe('tutorial', () => { it('handles TUTORIAL postbackHandlerState', async () => { const context: Context = { sessionId: FIXED_DATE, - searchedText: '', + msgs: [], }; tutorial.mockImplementationOnce(() => { return { - data: { sessionId: 0, searchedText: '' }, + context: { sessionId: 0, msgs: [] }, replies: [], }; }); @@ -242,10 +238,8 @@ describe('tutorial', () => { ).resolves.toMatchInlineSnapshot(` Object { "context": Object { - "data": Object { - "searchedText": "", - "sessionId": 0, - }, + "msgs": Array [], + "sessionId": 0, }, "replies": Array [], } diff --git a/src/webhook/handlers/__tests__/initState.test.ts b/src/webhook/handlers/__tests__/initState.test.ts index 481b1fb6..6a0b373a 100644 --- a/src/webhook/handlers/__tests__/initState.test.ts +++ b/src/webhook/handlers/__tests__/initState.test.ts @@ -31,10 +31,15 @@ it('article found', async () => { expect( await initState({ - data: { + context: { sessionId: 1497994017447, - searchedText: - '計程車上有裝悠遊卡感應器,老人悠悠卡可以享受優惠部分由政府補助,不影響司機收入', + msgs: [ + { + type: 'text', + id: 'abc', + text: '計程車上有裝悠遊卡感應器,老人悠悠卡可以享受優惠部分由政府補助,不影響司機收入', + }, + ], }, userId: 'Uc76d8ae9ccd1ada4f06c4e1515d46466', }) @@ -73,10 +78,15 @@ it('long article replies still below flex message limit', async () => { gql.__push(apiListArticleResult.twelveLongArticles); const result = await initState({ - data: { + context: { sessionId: 1502477506267, - searchedText: - '這樣的大事國內媒體竟然不敢報導!\n我國駐日代表將原「中華民國」申請更名為「台灣」結果被日本裁罰,須繳納7000萬日圓(合約台幣2100萬元)高額稅賦(轉載中時電子報)\n\n我駐日代表謝長廷將原「中華民國」申請更名為「台灣」,自認得意之時,結果遭自認友好日本國給出賣了,必須繳納7000萬日圓(合約台幣2100萬元)高額稅賦...民進黨沒想到如此更名竟然是這樣的下場:被他最信任也最友好的日本政府給坑了。\n果然錯誤的政策比貪污可怕,2100萬就這樣打水漂了,還要資助九州水患,核四停建違約賠償金.......夠全國軍公教退休2次.........\n\nhttp://www.chinatimes.com/newspapers/20170617000318-260118', + msgs: [ + { + type: 'text', + id: 'abc', + text: '這樣的大事國內媒體竟然不敢報導!\n我國駐日代表將原「中華民國」申請更名為「台灣」結果被日本裁罰,須繳納7000萬日圓(合約台幣2100萬元)高額稅賦(轉載中時電子報)\n\n我駐日代表謝長廷將原「中華民國」申請更名為「台灣」,自認得意之時,結果遭自認友好日本國給出賣了,必須繳納7000萬日圓(合約台幣2100萬元)高額稅賦...民進黨沒想到如此更名竟然是這樣的下場:被他最信任也最友好的日本政府給坑了。\n果然錯誤的政策比貪污可怕,2100萬就這樣打水漂了,還要資助九州水患,核四停建違約賠償金.......夠全國軍公教退休2次.........\n\nhttp://www.chinatimes.com/newspapers/20170617000318-260118', + }, + ], }, userId: 'Uc76d8ae9ccd1ada4f06c4e1515d46466', }); @@ -106,9 +116,9 @@ it('articles found with high similarity', async () => { expect( await initState({ - data: { + context: { sessionId: 1497994017447, - searchedText: 'YouTube · 寻找健康人生', + msgs: [{ type: 'text', id: 'abc', text: 'YouTube · 寻找健康人生' }], }, userId: 'Uc76d8ae9ccd1ada4f06c4e1515d46466', }) @@ -157,9 +167,9 @@ it('only one article found with high similarity and choose for user', async () = expect( await initState({ - data: { + context: { sessionId: 1497994017447, - searchedText: 'YouTube · 寻找健康人生', + msgs: [{ type: 'text', id: 'abc', text: 'YouTube · 寻找健康人生' }], }, userId: 'Uc76d8ae9ccd1ada4f06c4e1515d46466', }) @@ -214,9 +224,9 @@ it('should handle message matches only hyperlinks', async () => { expect( await initState({ - data: { + context: { sessionId: 1497994017447, - searchedText: 'YouTube · 寻找健康人生', + msgs: [{ type: 'text', id: 'abc', text: 'YouTube · 寻找健康人生' }], }, userId: 'Uc76d8ae9ccd1ada4f06c4e1515d46466', }) @@ -265,10 +275,15 @@ it('should handle text not found', async () => { MockDate.set('2020-01-01'); expect( await initState({ - data: { + context: { sessionId: 1497994017447, - searchedText: - 'YouTube · 寻找健康人生 驚!大批香蕉受到愛滋血污染!這種香蕉千萬不要吃!吃到可能會被 ...', + msgs: [ + { + type: 'text', + id: 'abc', + text: 'YouTube · 寻找健康人生 驚!大批香蕉受到愛滋血污染!這種香蕉千萬不要吃!吃到可能會被 ...', + }, + ], }, userId: 'Uc76d8ae9ccd1ada4f06c4e1515d46466', }) @@ -313,9 +328,9 @@ describe('input matches dialogflow intent', () => { expect( await initState({ - data: { + context: { sessionId: 1497994017447, - searchedText: '你好', + msgs: [{ type: 'text', id: 'abc', text: '你好' }], }, userId: 'Uc76d8ae9ccd1ada4f06c4e1515d46466', }) @@ -360,9 +375,9 @@ describe('input matches dialogflow intent', () => { expect( await initState({ - data: { + context: { sessionId: 1497994017447, - searchedText: '零一二三四五六七八九十', + msgs: [{ type: 'text', id: 'abc', text: '零一二三四五六七八九十' }], }, userId: 'Uc76d8ae9ccd1ada4f06c4e1515d46466', }) @@ -408,9 +423,9 @@ describe('input matches dialogflow intent', () => { MockDate.set('2020-01-01'); expect( await initState({ - data: { + context: { sessionId: 1497994017447, - searchedText: '零一二三四五六七八九十', + msgs: [{ type: 'text', id: 'abc', text: '零一二三四五六七八九十' }], }, userId: 'Uc76d8ae9ccd1ada4f06c4e1515d46466', }) diff --git a/src/webhook/handlers/__tests__/processMedia.test.js b/src/webhook/handlers/__tests__/processMedia.test.js index 9b092015..f23cf80e 100644 --- a/src/webhook/handlers/__tests__/processMedia.test.js +++ b/src/webhook/handlers/__tests__/processMedia.test.js @@ -18,17 +18,13 @@ it('one identical article found and choose for user', async () => { gql.__push(apiListArticlesResult.oneIdenticalImageArticle); gql.__push(apiGetArticleResult.oneImageArticle); - const event = { - type: 'message', - timestamp: 1497994016356, - message: { - type: 'image', - id: '6270464463537', - }, + const msg = { + type: 'image', + id: '6270464463537', }; const userId = 'Uc76d8ae9ccd1ada4f06c4e1515d46466'; MockDate.set('2020-01-01'); - expect(await processMedia(event, userId)).toMatchSnapshot(); + expect(await processMedia(msg, userId)).toMatchSnapshot(); MockDate.reset(); expect(gql.__finished()).toBe(true); expect(ga.eventMock.mock.calls).toMatchInlineSnapshot(` @@ -86,17 +82,13 @@ it('one identical article found and choose for user', async () => { it('one identical image and similar text found', async () => { gql.__push(apiListArticlesResult.identicalImageAndTextFound); - const event = { - type: 'message', - timestamp: 1497994016356, - message: { - type: 'image', - id: '6270464463537', - }, + const msg = { + type: 'image', + id: '6270464463537', }; const userId = 'Uc76d8ae9ccd1ada4f06c4e1515d46466'; MockDate.set('2020-01-01'); - expect(await processMedia(event, userId)).toMatchSnapshot(); + expect(await processMedia(msg, userId)).toMatchSnapshot(); MockDate.reset(); expect(gql.__finished()).toBe(true); }); @@ -104,17 +96,13 @@ it('one identical image and similar text found', async () => { it('one article found (not identical)', async () => { gql.__push(apiListArticlesResult.oneImageArticle); - const event = { - type: 'message', - timestamp: 1497994016356, - message: { - type: 'image', - id: '6270464463537', - }, + const msg = { + type: 'image', + id: '6270464463537', }; const userId = 'Uc76d8ae9ccd1ada4f06c4e1515d46466'; MockDate.set('2020-01-01'); - expect(await processMedia(event, userId)).toMatchSnapshot(); + expect(await processMedia(msg, userId)).toMatchSnapshot(); MockDate.reset(); expect(gql.__finished()).toBe(true); expect(ga.eventMock.mock.calls).toMatchInlineSnapshot(` @@ -149,18 +137,14 @@ it('one article found (not identical)', async () => { it('twelve articles found', async () => { gql.__push(apiListArticlesResult.twelveImageArticles); - const event = { - type: 'message', - timestamp: 1497994016356, - message: { - type: 'image', - id: '6530038889933', - }, + const msg = { + type: 'image', + id: '6530038889933', }; const userId = 'Uc76d8ae9ccd1ada4f06c4e1515d46466'; MockDate.set('2020-01-01'); - const result = await processMedia(event, userId); + const result = await processMedia(msg, userId); MockDate.reset(); expect(result).toMatchSnapshot(); expect(gql.__finished()).toBe(true); @@ -173,17 +157,13 @@ it('twelve articles found', async () => { it('should handle image not found', async () => { gql.__push(apiListArticlesResult.notFound); - const event = { - type: 'message', - timestamp: 1497994016356, - message: { - type: 'image', - id: '6530038889933', - }, + const msg = { + type: 'image', + id: '6530038889933', }; const userId = 'Uc76d8ae9ccd1ada4f06c4e1515d46466'; MockDate.set('2020-01-01'); - expect(await processMedia(event, userId)).toMatchSnapshot(); + expect(await processMedia(msg, userId)).toMatchSnapshot(); MockDate.reset(); expect(gql.__finished()).toBe(true); expect(ga.eventMock.mock.calls).toMatchInlineSnapshot(` diff --git a/src/webhook/handlers/__tests__/singleUserHandler.test.ts b/src/webhook/handlers/__tests__/singleUserHandler.test.ts index d82f8adf..792ac0d9 100644 --- a/src/webhook/handlers/__tests__/singleUserHandler.test.ts +++ b/src/webhook/handlers/__tests__/singleUserHandler.test.ts @@ -12,7 +12,7 @@ import originalHandlePostback from '../handlePostback'; import { TUTORIAL_STEPS } from '../tutorial'; import { MessageEvent, PostbackEvent, TextEventMessage } from '@line/bot-sdk'; -import { Context } from 'src/types/chatbotState'; +import { LegacyContext } from 'src/types/chatbotState'; jest.mock('src/webhook/lineClient'); jest.mock('src/lib/ga'); @@ -158,11 +158,11 @@ it('ignores sticker events', async () => { expect(lineClient.post.mock.calls).toMatchInlineSnapshot(`Array []`); }); -it('handles postbacks', async () => { +it('handles postbacks w/ LegacyContext', async () => { const sessionId = 123; redisGet.mockImplementationOnce( - (): Promise<{ data: Context }> => + (): Promise => Promise.resolve({ data: { sessionId, searchedText: '' }, }) @@ -185,9 +185,9 @@ it('handles postbacks', async () => { replyToken: '', }; - handlePostback.mockImplementationOnce((data) => { + handlePostback.mockImplementationOnce((context) => { return Promise.resolve({ - context: { data }, + context, replies: [ { type: 'text', @@ -205,7 +205,13 @@ it('handles postbacks', async () => { Array [ Array [ Object { - "searchedText": "", + "msgs": Array [ + Object { + "id": "123", + "text": "", + "type": "text", + }, + ], "sessionId": 123, }, Object { @@ -305,9 +311,9 @@ it('forwards to CHOOSING_ARTICLE when VIEW_ARTICLE_PREFIX is sent', async () => `${VIEW_ARTICLE_PREFIX}${getArticleURL('article-id')}` ); - handlePostback.mockImplementationOnce((data) => { + handlePostback.mockImplementationOnce((context) => { return Promise.resolve({ - context: { data }, + context, replies: [ { type: 'text', @@ -327,7 +333,7 @@ it('forwards to CHOOSING_ARTICLE when VIEW_ARTICLE_PREFIX is sent', async () => Array [ Array [ Object { - "searchedText": "", + "msgs": Array [], "sessionId": 1561982400000, }, Object { @@ -364,9 +370,9 @@ it('shows reply list when article URL is sent', async () => { getArticleURL('article-id') + ' \n ' /* simulate manual input */ ); - handlePostback.mockImplementationOnce((data) => { + handlePostback.mockImplementationOnce((context) => { return Promise.resolve({ - context: { data }, + context, replies: [ { type: 'text', @@ -386,7 +392,7 @@ it('shows reply list when article URL is sent', async () => { Array [ Array [ Object { - "searchedText": "", + "msgs": Array [], "sessionId": 1561982400000, }, Object { @@ -422,9 +428,9 @@ it('Resets session on free-form input, triggers fast-forward', async () => { const input = 'Newly forwarded message'; const event = createTextMessageEvent(input); - initState.mockImplementationOnce(({ data }) => { + initState.mockImplementationOnce(({ context }) => { return Promise.resolve({ - data, + context, replies: [ { type: 'text', @@ -463,8 +469,14 @@ it('Resets session on free-form input, triggers fast-forward', async () => { Array [ Array [ Object { - "data": Object { - "searchedText": "Newly forwarded message", + "context": Object { + "msgs": Array [ + Object { + "id": "TmV3bHkgZm9yd2FyZGVkIG1lc3NhZ2U=", + "text": "Newly forwarded message", + "type": "text", + }, + ], "sessionId": 1561982400000, }, "userId": "user-id", @@ -495,9 +507,9 @@ it('Resets session on free-form input, triggers fast-forward', async () => { it('handles tutorial trigger from rich menu', async () => { const event = createTextMessageEvent(TUTORIAL_STEPS['RICH_MENU']); - handlePostback.mockImplementationOnce((data) => { + handlePostback.mockImplementationOnce((context) => { return Promise.resolve({ - context: { data }, + context, replies: [ { type: 'text', @@ -516,7 +528,7 @@ it('handles tutorial trigger from rich menu', async () => { Array [ Array [ Object { - "searchedText": "", + "msgs": Array [], "sessionId": 1561982400000, }, Object { diff --git a/src/webhook/handlers/__tests__/tutorial.test.ts b/src/webhook/handlers/__tests__/tutorial.test.ts index 83117cac..cd719cca 100644 --- a/src/webhook/handlers/__tests__/tutorial.test.ts +++ b/src/webhook/handlers/__tests__/tutorial.test.ts @@ -12,9 +12,9 @@ import { ChatbotPostbackHandlerParams } from 'src/types/chatbotState'; const ga = originalGa as MockedGa; const param: ChatbotPostbackHandlerParams = { - data: { + context: { sessionId: 1497994017447, - searchedText: '', + msgs: [], }, postbackData: { sessionId: 1497994017447, @@ -242,6 +242,6 @@ it('createGreetingMessage()', () => { }); it('createTutorialMessage()', () => { - const result = createTutorialMessage(param.data.sessionId); + const result = createTutorialMessage(param.context.sessionId); expect(result).toMatchSnapshot(); }); diff --git a/src/webhook/handlers/askingArticleSource.ts b/src/webhook/handlers/askingArticleSource.ts index 5bfca2d7..2ae3f233 100644 --- a/src/webhook/handlers/askingArticleSource.ts +++ b/src/webhook/handlers/askingArticleSource.ts @@ -22,7 +22,7 @@ const inputSchema = z.enum([POSTBACK_NO, POSTBACK_YES]); export type Input = z.infer; const askingArticleSource: ChatbotPostbackHandler = async ({ - data, + context, postbackData: { state, input: postbackInput }, userId, }) => { @@ -36,11 +36,13 @@ const askingArticleSource: ChatbotPostbackHandler = async ({ let replies: Message[] = []; - const visitor = ga( - userId, - state, - 'searchedText' in data ? data.searchedText : data.messageId - ); + const firstMsg = context.msgs[0]; + // istanbul ignore if + if (!firstMsg || firstMsg.type !== 'text') { + throw new Error('No message found in context'); // Never happens + } + + const visitor = ga(userId, state, firstMsg.text); switch (input) { default: { @@ -113,7 +115,7 @@ const askingArticleSource: ChatbotPostbackHandler = async ({ t`See Tutorial`, TUTORIAL_STEPS.RICH_MENU, TUTORIAL_STEPS.RICH_MENU, - data.sessionId, + context.sessionId, 'TUTORIAL' ), style: 'primary', @@ -185,7 +187,7 @@ const askingArticleSource: ChatbotPostbackHandler = async ({ createTextMessage({ text: t`Do you want someone to fact-check this message?`, }), - createAskArticleSubmissionConsentReply(data.sessionId), + createAskArticleSubmissionConsentReply(context.sessionId), ]; visitor.event({ ec: 'UserInput', @@ -196,7 +198,7 @@ const askingArticleSource: ChatbotPostbackHandler = async ({ visitor.send(); - return { data, replies }; + return { context, replies }; }; export default askingArticleSource; diff --git a/src/webhook/handlers/askingArticleSubmissionConsent.ts b/src/webhook/handlers/askingArticleSubmissionConsent.ts index 66aa388e..18f1e4cb 100644 --- a/src/webhook/handlers/askingArticleSubmissionConsent.ts +++ b/src/webhook/handlers/askingArticleSubmissionConsent.ts @@ -38,7 +38,7 @@ function uppercase(s: T) { } const askingArticleSubmissionConsent: ChatbotPostbackHandler = async ({ - data, + context, postbackData: { state, input: postbackInput }, userId, }) => { @@ -50,10 +50,16 @@ const askingArticleSubmissionConsent: ChatbotPostbackHandler = async ({ throw new ManipulationError(t`Please choose from provided options.`); } + const firstMsg = context.msgs[0]; + // istanbul ignore if + if (!firstMsg) { + throw new ManipulationError('No message found in context'); // Should never happen + } + const visitor = ga( userId, state, - 'searchedText' in data ? data.searchedText : data.messageId + firstMsg.type === 'text' ? firstMsg.text : firstMsg.id ); let replies: Message[] = []; @@ -74,9 +80,8 @@ const askingArticleSubmissionConsent: ChatbotPostbackHandler = async ({ case POSTBACK_YES: { visitor.event({ ec: 'Article', ea: 'Create', el: 'Yes' }); - const isTextArticle = 'searchedText' in data && !('messageId' in data); let article; - if (isTextArticle) { + if (firstMsg.type === 'text') { const result = await gql` mutation SubmitTextArticleUnderConsent($text: String!) { CreateArticle(text: $text, reference: { type: LINE }) { @@ -86,23 +91,12 @@ const askingArticleSubmissionConsent: ChatbotPostbackHandler = async ({ `< SubmitTextArticleUnderConsentMutation, SubmitTextArticleUnderConsentMutationVariables - >({ text: data.searchedText ?? '' }, { userId }); + >({ text: firstMsg.text }, { userId }); article = result.data.CreateArticle; } else { - const articleType: ArticleTypeEnum = (() => { - switch (data.messageType) { - case 'image': - case 'audio': - case 'video': - return uppercase(data.messageType); - default: - throw new Error( - `[askingArticleSubmissionConsent] unsupported message type ${data.messageType}` - ); - } - })(); + const articleType: ArticleTypeEnum = uppercase(firstMsg.type); - const proxyUrl = getLineContentProxyURL(data.messageId); + const proxyUrl = getLineContentProxyURL(firstMsg.id); const result = await gql` mutation SubmitMediaArticleUnderConsent( @@ -137,7 +131,7 @@ const askingArticleSubmissionConsent: ChatbotPostbackHandler = async ({ ); // Create new session, make article submission button expire after submission - data.sessionId = Date.now(); + context.sessionId = Date.now(); const articleUrl = getArticleURL(article.id); const articleCreatedMsg = t`Your submission is now recorded at ${articleUrl}`; @@ -151,7 +145,7 @@ const askingArticleSubmissionConsent: ChatbotPostbackHandler = async ({ }), ]; - if (isTextArticle) { + if (firstMsg.type === 'text') { const aiReply = await createAIReply(article.id, userId); if (aiReply) { @@ -217,7 +211,7 @@ const askingArticleSubmissionConsent: ChatbotPostbackHandler = async ({ } visitor.send(); - return { data, replies }; + return { context, replies }; }; export default askingArticleSubmissionConsent; diff --git a/src/webhook/handlers/choosingArticle.ts b/src/webhook/handlers/choosingArticle.ts index 34f43696..1356ec8e 100644 --- a/src/webhook/handlers/choosingArticle.ts +++ b/src/webhook/handlers/choosingArticle.ts @@ -60,7 +60,7 @@ function reorderArticleReplies( const choosingArticle: ChatbotPostbackHandler = async (params) => { const { - data, + context, userId, postbackData: { input, state, sessionId }, } = params; @@ -70,8 +70,16 @@ const choosingArticle: ChatbotPostbackHandler = async (params) => { throw new ManipulationError(t`Please choose from provided options.`); } - if (input === POSTBACK_NO_ARTICLE_FOUND && 'searchedText' in data) { - const visitor = ga(userId, state, data.searchedText); + const firstMsg = context.msgs[0]; + // istanbul ignore if + if (!firstMsg) { + throw new Error('firstMsg is undefined'); // Should never happen + } + + // TODO: handle the case when there are multiple messages in context.msgs + // + if (input === POSTBACK_NO_ARTICLE_FOUND && firstMsg.type === 'text') { + const visitor = ga(userId, state, firstMsg.text); visitor.event({ ec: 'UserInput', ea: 'ArticleSearch', @@ -79,9 +87,9 @@ const choosingArticle: ChatbotPostbackHandler = async (params) => { }); visitor.send(); - const inputSummary = ellipsis(data.searchedText, 12); + const inputSummary = ellipsis(firstMsg.text, 12); return { - data, + context, replies: [ createTextMessage({ text: @@ -89,13 +97,13 @@ const choosingArticle: ChatbotPostbackHandler = async (params) => { '\n' + t`May I ask you a quick question?`, }), - createArticleSourceReply(data.sessionId), + createArticleSourceReply(context.sessionId), ], }; } - if (input === POSTBACK_NO_ARTICLE_FOUND && 'messageId' in data) { - const visitor = ga(userId, state, data.messageId); + if (input === POSTBACK_NO_ARTICLE_FOUND && firstMsg.type !== 'text') { + const visitor = ga(userId, state, firstMsg.id); visitor.event({ ec: 'UserInput', ea: 'ArticleSearch', @@ -104,7 +112,7 @@ const choosingArticle: ChatbotPostbackHandler = async (params) => { visitor.send(); return { - data, + context, replies: [ createTextMessage({ text: @@ -112,7 +120,7 @@ const choosingArticle: ChatbotPostbackHandler = async (params) => { '\n' + t`Do you want someone to fact-check this message?`, }), - createAskArticleSubmissionConsentReply(data.sessionId), + createAskArticleSubmissionConsentReply(context.sessionId), ], }; } @@ -179,7 +187,7 @@ const choosingArticle: ChatbotPostbackHandler = async (params) => { // choose reply for user return await choosingReply({ - data, + context, postbackData: { sessionId, state: 'CHOOSING_REPLY', @@ -317,7 +325,7 @@ const choosingArticle: ChatbotPostbackHandler = async (params) => { `👀 ${t`Take a look`}`, { a: selectedArticleId, r: reply.id }, t`I choose “${displayTextWhenChosen}”`, - data.sessionId, + context.sessionId, 'CHOOSING_REPLY' ), style: 'primary', @@ -461,7 +469,7 @@ Don’t trust the message just yet!`, visitor.send(); - return { data, replies }; + return { context, replies }; }; export default choosingArticle; diff --git a/src/webhook/handlers/choosingReply.ts b/src/webhook/handlers/choosingReply.ts index 1b81dda0..ca0986b7 100644 --- a/src/webhook/handlers/choosingReply.ts +++ b/src/webhook/handlers/choosingReply.ts @@ -140,7 +140,7 @@ function createShareBubble( } const choosingReply: ChatbotPostbackHandler = async ({ - data, + context, userId, postbackData: { input: postbackInput, state }, }) => { @@ -221,7 +221,7 @@ const choosingReply: ChatbotPostbackHandler = async ({ visitor.event({ ec: 'Reply', ea: 'Type', el: GetReply.type, ni: true }); visitor.send(); - return { data, replies }; + return { context, replies }; }; export default choosingReply; diff --git a/src/webhook/handlers/defaultState.ts b/src/webhook/handlers/defaultState.ts index f7421717..6396dac8 100644 --- a/src/webhook/handlers/defaultState.ts +++ b/src/webhook/handlers/defaultState.ts @@ -1,12 +1,9 @@ import { Message } from '@line/bot-sdk'; -import { - ChatbotPostbackHandlerParams, - ChatbotStateHandlerReturnType, -} from 'src/types/chatbotState'; +import { ChatbotPostbackHandlerParams, Result } from 'src/types/chatbotState'; export default function defaultState( params: ChatbotPostbackHandlerParams -): ChatbotStateHandlerReturnType { +): Result { const replies: Message[] = [ { type: 'text', diff --git a/src/webhook/handlers/handlePostback.ts b/src/webhook/handlers/handlePostback.ts index 11bacc47..89a9cbbe 100644 --- a/src/webhook/handlers/handlePostback.ts +++ b/src/webhook/handlers/handlePostback.ts @@ -8,7 +8,7 @@ import { ManipulationError } from './utils'; import tutorial from './tutorial'; import { ChatbotPostbackHandlerParams, - ChatbotStateHandlerReturnType, + Result, Context, PostbackActionData, } from 'src/types/chatbotState'; @@ -21,17 +21,17 @@ import { * @param userId LINE user ID that does the input */ export default async function handlePostback( - data: Context, + context: Context, postbackData: PostbackActionData, userId: string ) { const params: ChatbotPostbackHandlerParams = { - data, + context, postbackData, userId, }; - let result: ChatbotStateHandlerReturnType; + let result: Result; // Sets data and replies // @@ -65,7 +65,7 @@ export default async function handlePostback( } catch (e) { if (e instanceof ManipulationError) { result = { - ...params, + context, replies: [ { type: 'flex', @@ -109,8 +109,5 @@ export default async function handlePostback( } } - return { - context: { data: result.data }, - replies: result.replies, - }; + return result; } diff --git a/src/webhook/handlers/initState.ts b/src/webhook/handlers/initState.ts index 513824b1..13f5bca1 100644 --- a/src/webhook/handlers/initState.ts +++ b/src/webhook/handlers/initState.ts @@ -8,8 +8,9 @@ import { TextMessage, } from '@line/bot-sdk'; import type { - ChatbotStateHandlerReturnType, + Result, Context, + CooccurredMessage, } from 'src/types/chatbotState'; import gql from 'src/lib/gql'; import { @@ -31,17 +32,19 @@ import { const SIMILARITY_THRESHOLD = 0.95; const initState = async ({ - data, + context, userId, }: { // Context initiated by text search - data: Context & { searchedText: string }; + context: Context & { + msgs: ReadonlyArray; + }; userId: string; -}): Promise => { +}): Promise => { const state = '__INIT__'; let replies: Message[] = []; - const input = data.searchedText; + const input = context.msgs[0].text; // Track text message type send by user const visitor = ga(userId, state, input); @@ -54,12 +57,12 @@ const initState = async ({ // send input to dialogflow before doing search // uses dialogflowResponse as reply only when there's a intent matched and // input.length <= 10 or input.length > 10 but intentDetectionConfidence == 1 - const dialogflowResponse = await detectDialogflowIntent(data.searchedText); + const dialogflowResponse = await detectDialogflowIntent(input); if ( dialogflowResponse && dialogflowResponse.queryResult && dialogflowResponse.queryResult.intent && - (data.searchedText.length <= 10 || + (input.length <= 10 || dialogflowResponse.queryResult.intentDetectionConfidence == 1) ) { replies = [ @@ -74,7 +77,7 @@ const initState = async ({ el: dialogflowResponse.queryResult.intent.displayName ?? undefined, }); visitor.send(); - return { data, replies }; + return { context, replies }; } // Search for articles @@ -104,10 +107,10 @@ const initState = async ({ } } `({ - text: data.searchedText, + text: input, }); - const inputSummary = ellipsis(data.searchedText, 12); + const inputSummary = ellipsis(input, 12); if (ListArticles?.edges.length) { // Track if find similar Articles in DB. @@ -130,7 +133,7 @@ const initState = async ({ // Remove spaces so that we count word's similarities only // (edge.node.text ?? '').replace(/\s/g, ''), - data.searchedText.replace(/\s/g, '') + input.replace(/\s/g, '') ), })) .sort((edge1, edge2) => edge2.similarity - edge1.similarity) @@ -143,10 +146,10 @@ const initState = async ({ visitor.send(); return await choosingArticle({ - data, + context, // choose for user postbackData: { - sessionId: data.sessionId, + sessionId: context.sessionId, state: 'CHOOSING_ARTICLE', input: edgesSortedWithSimilarity[0].node.id, }, @@ -239,7 +242,7 @@ const initState = async ({ t`Choose this one`, id, t`I choose “${displayTextWhenChosen}”`, - data.sessionId, + context.sessionId, 'CHOOSING_ARTICLE' ), style: 'primary', @@ -298,7 +301,7 @@ const initState = async ({ t`Tell us more`, POSTBACK_NO_ARTICLE_FOUND, t`None of these messages matches mine :(`, - data.sessionId, + context.sessionId, 'CHOOSING_ARTICLE' ), style: 'primary', @@ -350,11 +353,11 @@ const initState = async ({ '\n' + t`May I ask you a quick question?`, }), - createArticleSourceReply(data.sessionId), + createArticleSourceReply(context.sessionId), ]; } visitor.send(); - return { data, replies }; + return { context, replies }; }; export default initState; diff --git a/src/webhook/handlers/processBatch.ts b/src/webhook/handlers/processBatch.ts new file mode 100644 index 00000000..1fde2276 --- /dev/null +++ b/src/webhook/handlers/processBatch.ts @@ -0,0 +1,27 @@ +import { Message } from '@line/bot-sdk'; + +import { Context, CooccurredMessage } from 'src/types/chatbotState'; +import { sleep } from 'src/lib/sharedUtils'; + +import { createTextMessage } from './utils'; + +async function processBatch(messages: CooccurredMessage[]) { + const context: Context = { + sessionId: Date.now(), + msgs: [], + }; + + const replies: Message[] = [ + createTextMessage({ + text: `目前我還沒辦法一次處理 ${messages.length} 則訊息,請一則一則傳進來唷!`, + }), + ]; + + // TODO: initiate multi-message processing here + // + await sleep(1000); // Simulate multi-message processing and see if more message in batch. + + return { context, replies }; +} + +export default processBatch; diff --git a/src/webhook/handlers/processMedia.ts b/src/webhook/handlers/processMedia.ts index 14ed77a0..220a49be 100644 --- a/src/webhook/handlers/processMedia.ts +++ b/src/webhook/handlers/processMedia.ts @@ -1,12 +1,11 @@ import { t } from 'ttag'; import type { - MessageEvent, FlexBubble, Message, FlexMessage, FlexComponent, } from '@line/bot-sdk'; -import { Context } from 'src/types/chatbotState'; +import { Context, CooccurredMessage } from 'src/types/chatbotState'; import { getLineContentProxyURL, @@ -27,29 +26,22 @@ import { const CIRCLED_DIGITS = '⓪①②③④⑤⑥⑦⑧⑨⑩⑪'; const SIMILARITY_THRESHOLD = 0.95; -export default async function ( - event: { - message: Pick; - }, - userId: string -) { - const proxyUrl = getLineContentProxyURL(event.message.id); +export default async function (message: CooccurredMessage, userId: string) { + const proxyUrl = getLineContentProxyURL(message.id); console.log(`Media url: ${proxyUrl}`); const visitor = ga(userId, '__PROCESS_MEDIA__', proxyUrl); // Track media message type send by user - visitor.event({ ec: 'UserInput', ea: 'MessageType', el: event.message.type }); + visitor.event({ ec: 'UserInput', ea: 'MessageType', el: message.type }); let replies; - const data: Context = { + const context: Context = { // Start a new session sessionId: Date.now(), // Store user messageId into context, which will use for submit new image article - searchedText: '', - messageId: event.message.id, - messageType: event.message.type, + msgs: [message], }; const { @@ -114,18 +106,16 @@ export default async function ( if (ListArticles.edges.length === 1 && hasIdenticalDocs) { visitor.send(); - const result = await choosingArticle({ - data, + return await choosingArticle({ + context, // choose for user postbackData: { state: 'CHOOSING_ARTICLE', - sessionId: data.sessionId, + sessionId: context.sessionId, input: edgesSortedWithSimilarity[0].node.id, }, userId, }); - - return { context: { data: result.data }, replies: result.replies }; } const articleOptions = ListArticles.edges @@ -238,7 +228,7 @@ export default async function ( t`Choose this one`, id, t`I choose ${displayTextWhenChosen}`, - data.sessionId, + context.sessionId, 'CHOOSING_ARTICLE' ), style: 'primary', @@ -298,7 +288,7 @@ export default async function ( t`Tell us more`, POSTBACK_NO_ARTICLE_FOUND, t`None of these messages matches mine :(`, - data.sessionId, + context.sessionId, 'CHOOSING_ARTICLE' ), style: 'primary', @@ -350,9 +340,9 @@ export default async function ( '\n' + t`Do you want someone to fact-check this message?`, }), - createAskArticleSubmissionConsentReply(data.sessionId), + createAskArticleSubmissionConsentReply(context.sessionId), ]; } visitor.send(); - return { context: { data }, replies }; + return { context, replies }; } diff --git a/src/webhook/handlers/singleUserHandler.ts b/src/webhook/handlers/singleUserHandler.ts index 8b7d0d3c..688bc512 100644 --- a/src/webhook/handlers/singleUserHandler.ts +++ b/src/webhook/handlers/singleUserHandler.ts @@ -1,13 +1,18 @@ import { t } from 'ttag'; import { WebhookEvent } from '@line/bot-sdk'; -import { CooccurredMessage, PostbackActionData } from 'src/types/chatbotState'; +import { + CooccurredMessage, + PostbackActionData, + Result, + LegacyContext, + Context, +} from 'src/types/chatbotState'; import ga from 'src/lib/ga'; import redis from 'src/lib/redisClient'; import { extractArticleId, sleep } from 'src/lib/sharedUtils'; import lineClient from 'src/webhook/lineClient'; import UserSettings from 'src/database/models/userSettings'; -import { Result } from 'src/types/result'; import handlePostback from './handlePostback'; import { @@ -16,8 +21,8 @@ import { createTutorialMessage, } from './tutorial'; import processMedia from './processMedia'; +import processBatch from './processBatch'; import initState from './initState'; -import { createTextMessage } from './utils'; const userIdBlacklist = (process.env.USERID_BLACKLIST || '').split(','); @@ -77,12 +82,7 @@ const singleUserHandler = async ( isRepliedDueToTimeout = true; }, REPLY_TIMEOUT); - // Get user's context from redis or create a new one - // - const context = (await redis.get(userId)) || { - data: { sessionId: Date.now() }, - }; - + const context = await getContextForUser(userId); const REDIS_BATCH_KEY = getRedisBatchKey(userId); /** @@ -184,56 +184,28 @@ const singleUserHandler = async ( ); if (messages.length !== 1) { - // TODO: initiate multi-message processing here - // - await sleep(1000); // Simulate multi-message processing and see if more message in batch. - return send( - { - context, - replies: [ - createTextMessage({ - text: `目前我還沒辦法一次處理 ${messages.length} 則訊息,請一則一則傳進來唷!`, - }), - ], - }, - msg - ); + return send(await processBatch(messages), msg); } - const firstMsg = messages[0]; - if (firstMsg.type !== 'text') { - return send( - await processMedia( - { - message: { - id: firstMsg.id, - type: firstMsg.type, - }, - }, - userId - ), - msg - ); + // Now there is only one message in the batch; + // messages[0] should be identical to msg. + // + if (msg.type !== 'text') { + return send(await processMedia(msg, userId), msg); } - const result = await initState({ - data: { + + return send( + await initState({ // Create a new "search session". // Used to determine button postbacks and GraphQL requests are from // previous sessions // - sessionId: Date.now(), - - // Store user input into context - searchedText: firstMsg.text, - }, - userId, - }); - - return send( - { - context: { data: result.data }, - replies: result.replies, - }, + context: { + ...getNewContext(), + msgs: [msg], + }, + userId, + }), msg ); } @@ -253,7 +225,7 @@ const singleUserHandler = async ( await UserSettings.setAllowNewReplyUpdate(userId, true); // Create new context - const data = { sessionId: Date.now() }; + const context = getNewContext(); const visitor = ga(userId, 'TUTORIAL'); visitor.event({ @@ -264,10 +236,10 @@ const singleUserHandler = async ( visitor.send(); return send({ - context: { data }, + context, replies: [ createGreetingMessage(), - createTutorialMessage(data.sessionId), + createTutorialMessage(context.sessionId), ], }); } @@ -277,8 +249,8 @@ const singleUserHandler = async ( webhookEvent.postback.data ) as PostbackActionData; - if (postbackData.sessionId === context.data.sessionId) { - return send(await handlePostback(context.data, postbackData, userId)); + if (postbackData.sessionId === context.sessionId) { + return send(await handlePostback(context, postbackData, userId)); } // Postback data session ID != context session ID can happen when @@ -350,13 +322,13 @@ const singleUserHandler = async ( case TUTORIAL_STEPS['RICH_MENU']: { // Start new session, reroute to TUTORIAL - const sessionId = Date.now(); + const context = getNewContext(); return send( await handlePostback( - { sessionId, searchedText: '' }, + context, { state: 'TUTORIAL', - sessionId, + sessionId: context.sessionId, input: TUTORIAL_STEPS['RICH_MENU'], }, userId @@ -371,17 +343,14 @@ const singleUserHandler = async ( if (articleId) { // It is a predefined text message wanting us to visit a article. // Start new session, reroute to CHOOSING_ARTILCE and simulate "choose article" postback event - const sessionId = Date.now(); + const context = getNewContext(); return send( await handlePostback( // Start a new session - { - sessionId, - searchedText: '', - }, + context, { state: 'CHOOSING_ARTICLE', - sessionId, + sessionId: context.sessionId, input: articleId, }, userId @@ -404,4 +373,46 @@ export function getRedisBatchKey(userId: string) { return `${userId}:batch`; } +function getNewContext(): Context { + return { + sessionId: Date.now(), + msgs: [], + }; +} + +/** + * Get user's context from redis or create a new one. + * Automatically convert legacy context to new context. + * + * @param userId + * @returns user's context from Redis, or newly created context + */ +async function getContextForUser(userId: string): Promise { + const context = ((await redis.get(userId)) || getNewContext()) as + | LegacyContext + | Context; + + if (!('data' in context)) { + // New context + return context; + } + + // Converting legacy context to new context + return { + sessionId: context.data.sessionId, + msgs: [ + 'searchedText' in context.data + ? { + id: context.data.sessionId.toString(), // Original message ID is not available, use session id to differentiate + type: 'text', + text: context.data.searchedText, + } + : { + id: context.data.messageId, + type: context.data.messageType, + }, + ], + }; +} + export default singleUserHandler; diff --git a/src/webhook/handlers/tutorial.ts b/src/webhook/handlers/tutorial.ts index 26704534..f877deef 100644 --- a/src/webhook/handlers/tutorial.ts +++ b/src/webhook/handlers/tutorial.ts @@ -11,7 +11,7 @@ import { CreateReplyMessagesReplyFragment } from 'typegen/graphql'; import { ChatbotPostbackHandlerParams, ChatbotState, - ChatbotStateHandlerReturnType, + Result, } from 'src/types/chatbotState'; /** @@ -295,10 +295,10 @@ function createPermissionSetupDialog(message: string): FlexMessage { } export default function tutorial({ - data, + context, postbackData, userId, -}: ChatbotPostbackHandlerParams): ChatbotStateHandlerReturnType { +}: ChatbotPostbackHandlerParams): Result { let replies: Message[] = []; const replyProvidePermissionSetup = `${t`You are smart`} 😊`; @@ -317,11 +317,11 @@ export default function tutorial({ if (!process.env.RUMORS_LINE_BOT_URL) { throw new Error('RUMORS_LINE_BOT_URL undefined'); } else if (postbackData.input === TUTORIAL_STEPS['RICH_MENU']) { - replies = [createTutorialMessage(data.sessionId)]; + replies = [createTutorialMessage(context.sessionId)]; } else if ( postbackData.input === TUTORIAL_STEPS['SIMULATE_FORWARDING_MESSAGE'] ) { - replies = createMockReplyMessages(data.sessionId); + replies = createMockReplyMessages(context.sessionId); } else if ( postbackData.input === TUTORIAL_STEPS['PROVIDE_PERMISSION_SETUP'] ) { @@ -336,17 +336,17 @@ export default function tutorial({ items: [ createQuickReplyPostbackItem( TUTORIAL_STEPS['SETUP_DONE'], - data.sessionId, + context.sessionId, 'TUTORIAL' ), createQuickReplyPostbackItem( TUTORIAL_STEPS['SETUP_LATER'], - data.sessionId, + context.sessionId, 'TUTORIAL' ), createQuickReplyPostbackItem( TUTORIAL_STEPS['PROVIDE_PERMISSION_SETUP_WITH_EXPLANATION'], - data.sessionId, + context.sessionId, 'TUTORIAL' ), ], @@ -368,17 +368,17 @@ export default function tutorial({ items: [ createQuickReplyPostbackItem( TUTORIAL_STEPS['SETUP_DONE'], - data.sessionId, + context.sessionId, 'TUTORIAL' ), createQuickReplyPostbackItem( TUTORIAL_STEPS['SETUP_LATER'], - data.sessionId, + context.sessionId, 'TUTORIAL' ), createQuickReplyPostbackItem( TUTORIAL_STEPS['PROVIDE_PERMISSION_SETUP_WITH_EXPLANATION'], - data.sessionId, + context.sessionId, 'TUTORIAL' ), ], @@ -396,12 +396,12 @@ export default function tutorial({ items: [ createQuickReplyPostbackItem( TUTORIAL_STEPS['SETUP_DONE'], - data.sessionId, + context.sessionId, 'TUTORIAL' ), createQuickReplyPostbackItem( TUTORIAL_STEPS['SETUP_LATER'], - data.sessionId, + context.sessionId, 'TUTORIAL' ), ], @@ -432,5 +432,5 @@ export default function tutorial({ }); visitor.send(); - return { data, replies }; + return { context, replies }; }