From 27cd8f4f6b69e44740ab241397b6be8a9a00ee85 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Sun, 19 Nov 2023 23:56:40 +0800 Subject: [PATCH] refactor(webhook): type yes/no postback handlers --- src/webhook/handlers/askingArticleSource.ts | 27 ++++++++++--- .../askingArticleSubmissionConsent.ts | 38 +++++++++++++------ src/webhook/handlers/utils.ts | 21 +++++++--- 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/webhook/handlers/askingArticleSource.ts b/src/webhook/handlers/askingArticleSource.ts index a560cded..629eac60 100644 --- a/src/webhook/handlers/askingArticleSource.ts +++ b/src/webhook/handlers/askingArticleSource.ts @@ -1,5 +1,8 @@ import { t } from 'ttag'; +import { Message } from '@line/bot-sdk'; +import { z } from 'zod'; import ga from 'src/lib/ga'; +import { ChatbotPostbackHandler } from 'src/types/chatbotState'; import { POSTBACK_YES, @@ -12,14 +15,26 @@ import { } from './utils'; import { TUTORIAL_STEPS } from './tutorial'; -import { ChatbotPostbackHandler } from 'src/types/chatbotState'; -import { Message } from '@line/bot-sdk'; + + +const inputSchema = z.enum([POSTBACK_NO, POSTBACK_YES]); + +/** Postback input type for ASKING_ARTICLE_SOURCE state handler */ +export type Input = z.infer; const askingArticleSource: ChatbotPostbackHandler = async ({ data, - postbackData: { state, input }, + postbackData: { state, input: postbackInput }, userId, }) => { + let input: Input; + try { + input = inputSchema.parse(postbackInput); + } catch (e) { + console.error('[choosingReply]', e); + throw new ManipulationError(t`Please choose from provided options.`); + } + let replies: Message[] = []; const visitor = ga( @@ -29,8 +44,10 @@ const askingArticleSource: ChatbotPostbackHandler = async ({ ); switch (input) { - default: - throw new ManipulationError(t`Please choose from provided options.`); + default: { + // Exhaustive check + return input satisfies never; + } case POSTBACK_NO: replies = [ diff --git a/src/webhook/handlers/askingArticleSubmissionConsent.ts b/src/webhook/handlers/askingArticleSubmissionConsent.ts index 1ec3a140..66aa388e 100644 --- a/src/webhook/handlers/askingArticleSubmissionConsent.ts +++ b/src/webhook/handlers/askingArticleSubmissionConsent.ts @@ -1,10 +1,21 @@ import { t } from 'ttag'; import { Message } from '@line/bot-sdk'; +import { z } from 'zod'; import { ChatbotPostbackHandler } from 'src/types/chatbotState'; import ga from 'src/lib/ga'; import gql from 'src/lib/gql'; import { getArticleURL } from 'src/lib/sharedUtils'; +import UserSettings from 'src/database/models/userSettings'; +import UserArticleLink from 'src/database/models/userArticleLink'; +import { + ArticleTypeEnum, + SubmitMediaArticleUnderConsentMutation, + SubmitMediaArticleUnderConsentMutationVariables, + SubmitTextArticleUnderConsentMutation, + SubmitTextArticleUnderConsentMutationVariables, +} from 'typegen/graphql'; + import { POSTBACK_YES, POSTBACK_NO, @@ -16,15 +27,11 @@ import { getLineContentProxyURL, createAIReply, } from './utils'; -import UserSettings from 'src/database/models/userSettings'; -import UserArticleLink from 'src/database/models/userArticleLink'; -import { - ArticleTypeEnum, - SubmitMediaArticleUnderConsentMutation, - SubmitMediaArticleUnderConsentMutationVariables, - SubmitTextArticleUnderConsentMutation, - SubmitTextArticleUnderConsentMutationVariables, -} from 'typegen/graphql'; + +const inputSchema = z.enum([POSTBACK_NO, POSTBACK_YES]); + +/** Postback input type for ASKING_ARTICLE_SUBMISSION_CONSENT state handler */ +export type Input = z.infer; function uppercase(s: T) { return s.toUpperCase() as Uppercase; @@ -32,9 +39,17 @@ function uppercase(s: T) { const askingArticleSubmissionConsent: ChatbotPostbackHandler = async ({ data, - postbackData: { state, input }, + postbackData: { state, input: postbackInput }, userId, }) => { + let input: Input; + try { + input = inputSchema.parse(postbackInput); + } catch (e) { + console.error('[askingArticleSubmissionConsnet]', e); + throw new ManipulationError(t`Please choose from provided options.`); + } + const visitor = ga( userId, state, @@ -45,7 +60,8 @@ const askingArticleSubmissionConsent: ChatbotPostbackHandler = async ({ switch (input) { default: - throw new ManipulationError(t`Please choose from provided options.`); + // Exhaustive check + return input satisfies never; case POSTBACK_NO: visitor.event({ ec: 'Article', ea: 'Create', el: 'No' }); diff --git a/src/webhook/handlers/utils.ts b/src/webhook/handlers/utils.ts index 2bf3d1a3..8f87f143 100644 --- a/src/webhook/handlers/utils.ts +++ b/src/webhook/handlers/utils.ts @@ -11,7 +11,11 @@ import type { } from '@line/bot-sdk'; import { t, msgid, ngettext } from 'ttag'; import GraphemeSplitter from 'grapheme-splitter'; + import gql from 'src/lib/gql'; +import { getArticleURL, createTypeWords } from 'src/lib/sharedUtils'; +import { sign } from 'src/lib/jwt'; +import { ChatbotState, PostbackActionData } from 'src/types/chatbotState'; import type { CreateHighlightContentsHighlightFragment, @@ -21,26 +25,31 @@ import type { CreateAiReplyMutation, CreateAiReplyMutationVariables, } from 'typegen/graphql'; -import { getArticleURL, createTypeWords } from 'src/lib/sharedUtils'; -import { sign } from 'src/lib/jwt'; -import { ChatbotState, PostbackActionData } from 'src/types/chatbotState'; + import type { Input as ChoosingReplyInput } from './choosingReply'; +import type { Input as AskingArticleSourceInput } from './askingArticleSource'; +import type { Input as AskingArticleSubmissionConsentInput } from './askingArticleSubmissionConsent'; const splitter = new GraphemeSplitter(); +/** + * Maps ChatbotState to the postback action data + */ type StateInputMap = { __INIT__: string; TUTORIAL: string; CHOOSING_ARTICLE: string; CHOOSING_REPLY: ChoosingReplyInput; - ASKING_ARTICLE_SOURCE: string; - ASKING_ARTICLE_SUBMISSION_CONSENT: string; + ASKING_ARTICLE_SOURCE: AskingArticleSourceInput; + ASKING_ARTICLE_SUBMISSION_CONSENT: AskingArticleSubmissionConsentInput; Error: unknown; }; /** + * Generate a postback action with a payload that the state handler can process properly. + * * @param label - Postback action button text, max 20 words - * @param input - Input when pressed + * @param input - Input when pressed. The format must match the postback data type for that state. * @param displayText - Text to display in chat window. * @param sessionId - Current session ID * @param state - the state that processes the postback