Links
https://github.com/EndyKaufman/kaufman-bot - source code of bot
https://telegram.me/DevelopKaufmanBot - current bot in telegram
https://cloud.google.com/dialogflow/docs/support/getting-support - official docs
https://github.com/googleapis/nodejs-dialogflow - node library for work with dialogflow
Install dependecies
npm i --save @google-cloud/dialogflow uuid
endy@endy-virtual-machine:~/Projects/current/kaufman-bot$ npm i --save-dev @google-cloud/dialogflow uuid added 42 packages, and audited 984 packages in 8s 115 packages are looking for funding run `npm fund` for details found 0 vulnerabilities
npm i --save-dev @types/uuid
endy@endy-virtual-machine:~/Projects/current/kaufman-bot$ npm i --save-dev @types/uuid added 1 package, and audited 985 packages in 2s 115 packages are looking for funding run `npm fund` for details found 0 vulnerabilities
Setup dialogflow
Create project
Navigate to https://dialogflow.cloud.google.com/
After create you see two default intents
Response for answer was selected from default responses
Settings of authorizations
Navigate to project list
https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create?supportedpurview=project
In the Service account description field, enter a description. For example, Service account for quickstart.
Select role with Dialogflow and click continue
After click done you see list of accounts
Click Add key, then click Create new key
A JSON key file is downloaded to your computer, click Close
Copy downloaded file to root folder of application and add it file name to .gitignore
Update core source for correct work dialogflow logic
Update OnAfterBotCommands
libs/core/server/src/lib/bot-commands/bot-commands-types/on-after-bot-commands.interface.ts
import { BotCommandsProviderActionResultType } from './bot-commands-provider-action-result-type'; import { BotCommandsProviderActionMsg } from './bot-commands-provider.interface'; export interface OnAfterBotCommands { onAfterBotCommands< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >( result: BotCommandsProviderActionResultType<TMsg>, msg: TMsg, ctx?, defaultHandler?: () => Promise<unknown> ): Promise<{ result: BotCommandsProviderActionResultType<TMsg>; msg: TMsg }>; }
Update BotСommandsService
libs/core/server/src/lib/bot-commands/bot-commands-services/bot-commands.service.ts
import { Injectable } from '@nestjs/common'; import { CustomInject } from 'nestjs-custom-injector'; import { BotCommandsEnum } from '../bot-commands-types/bot-commands-enum'; import { BotCommandsProviderActionResultType } from '../bot-commands-types/bot-commands-provider-action-result-type'; import { BotCommandsProvider, BotCommandsProviderActionContext, BotCommandsProviderActionMsg, BOT_COMMANDS_PROVIDER, } from '../bot-commands-types/bot-commands-provider.interface'; import { OnAfterBotCommands } from '../bot-commands-types/on-after-bot-commands.interface'; import { OnBeforeBotCommands } from '../bot-commands-types/on-before-bot-commands.interface'; import { BotСommandsToolsService } from './bot-commands-tools.service'; @Injectable() export class BotСommandsService implements BotCommandsProvider { @CustomInject(BOT_COMMANDS_PROVIDER, { multi: true }) private botCommandsProviders!: (BotCommandsProvider & Partial<OnBeforeBotCommands> & Partial<OnAfterBotCommands>)[]; constructor( private readonly botСommandsToolsService: BotСommandsToolsService ) {} async process(ctx, defaultHandler?: () => Promise<unknown>) { let msg: BotCommandsProviderActionMsg = ctx.update.message; const result = await this.onMessage(msg, ctx, defaultHandler); if (result?.type === 'message') { msg = result.message; } if (result?.type === 'markdown') { await ctx.reply(result.markdown, { parse_mode: 'MarkdownV2' }); return; } if (result?.type === 'text') { await ctx.reply(result.text); return; } } async onHelp<TMsg extends BotCommandsProviderActionMsg>( msg: TMsg, ctx: BotCommandsProviderActionContext ): Promise<BotCommandsProviderActionResultType<TMsg>> { const allResults: string[] = []; const len = this.botCommandsProviders.length; for (let i = 0; i < len; i++) { const botCommandsProvider = this.botCommandsProviders[i]; const result = await botCommandsProvider.onHelp(msg, ctx); if (result !== null && result.type === 'text') { allResults.push(result.text); } if (result !== null && result.type === 'markdown') { allResults.push(result.markdown); } } return { type: 'markdown', markdown: allResults.join('\n\n'), }; } async onMessage<TMsg extends BotCommandsProviderActionMsg>( msg: TMsg, ctx: BotCommandsProviderActionContext, defaultHandler?: () => Promise<unknown> ): Promise<BotCommandsProviderActionResultType<TMsg>> { msg = await this.processOnBeforeBotCommands(msg, ctx); const len = this.botCommandsProviders.length; let result: BotCommandsProviderActionResultType<TMsg> = null; for (let i = 0; i < len; i++) { if (!result) { const botCommandsProvider = this.botCommandsProviders[i]; result = await botCommandsProvider.onMessage(msg, ctx); } } if ( result === null && this.botСommandsToolsService.checkCommands( msg.text, [BotCommandsEnum.help], msg.from.language_code ) ) { return this.onHelp(msg, ctx); } const afterBotCommand = await this.processOnAfterBotCommands( result, msg, ctx, defaultHandler ); if (defaultHandler) { await defaultHandler(); } return afterBotCommand.result; } async processOnBeforeBotCommands<TMsg extends BotCommandsProviderActionMsg>( msg: TMsg, ctx?: BotCommandsProviderActionContext ): Promise<TMsg> { const len = this.botCommandsProviders.length; for (let i = 0; i < len; i++) { const botCommandsProvider = this.botCommandsProviders[i]; if (botCommandsProvider.onBeforeBotCommands) msg = await botCommandsProvider.onBeforeBotCommands(msg, ctx); } return msg; } async processOnAfterBotCommands<TMsg extends BotCommandsProviderActionMsg>( result: BotCommandsProviderActionResultType<TMsg>, msg: TMsg, ctx?: BotCommandsProviderActionContext, defaultHandler?: () => Promise<unknown> ): Promise<{ result: BotCommandsProviderActionResultType<TMsg>; msg: TMsg }> { const len = this.botCommandsProviders.length; for (let i = 0; i < len; i++) { const botCommandsProvider = this.botCommandsProviders[i]; if (botCommandsProvider.onAfterBotCommands) { const afterBotCommand = await botCommandsProvider.onAfterBotCommands<TMsg>( result, msg, ctx, defaultHandler ); result = afterBotCommand.result; msg = afterBotCommand.msg; } } return { result, msg }; } }
Update debug-messages modules files for reuse it in other libs
Create DebugService
libs/debug-messages/server/src/lib/debug-messages-services/debug.service.ts
import { BotCommandsProviderActionMsg } from '@kaufman-bot/core/server'; import { Injectable, Logger } from '@nestjs/common'; const DEBUG_MODE = 'debugMode'; @Injectable() export class DebugService { private readonly logger = new Logger(DebugService.name); setDebugMode< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg, value: boolean) { if (!msg.botContext) { msg.botContext = {}; } msg.botContext[DEBUG_MODE] = value; return msg; } sendDebugInfo< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >( msg: TMsg, // eslint-disable-next-line @typescript-eslint/no-explicit-any ctx: any, // eslint-disable-next-line @typescript-eslint/no-explicit-any data: any, context: string ) { if (msg.botContext?.[DEBUG_MODE]) { ctx.reply( [ `*${context} \\(${+new Date()}\\):*`, '``` ', JSON.stringify(data, undefined, 4), ' ```', ].join('\n'), { parse_mode: 'MarkdownV2', } ); } this.logger.debug(data, context); } }
Update DebugMessagesService
libs/debug-messages/server/src/lib/debug-messages-services/debug-messages.service.ts
import { BotCommandsEnum, BotCommandsProvider, BotCommandsProviderActionMsg, BotCommandsProviderActionResultType, BotСommandsToolsService, OnAfterBotCommands, OnBeforeBotCommands, } from '@kaufman-bot/core/server'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { getText } from 'class-validator-multi-lang'; import { TranslatesService } from 'nestjs-translates'; import { DebugMessagesConfig, DEBUG_MESSAGES_CONFIG, } from '../debug-messages-config/debug-messages.config'; import { DebugMessagesCommandsEnum } from '../debug-messages-types/debug-messages-commands'; import { DebugMessagesStorage } from './debug-messages.storage'; import { DebugService } from './debug.service'; @Injectable() export class DebugMessagesService implements BotCommandsProvider, OnBeforeBotCommands, OnAfterBotCommands { private readonly logger = new Logger(DebugMessagesService.name); constructor( @Inject(DEBUG_MESSAGES_CONFIG) private readonly debugMessagesConfig: DebugMessagesConfig, private readonly translatesService: TranslatesService, private readonly debugMessagesStorage: DebugMessagesStorage, private readonly commandToolsService: BotСommandsToolsService, private readonly debugService: DebugService ) {} async onAfterBotCommands< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >( result: BotCommandsProviderActionResultType<TMsg>, msg: TMsg, ctx ): Promise<{ result: BotCommandsProviderActionResultType<TMsg>; msg: TMsg }> { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { botContext, ...debugData } = msg; this.debugService.sendDebugInfo( msg, ctx, debugData, this.debugMessagesConfig.name ); return { msg, result, }; } async onBeforeBotCommands< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg): Promise<TMsg> { const debugMode = await this.debugMessagesStorage.getDebugModeOfUser( msg.from?.id ); return this.debugService.setDebugMode(msg, debugMode); } async onHelp< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg): Promise<BotCommandsProviderActionResultType<TMsg>> { return await this.onMessage({ ...msg, text: `${this.debugMessagesConfig.name} ${BotCommandsEnum.help}`, }); } async onMessage< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg): Promise<BotCommandsProviderActionResultType<TMsg>> { const locale = msg.from?.language_code || 'en'; const spyWord = this.debugMessagesConfig.spyWords.find((spyWord) => this.commandToolsService.checkCommands(msg.text, [spyWord], locale) ); if (spyWord) { if ( this.commandToolsService.checkCommands( msg.text, [BotCommandsEnum.help], locale ) ) { return { type: 'markdown', markdown: this.commandToolsService.generateHelpMessage( locale, this.debugMessagesConfig.name, this.debugMessagesConfig.descriptions, this.debugMessagesConfig.usage ), }; } const processedMsg = await this.process(msg, locale); if (typeof processedMsg === 'string') { return { type: 'text', text: processedMsg, }; } if (processedMsg) { return { type: 'message', message: processedMsg }; } this.logger.warn(`Unhandled commands for text: "${msg.text}"`); this.logger.debug(msg); } return null; } private async process< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg, locale: string) { const debugMode = await this.debugMessagesStorage.getDebugModeOfUser( msg.from?.id ); if ( this.commandToolsService.checkCommands( msg.text, [DebugMessagesCommandsEnum.on], locale ) ) { if (!debugMode) { await this.debugMessagesStorage.setDebugModeOfUser(msg.from?.id, true); return this.translatesService.translate( getText(`debug enabled`), locale, { locale, } ); } else { return this.translatesService.translate( getText(`debug already enabled`), locale, { locale, } ); } } if ( this.commandToolsService.checkCommands( msg.text, [DebugMessagesCommandsEnum.off], locale ) ) { if (debugMode) { await this.debugMessagesStorage.setDebugModeOfUser(msg.from?.id, false); return this.translatesService.translate( getText(`debug disabled`), locale, { locale, } ); } else { return this.translatesService.translate( getText(`debug already disabled`), locale, { locale, } ); } } if ( this.commandToolsService.checkCommands( msg.text, [DebugMessagesCommandsEnum.current], locale ) ) { return this.translatesService.translate( getText(`debug: {{debugMode}}`), locale, { debugMode: debugMode ? getText('enabled') : getText('disabled') } ); } return null; } }
Update DebugMessagesModule
libs/debug-messages/server/src/lib/debug-messages.module.ts
... import { DebugService } from './debug-messages-services/debug.service'; @Module({ imports: [TranslatesModule, PrismaClientModule, BotCommandsModule], providers: [DebugMessagesStorage, DebugService], exports: [ TranslatesModule, PrismaClientModule, BotCommandsModule, DebugMessagesStorage, DebugService, ], }) export class DebugMessagesModule { ...
Create DialogFlowModule
Create table for store metadata with user activity
Create migration
migrations/V202204030939__CreateDialogflowTable.pgsql
CREATE TABLE IF NOT EXISTS "DialogflowSession" ( id uuid DEFAULT uuid_generate_v4 () NOT NULL, "userId" uuid NOT NULL CONSTRAINT "FK_DIALOGFLOW_SESSION__USER_ID" REFERENCES "User", "projectId" varchar(512) NOT NULL, "sessionId" uuid NOT NULL, "requestsMetadata" jsonb DEFAULT '[]' NOT NULL, "responsesMetadata" jsonb DEFAULT '[]' NOT NULL, "createdAt" timestamp DEFAULT now() NOT NULL, "updatedAt" timestamp DEFAULT now() NOT NULL, CONSTRAINT "PK_DIALOGFLOW_SESSION" PRIMARY KEY (id) ); CREATE UNIQUE INDEX IF NOT EXISTS "UQ_DIALOGFLOW_SESSION" ON "DialogflowSession" ("userId", "projectId", "sessionId");
Apply migrations
npm run migrate:local
endy@endy-virtual-machine:~/Projects/current/kaufman-bot$ npm run migrate:local > kaufman-bot@0.0.0 migrate:local > export $(xargs < ./.env.local) > /dev/null 2>&1 && export DATABASE_URL=$SERVER_POSTGRES_URL && npm run migrate > kaufman-bot@0.0.0 migrate > npm run flyway -- migrate > kaufman-bot@0.0.0 flyway > flyway -c .flyway.js "migrate" Flyway Community Edition 6.3.2 by Redgate Database: jdbc:postgresql://localhost:5432/kaufman_bot_develop (PostgreSQL 13.3) WARNING: Flyway upgrade recommended: PostgreSQL 13.3 is newer than this version of Flyway and support has not been tested. The latest supported version of PostgreSQL is 12. Successfully validated 4 migrations (execution time 00:00.020s) Current version of schema "public": 202203310937 Migrating schema "public" to version 202204030939 - CreateDialogflowTable Successfully applied 1 migration to schema "public" (execution time 00:00.051s)
Pull database to prisma schema and regenerate prisma client
npm run prisma:pull:local
endy@endy-virtual-machine:~/Projects/current/kaufman-bot$ npm run prisma:pull:local > kaufman-bot@0.0.0 prisma:pull:local > export $(xargs < ./.env.local) > /dev/null 2>&1 && export DATABASE_URL=$SERVER_POSTGRES_URL && npm run -- prisma db pull && npm run prisma:generate > kaufman-bot@0.0.0 prisma > prisma "db" "pull" Prisma schema loaded from prisma/schema.prisma Datasource "db": PostgreSQL database "kaufman_bot_develop", schema "public" at "localhost:5432" Introspecting based on datasource defined in prisma/schema.prisma … ✔ Introspected 3 models and wrote them into prisma/schema.prisma in 212ms Run prisma generate to generate Prisma Client. > kaufman-bot@0.0.0 prisma:generate > npm run -- prisma generate > kaufman-bot@0.0.0 prisma > prisma "generate" Prisma schema loaded from prisma/schema.prisma ✔ Generated Prisma Client (3.11.1 | library) to ./node_modules/@prisma/client in 205ms You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client import { PrismaClient } from '@prisma/client' const prisma = new PrismaClient()
New version of prisma schema
prisma/schema.prisma
generator client { provider = "prisma-client-js" binaryTargets = ["native", "linux-musl"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id(map: "PK_USERS") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid telegramId String @unique(map: "UQ_USERS__TELEGRAM_ID") @db.VarChar(64) langCode String @default("en") @db.VarChar(64) debugMode Boolean @default(false) DialogflowSession DialogflowSession[] } model migrations { installed_rank Int @id(map: "__migrations_pk") version String? @db.VarChar(50) description String @db.VarChar(200) type String @db.VarChar(20) script String @db.VarChar(1000) checksum Int? installed_by String @db.VarChar(100) installed_on DateTime @default(now()) @db.Timestamp(6) execution_time Int success Boolean @@index([success], map: "__migrations_s_idx") @@map("__migrations") } model DialogflowSession { id String @id(map: "PK_DIALOGFLOW_SESSION") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid userId String @db.Uuid projectId String @db.VarChar(512) sessionId String @db.Uuid requestsMetadata Json @default("[]") responsesMetadata Json @default("[]") createdAt DateTime @default(now()) @db.Timestamp(6) updatedAt DateTime @default(now()) @db.Timestamp(6) User User @relation(fields: [userId], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_DIALOGFLOW_SESSION__USER_ID") @@unique([userId, projectId, sessionId], map: "UQ_DIALOGFLOW_SESSION") }
Create nx lib
npm run -- nx g @nrwl/nest:lib dialogflow/server
endy@endy-virtual-machine:~/Projects/current/kaufman-bot$ npm run -- nx g @nrwl/nest:lib dialogflow/server > kaufman-bot@0.0.0 nx > nx "g" "@nrwl/nest:lib" "dialogflow/server" CREATE libs/dialogflow/server/README.md CREATE libs/dialogflow/server/.babelrc CREATE libs/dialogflow/server/src/index.ts CREATE libs/dialogflow/server/tsconfig.json CREATE libs/dialogflow/server/tsconfig.lib.json UPDATE tsconfig.base.json CREATE libs/dialogflow/server/project.json UPDATE workspace.json CREATE libs/dialogflow/server/.eslintrc.json CREATE libs/dialogflow/server/jest.config.js CREATE libs/dialogflow/server/tsconfig.spec.json CREATE libs/dialogflow/server/src/lib/dialogflow-server.module.ts
Add types
All work with dialogflow store in database, and it clear if user start work with another commands
libs/dialogflow/server/src/lib/dialogflow-types/dialogflow-session-metadata.ts
import { protos } from '@google-cloud/dialogflow'; export type DialogflowSessionRequestsMetadata = { ts: number; request: protos.google.cloud.dialogflow.v2.IDetectIntentRequest; }[]; export type DialogflowSessionResponsesMetadata = { ts: number; response: protos.google.cloud.dialogflow.v2.IDetectIntentResponse; }[];
Add config interface
libs/dialogflow/server/src/lib/dialogflow-config/dialogflow.config.ts
export const DIALOGFLOW_CONFIG = 'DIALOGFLOW_CONFIG'; export interface DialogflowConfig { name: string; descriptions: string; usage: string[]; spyWords: string[]; projectId: string; }
Add storage service
libs/dialogflow/server/src/lib/dialogflow-services/dialogflow.storage.ts
import { PrismaClientService } from '@kaufman-bot/core/server'; import { Injectable } from '@nestjs/common'; import { DialogflowSessionRequestsMetadata, DialogflowSessionResponsesMetadata, } from '../dialogflow-types/dialogflow-session-metadata'; export type SessionOfUsers = { sessionId: string; responsesMetadata: DialogflowSessionResponsesMetadata; requestsMetadata: DialogflowSessionRequestsMetadata; }; @Injectable() export class DialogflowStorage { private readonly sessionOfUsers: Record<number, SessionOfUsers> = {}; constructor(private readonly prismaClientService: PrismaClientService) {} async getUserSession({ telegramUserId, projectId, }: { telegramUserId: number; projectId: string; }): Promise<SessionOfUsers | null> { const currentSessionOfUsers: SessionOfUsers = this.sessionOfUsers[this.getKey({ telegramUserId, projectId })]; if (currentSessionOfUsers) { return currentSessionOfUsers; } try { const currentFromDatabase = await this.prismaClientService.dialogflowSession.findFirst({ where: { User: { telegramId: telegramUserId.toString() }, projectId, }, rejectOnNotFound: true, }); this.sessionOfUsers[this.getKey({ telegramUserId, projectId })] = { sessionId: currentFromDatabase.sessionId, requestsMetadata: currentFromDatabase.requestsMetadata, responsesMetadata: currentFromDatabase.responsesMetadata, }; return this.sessionOfUsers[this.getKey({ telegramUserId, projectId })]; } catch (error) { return null; } } async appendToUserSession({ telegramUserId, projectId, sessionOfUsers, }: { telegramUserId: number; projectId: string; sessionOfUsers: SessionOfUsers; }): Promise<void> { const user = await this.getUser(telegramUserId); const currentSessionOfUsers: SessionOfUsers = this.sessionOfUsers[this.getKey({ telegramUserId, projectId })] || {}; currentSessionOfUsers.requestsMetadata = [ ...(currentSessionOfUsers.requestsMetadata || []), ...sessionOfUsers.requestsMetadata, ]; currentSessionOfUsers.responsesMetadata = [ ...(currentSessionOfUsers.responsesMetadata || []), ...sessionOfUsers.responsesMetadata, ]; await this.prismaClientService.dialogflowSession.upsert({ create: { userId: user.id, projectId, sessionId: sessionOfUsers.sessionId, // eslint-disable-next-line @typescript-eslint/no-explicit-any requestsMetadata: currentSessionOfUsers.requestsMetadata as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any responsesMetadata: currentSessionOfUsers.responsesMetadata as any, }, update: { // eslint-disable-next-line @typescript-eslint/no-explicit-any requestsMetadata: currentSessionOfUsers.requestsMetadata as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any responsesMetadata: currentSessionOfUsers.responsesMetadata as any, }, where: { userId_projectId_sessionId: { projectId, userId: user.id, sessionId: sessionOfUsers.sessionId, }, }, }); this.sessionOfUsers[this.getKey({ telegramUserId, projectId })] = { sessionId: sessionOfUsers.sessionId, requestsMetadata: currentSessionOfUsers.requestsMetadata, responsesMetadata: currentSessionOfUsers.responsesMetadata, }; } private async getUser(telegramUserId: number) { let user; try { user = await this.prismaClientService.user.findFirst({ select: { id: true }, where: { telegramId: telegramUserId.toString() }, rejectOnNotFound: true, }); } catch (error) { user = await this.prismaClientService.user.create({ data: { telegramId: telegramUserId.toString() }, }); } return user; } async setUserSession({ telegramUserId, projectId, sessionOfUsers, }: { telegramUserId: number; projectId: string; sessionOfUsers: SessionOfUsers; }): Promise<void> { const user = await this.getUser(telegramUserId); const currentSessionOfUsers: SessionOfUsers = this.sessionOfUsers[this.getKey({ telegramUserId, projectId })] || {}; currentSessionOfUsers.requestsMetadata = [ ...sessionOfUsers.requestsMetadata, ]; currentSessionOfUsers.responsesMetadata = [ ...sessionOfUsers.responsesMetadata, ]; await this.prismaClientService.dialogflowSession.upsert({ create: { userId: user.id, projectId, sessionId: sessionOfUsers.sessionId, // eslint-disable-next-line @typescript-eslint/no-explicit-any requestsMetadata: currentSessionOfUsers.requestsMetadata as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any responsesMetadata: currentSessionOfUsers.responsesMetadata as any, }, update: { // eslint-disable-next-line @typescript-eslint/no-explicit-any requestsMetadata: currentSessionOfUsers.requestsMetadata as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any responsesMetadata: currentSessionOfUsers.responsesMetadata as any, }, where: { userId_projectId_sessionId: { projectId, userId: user.id, sessionId: sessionOfUsers.sessionId, }, }, }); this.sessionOfUsers[this.getKey({ telegramUserId, projectId })] = { sessionId: sessionOfUsers.sessionId, requestsMetadata: currentSessionOfUsers.requestsMetadata, responsesMetadata: currentSessionOfUsers.responsesMetadata, }; } async resetUserSession({ telegramUserId, projectId, }: { telegramUserId: number; projectId: string; }) { const defaultUserSession = await this.prismaClientService.dialogflowSession.findFirst({ where: { User: { telegramId: telegramUserId.toString() }, projectId, }, }); if (defaultUserSession) { await this.prismaClientService.dialogflowSession.updateMany({ data: { requestsMetadata: [], responsesMetadata: [], }, where: { sessionId: defaultUserSession.sessionId, projectId, }, }); this.sessionOfUsers[this.getKey({ telegramUserId, projectId })] = { sessionId: defaultUserSession.sessionId, requestsMetadata: [], responsesMetadata: [], }; } } private getKey({ telegramUserId, projectId, }: { telegramUserId: number; projectId: string; }) { return `${telegramUserId}_${projectId}`; } }
Add service with command logics
libs/dialogflow/server/src/lib/dialogflow-services/dialogflow.service.ts
import dialogflow, { protos } from '@google-cloud/dialogflow'; import { BotCommandsEnum, BotCommandsProvider, BotCommandsProviderActionMsg, BotCommandsProviderActionResultType, BotСommandsToolsService, OnAfterBotCommands, } from '@kaufman-bot/core/server'; import { DebugService } from '@kaufman-bot/debug-messages/server'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { v4 } from 'uuid'; import { DialogflowConfig, DIALOGFLOW_CONFIG, } from '../dialogflow-config/dialogflow.config'; import { DialogflowStorage } from './dialogflow.storage'; @Injectable() export class DialogflowService implements BotCommandsProvider, OnAfterBotCommands { private readonly logger = new Logger(DialogflowService.name); constructor( @Inject(DIALOGFLOW_CONFIG) private readonly dialogflowConfig: DialogflowConfig, private readonly dialogflowStorage: DialogflowStorage, private readonly botСommandsToolsService: BotСommandsToolsService, private readonly debugService: DebugService ) {} async onAfterBotCommands< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >( result: BotCommandsProviderActionResultType<TMsg>, msg: TMsg, ctx?, defaultHandler?: () => Promise<unknown> ): Promise<{ result: BotCommandsProviderActionResultType<TMsg>; msg: TMsg }> { if (!defaultHandler && result === null) { msg.text = `dialog ${msg.text}`; const dialogResult = await this.onMessage<TMsg>(msg, ctx); if (dialogResult !== null) { return { result: dialogResult, msg }; } } if (result !== null) { this.debugService.sendDebugInfo( msg, ctx, `call:resetUserSession`, this.dialogflowConfig.name ); // reset last session if unhandled with dialog commands await this.dialogflowStorage.resetUserSession({ telegramUserId: msg.from.id, projectId: this.dialogflowConfig.projectId, }); } return { result, msg }; } async onHelp< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg, ctx): Promise<BotCommandsProviderActionResultType<TMsg>> { return await this.onMessage( { ...msg, text: `${this.dialogflowConfig.name} ${BotCommandsEnum.help}`, }, ctx ); } async onMessage< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg, ctx): Promise<BotCommandsProviderActionResultType<TMsg>> { const locale = msg.from?.language_code || 'en'; const spyWord = this.dialogflowConfig.spyWords.find((spyWord) => this.botСommandsToolsService.checkCommands(msg.text, [spyWord], locale) ); if (spyWord) { if ( this.botСommandsToolsService.checkCommands( msg.text, [BotCommandsEnum.help], locale ) ) { return { type: 'markdown', markdown: this.botСommandsToolsService.generateHelpMessage( locale, this.dialogflowConfig.name, this.dialogflowConfig.descriptions, this.dialogflowConfig.usage ), }; } const preparedText = this.botСommandsToolsService.clearCommands( msg.text, [spyWord], locale ); const processedMsg = await this.process(msg, ctx, locale, preparedText); if (typeof processedMsg === 'string') { return { type: 'text', text: processedMsg, }; } if (processedMsg) { return { type: 'message', message: processedMsg }; } this.logger.warn(`Unhandled commands for text: "${msg.text}"`); this.logger.debug(msg); } return null; } private async process< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg, ctx, locale: string, text: string) { const ts = +new Date(); const current = await this.dialogflowStorage.getUserSession({ telegramUserId: msg.from.id, projectId: this.dialogflowConfig.projectId, }); const sessionId = current ? current.sessionId : v4(); const sessionClient = new dialogflow.SessionsClient(); const sessionPath = sessionClient.projectAgentSessionPath( this.dialogflowConfig.projectId, sessionId ); const request: protos.google.cloud.dialogflow.v2.IDetectIntentRequest = { session: sessionPath, queryInput: { text: { text: text, languageCode: locale, }, }, }; const responses = await sessionClient.detectIntent(request); this.debugService.sendDebugInfo( msg, ctx, 'Detected intent', this.dialogflowConfig.name ); const result = responses[0].queryResult; if (!result) { this.debugService.sendDebugInfo( msg, ctx, `Result not set`, this.dialogflowConfig.name ); return null; } this.debugService.sendDebugInfo( msg, ctx, { Query: result.queryText, Response: result.fulfillmentText, }, this.dialogflowConfig.name ); if (result.intent) { if (current) { this.debugService.sendDebugInfo( msg, ctx, `call:appendToUserSession`, this.dialogflowConfig.name ); await this.dialogflowStorage.appendToUserSession({ telegramUserId: msg.from.id, projectId: this.dialogflowConfig.projectId, sessionOfUsers: { sessionId, requestsMetadata: [{ ts, request }], responsesMetadata: [{ ts, response: responses[0] }], }, }); } else { this.debugService.sendDebugInfo( msg, ctx, `call:setUserSession`, this.dialogflowConfig.name ); await this.dialogflowStorage.setUserSession({ telegramUserId: msg.from.id, projectId: this.dialogflowConfig.projectId, sessionOfUsers: { sessionId, requestsMetadata: [{ ts, request }], responsesMetadata: [{ ts, response: responses[0] }], }, }); } this.debugService.sendDebugInfo( msg, ctx, `Intent: ${result.intent.displayName}`, this.dialogflowConfig.name ); } else { this.debugService.sendDebugInfo( msg, ctx, 'No intent matched.', this.dialogflowConfig.name ); } return result.fulfillmentText; } }
Add module
libs/dialogflow/server/src/lib/dialogflow.module.ts
import { BotCommandsModule, BOT_COMMANDS_PROVIDER, PrismaClientModule, } from '@kaufman-bot/core/server'; import { DebugMessagesModule } from '@kaufman-bot/debug-messages/server'; import { DynamicModule, Module } from '@nestjs/common'; import { getText } from 'class-validator-multi-lang'; import { CustomInjectorModule } from 'nestjs-custom-injector'; import { TranslatesModule } from 'nestjs-translates'; import { DialogflowConfig, DIALOGFLOW_CONFIG, } from './dialogflow-config/dialogflow.config'; import { DialogflowService } from './dialogflow-services/dialogflow.service'; import { DialogflowStorage } from './dialogflow-services/dialogflow.storage'; @Module({ imports: [ TranslatesModule, PrismaClientModule, BotCommandsModule, DebugMessagesModule, ], providers: [DialogflowStorage], exports: [ TranslatesModule, PrismaClientModule, BotCommandsModule, DebugMessagesModule, DialogflowStorage, ], }) export class DialogflowModule { static forRoot(config: Pick<DialogflowConfig, 'projectId'>): DynamicModule { return { module: DialogflowModule, imports: [ CustomInjectorModule.forFeature({ imports: [DialogflowModule], providers: [ { provide: DIALOGFLOW_CONFIG, useValue: <DialogflowConfig>{ name: getText('Dialogflow'), usage: [ getText('dialog hello'), getText('ai hello'), getText('debug help'), getText('ai help'), ], descriptions: getText( 'Commands for process request with dialogflow intents' ), spyWords: [getText('dialog'), getText('ai')], ...config, }, }, { provide: BOT_COMMANDS_PROVIDER, useClass: DialogflowService, }, ], exports: [DIALOGFLOW_CONFIG], }), ], }; } }
Update application files
Update AppService
apps/server/src/app/app.service.ts
import { BotСommandsService } from '@kaufman-bot/core/server'; import { Injectable, Logger } from '@nestjs/common'; import { Hears, On, Start, Update } from 'nestjs-telegraf'; import { Context } from 'telegraf'; @Update() @Injectable() export class AppService { private readonly logger = new Logger(AppService.name); constructor(private readonly botСommandsService: BotСommandsService) {} getData(): { message: string } { return { message: 'Welcome to server!' }; } @Start() async startCommand(ctx: Context) { await this.botСommandsService.process(ctx, () => ctx.reply('Welcome')); } @On('sticker') async onSticker(ctx) { await this.botСommandsService.process(ctx, () => ctx.reply('👍')); } @Hears('hi') async hearsHi(ctx: Context) { await this.botСommandsService.process(ctx, () => ctx.reply('Hey there')); } @On('text') async onMessage(ctx) { await this.botСommandsService.process(ctx); } }
Update AppModule
apps/server/src/app/app.module.ts
import { BotCommandsModule, PrismaClientModule, } from '@kaufman-bot/core/server'; import { CurrencyConverterModule } from '@kaufman-bot/currency-converter/server'; import { DebugMessagesModule } from '@kaufman-bot/debug-messages/server'; import { DialogflowModule } from '@kaufman-bot/dialogflow/server'; import { FactsGeneratorModule } from '@kaufman-bot/facts-generator/server'; import { DEFAULT_LANGUAGE, LanguageSwitherModule, } from '@kaufman-bot/language-swither/server'; import { Module } from '@nestjs/common'; import env from 'env-var'; import { TelegrafModule } from 'nestjs-telegraf'; import { getDefaultTranslatesModuleOptions, TranslatesModule, } from 'nestjs-translates'; import { join } from 'path'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [ TelegrafModule.forRoot({ token: env.get('TELEGRAM_BOT_TOKEN').required().asString(), }), PrismaClientModule.forRoot({ databaseUrl: env.get('SERVER_POSTGRES_URL').required().asString(), logging: 'long_queries', maxQueryExecutionTime: 5000, }), TranslatesModule.forRoot( getDefaultTranslatesModuleOptions({ localePaths: [ join(__dirname, 'assets', 'i18n'), join(__dirname, 'assets', 'i18n', 'class-validator-messages'), ], vendorLocalePaths: [join(__dirname, 'assets', 'i18n')], locales: [DEFAULT_LANGUAGE, 'ru'], }) ), BotCommandsModule, LanguageSwitherModule.forRoot(), DebugMessagesModule.forRoot(), CurrencyConverterModule.forRoot(), FactsGeneratorModule.forRoot(), DialogflowModule.forRoot({ projectId: env.get('DIALOGFLOW_PROJECT_ID').required().asString(), }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
Update .env.local
.env.local
TELEGRAM_BOT_TOKEN=1111111:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ROOT_POSTGRES_USER=postgres ROOT_POSTGRES_PASSWORD=postgres ROOT_POSTGRES_URL=postgres://${ROOT_POSTGRES_USER}:${ROOT_POSTGRES_PASSWORD}@localhost:5432/postgres?schema=public SERVER_POSTGRES_URL=postgres://admin_develop:password_develop@localhost:5432/kaufman_bot_develop?schema=public GOOGLE_APPLICATION_CREDENTIALS=google-credentials.json DIALOGFLOW_PROJECT_ID=service-account-urui
Update all ts files and translates
npm run generate
Update all dictionaries with po editor
Convert all po files to json
npm run generate
Add environments and file with google-credentials to github
Add google-credentials.json
Because file is multiline, you must convert it to base 64 string
Add project id
View all envs
Update for deploy
Update github action config
.github/workflows/develop.deploy.yml
name: "deploy" # yamllint disable-line rule:truthy on: push: branches: - feature/73 jobs: migrate: runs-on: [self-hosted, develop-vps] environment: dev steps: - name: Cloning repo uses: actions/checkout@v2 with: fetch-depth: 0 - name: Apply migrations run: | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash . ~/.nvm/nvm.sh nvm --version nvm install v16.13.2 nvm use v16.13.2 npm i --force export POSTGRES_HOST=$(dokku postgres:info global-postgres --internal-ip) export ROOT_POSTGRES_URL=postgres://postgres:${{secrets.ROOT_POSTGRES_PASSWORD}}@${POSTGRES_HOST}:5432/postgres?schema=public export SERVER_POSTGRES_URL=${{secrets.SERVER_POSTGRES_URL}} npm run rucken -- postgres export DATABASE_URL=$SERVER_POSTGRES_URL && npm run migrate dokku config:set --no-restart kaufman-bot SERVER_POSTGRES_URL=$SERVER_POSTGRES_URL dokku config:set --no-restart --global POSTGRES_HOST=global-postgres dokku config:set --no-restart kaufman-bot GOOGLE_APPLICATION_CREDENTIALS=google-credentials.json dokku config:set --no-restart kaufman-bot GOOGLE_CREDENTIALS=${{secrets.GOOGLE_CREDENTIALS}} dokku config:set --no-restart kaufman-bot DIALOGFLOW_PROJECT_ID=${{secrets.DIALOGFLOW_PROJECT_ID}} deploy: needs: [migrate] runs-on: ubuntu-latest environment: dev steps: - name: Cloning repo uses: actions/checkout@v2 with: fetch-depth: 0 - name: Push to dokku uses: dokku/github-action@master with: branch: "feature/73" git_remote_url: "ssh://dokku@${{secrets.HOST}}:22/kaufman-bot" ssh_private_key: ${{secrets.SSH_PRIVATE_KEY}}
Update start sections in package.json
package.json
... "scripts": { ... "start": "echo $GOOGLE_CREDENTIALS | base64 --decode > ./$GOOGLE_APPLICATION_CREDENTIALS && node dist/apps/server/main.js", ... } ...
Check new logic in telegram bot
Help message for dialogflow command
Check work command with use spy word
Check work global handler for all unhandled messages
Check disabled work global handler if set default application handler
Check logic in Russia language
Show debug information of process
In the next post, I will add different multilingual settings for facts commands...
Top comments (0)