UNPKG

@botonic/core

Version:
269 lines (233 loc) 6.92 kB
import { Inspector } from './debug' import { BotContext, BotonicAction, BotRequest, BotResponse, INPUT, Input, ProcessInputResult, ResolvedPlugins, Route, Routes, Session, } from './models' import { runPlugins } from './plugins' import { getComputedRoutes, Router } from './routing' export interface CoreBotConfig { appId?: string defaultDelay?: number defaultRoutes?: Route[] defaultTyping?: number inspector?: Inspector plugins?: ResolvedPlugins renderer: any routes: Routes theme?: any } export class CoreBot { appId?: string defaultDelay: number defaultRoutes: Route[] defaultTyping: number inspector: Inspector plugins: ResolvedPlugins renderer: any // TODO use a type like ({ request, actions }: RendererArgs) => Promise<any[]> rootElement: any router: Router | null routes: Routes theme?: any constructor({ renderer, routes, theme, plugins, appId, defaultTyping = 0.6, defaultDelay = 0.4, defaultRoutes, inspector, }: CoreBotConfig) { this.renderer = renderer this.plugins = plugins || {} this.theme = theme || {} this.defaultTyping = defaultTyping this.defaultDelay = defaultDelay this.appId = appId || undefined this.rootElement = null this.inspector = inspector || new Inspector() this.routes = routes this.defaultRoutes = defaultRoutes || [] this.router = this.routes instanceof Function ? null : new Router( [...this.routes, ...this.defaultRoutes], this.inspector.routeInspector ) } setSystemLocale(locale: string, session: Session): void { session.user.system_locale = locale } setUserLocale(locale: string, session: Session): void { session.user.locale = locale } setUserCountry(country: string, session: Session): void { session.user.country = country } async input(request: BotRequest): Promise<BotResponse> { const botContext = this.getBotContext(request) const output = await this.runInput(botContext) if ( botContext.session._botonic_action?.startsWith(BotonicAction.Redirect) ) { return await this.runRedirectAction(output, botContext) } return output } private getBotContext(request: BotRequest): BotContext { const { input, session, lastRoutePath } = request return { input, session, lastRoutePath, params: {}, plugins: this.plugins, defaultTyping: this.defaultTyping, defaultDelay: this.defaultDelay, getUserCountry: () => session.user.country, getUserLocale: () => session.user.locale, getSystemLocale: () => session.user.system_locale, setUserCountry: (country: string) => this.setUserCountry(country, session), setUserLocale: (locale: string) => this.setUserLocale(locale, session), setSystemLocale: (locale: string) => this.setSystemLocale(locale, session), } } private async runInput(botContext: BotContext): Promise<BotResponse> { // After first updateSession, user country locale and system_locale are always defined this.updateSession(botContext.session) if (botContext.input.type === INPUT.CHAT_EVENT) { return { input: botContext.input, session: botContext.session, lastRoutePath: botContext.lastRoutePath, response: [], } } await this.runPrePlugins(botContext) const output = await this.getOutput(botContext) botContext = this.updateRequestFromOutput(botContext, output) const response = await this.renderer({ request: botContext, actions: [output.fallbackAction, output.action, output.emptyAction], }) await this.runPostPlugins(botContext, response) botContext.session.is_first_interaction = false return { input: botContext.input, response, session: botContext.session, lastRoutePath: botContext.lastRoutePath, } } private updateSession(session: Session) { // set new user fields (country, locale, system_locale) from old fields in extra_data if (!session.user.country) { const country = session.user.extra_data?.country this.setUserCountry(country, session) } if (!session.user.locale) { const language = session.user.extra_data?.language this.setUserLocale(language, session) } if (!session.user.system_locale) { const locale = session.user.locale if (locale) { this.setSystemLocale(locale, session) } } } private async runPrePlugins(botContext: BotContext) { if (this.plugins) { await runPlugins({ botContext, mode: 'pre' }) } } private async getOutput(botContext: BotContext): Promise<ProcessInputResult> { await this.setRouter(botContext) const output = (this.router as Router).processInput( botContext.input, botContext.session, botContext.lastRoutePath ) return output } private async setRouter(botContext: BotContext) { if (this.routes instanceof Function) { this.router = new Router( [ ...(await getComputedRoutes(this.routes, botContext)), ...this.defaultRoutes, ], this.inspector.routeInspector ) } } private updateRequestFromOutput( botContext: BotContext, output: ProcessInputResult ): BotContext { return { ...botContext, params: output.params || {}, lastRoutePath: output.lastRoutePath, } } private async runPostPlugins(botContext: BotContext, response: any) { if (this.plugins) { await runPlugins({ botContext, mode: 'post', response, }) } } private async runRedirectAction( previousResponse: BotResponse, botContext: BotContext, numOfRedirects: number = 0 ) { if (numOfRedirects > 10) { throw new Error('Maximum BotAction recursive calls reached (10)') } const nextPayload = botContext.session._botonic_action?.split( `${BotonicAction.Redirect}:` )[1] const inputWithBotActionPayload: Input = { ...botContext.input, payload: nextPayload, type: INPUT.POSTBACK, data: undefined, text: undefined, } botContext.session._botonic_action = undefined botContext.input = inputWithBotActionPayload const followUp = await this.runInput(botContext) const response = { input: followUp.input, response: previousResponse.response.concat(followUp.response), session: botContext.session, lastRoutePath: followUp.lastRoutePath, } // Recursive call to handle multiple bot actions in a row if (response.session._botonic_action?.startsWith(BotonicAction.Redirect)) { return await this.runRedirectAction( response, botContext, numOfRedirects + 1 ) } return response } }