NESTJSの上でTelegramボットで再帰的な文脈仕事を使用している短いコマンドと例をつくってください


リンク


https://github.com/EndyKaufman/kaufman-bot - ボットのソースコード
https://telegram.me/DevelopKaufmanBot - 現在のロボット

作品の説明


このポストでは、短いコマンドモジュールの作成について説明します.
ポストの終わりには、再帰的な文脈作業を使用する例をあげます

短いコマンドを追加


このコマンドモジュールは、他のコマンドハンドラを実行するための短いバージョンを使用する必要があります

ライブラリの作成


npm run -- nx g @nrwl/nest:lib short-commands/server


[email protected]:~/Projects/current/kaufman-bot$ npm run -- nx g @nrwl/nest:lib short-commands/server

> [email protected] nx
> nx "g" "@nrwl/nest:lib" "short-commands/server"

CREATE libs/short-commands/server/README.md
CREATE libs/short-commands/server/.babelrc
CREATE libs/short-commands/server/src/index.ts
CREATE libs/short-commands/server/tsconfig.json
CREATE libs/short-commands/server/tsconfig.lib.json
UPDATE tsconfig.base.json
CREATE libs/short-commands/server/project.json
UPDATE workspace.json
CREATE libs/short-commands/server/.eslintrc.json
CREATE libs/short-commands/server/jest.config.js
CREATE libs/short-commands/server/tsconfig.spec.json
CREATE libs/short-commands/server/src/lib/short-commands-server.module.ts

ショートコマンドを追加


LIBS/ショートコマンド/サーバ/src/lib/shortコマンドconfig/shortコマンド.設定.TS
export const SHORT_COMMANDS_CONFIG = Symbol('SHORT_COMMANDS_CONFIG');

export interface ShortCommandsConfig {
  title: string;
  name: string;
  descriptions: string;
  usage: string[];
  spyWords: string[];
  commands: { [langCode: string]: { [text: string]: string } };
}

サービスを追加する


LIBS/ショートコマンド/サーバ/src/lib/ショートコマンドサービス/ショートコマンド.サービスTS
import {
  BotCommandsEnum,
  BotCommandsProvider,
  BotCommandsProviderActionMsg,
  BotCommandsProviderActionResultType,
  BotСommandsToolsService,
  OnBeforeBotCommands,
} from '@kaufman-bot/core/server';
import { DEFAULT_LANGUAGE } from '@kaufman-bot/language-swither/server';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { getText } from 'class-validator-multi-lang';
import { TranslatesService, TranslatesStorage } from 'nestjs-translates';
import {
  ShortCommandsConfig,
  SHORT_COMMANDS_CONFIG,
} from '../short-commands-config/short-commands.config';

@Injectable()
export class ShortCommandsService
  implements BotCommandsProvider, OnBeforeBotCommands
{
  private readonly logger = new Logger(ShortCommandsService.name);

  constructor(
    @Inject(SHORT_COMMANDS_CONFIG)
    private readonly shortCommandsConfig: ShortCommandsConfig,
    private readonly botСommandsToolsService: BotСommandsToolsService,
    private readonly translatesStorage: TranslatesStorage,
    private readonly translatesService: TranslatesService
  ) {}

  async onBeforeBotCommands<
    TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg
  >(msg: TMsg): Promise<TMsg> {
    const locale = msg.from.language_code;
    const text = msg.text;
    if (locale && this.shortCommandsConfig) {
      const shortCommands = this.shortCommandsConfig.commands[locale] || {};
      const matchedCommands = Object.keys(shortCommands)
        .filter((commands) =>
          this.botСommandsToolsService.checkCommands(text, commands.split('|'))
        )
        .map((commands) => shortCommands[commands]);
      if (matchedCommands?.length > 0) {
        msg.text = matchedCommands[0];
      }
    }
    return msg;
  }

  async onHelp<
    TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg
  >(msg: TMsg): Promise<BotCommandsProviderActionResultType<TMsg>> {
    return await this.onMessage({
      ...msg,
      text: `${this.shortCommandsConfig.name} ${BotCommandsEnum.help}`,
    });
  }

  async onMessage<
    TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg
  >(msg: TMsg): Promise<BotCommandsProviderActionResultType<TMsg>> {
    let locale = msg.from?.language_code;
    if (
      !locale ||
      !Object.keys(this.translatesStorage.translates).find((key) =>
        locale?.includes(key)
      )
    ) {
      locale = DEFAULT_LANGUAGE;
    }

    const spyWord = this.shortCommandsConfig.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',
          message: msg,
          markdown: this.botСommandsToolsService.generateHelpMessage({
            locale,
            name: this.shortCommandsConfig.title,
            descriptions: this.shortCommandsConfig.descriptions,
            usage: this.shortCommandsConfig.usage,
          }),
        };
      }

      if (
        this.botСommandsToolsService.checkCommands(
          msg.text,
          [BotCommandsEnum.state],
          locale
        )
      ) {
        const detectedLang =
          Object.keys(this.shortCommandsConfig.commands).filter(
            (langCode) =>
              this.shortCommandsConfig.commands[langCode] && langCode === locale
          )[0] || DEFAULT_LANGUAGE;
        const commands = this.shortCommandsConfig.commands[detectedLang] || {};
        const aliases = Object.keys(commands);

        const markdown = [
          `__${this.translatesService.translate(
            getText('List of short commands:'),
            locale
          )}__`,
          ...aliases.map((alias) =>
            locale
              ? [
                  `${this.translatesService.translate(
                    getText('alias'),
                    locale
                  )}: ${alias
                    .split('|')
                    .map((u) => `_${u}_`)
                    .join(', ')}`,
                  `${this.translatesService.translate(
                    getText('command'),
                    locale
                  )}: ${commands[alias]}\n`,
                ].join('\n')
              : ''
          ),
        ]
          .filter(Boolean)
          .join('\n');
        return {
          type: 'markdown',
          message: msg,
          markdown,
        };
      }

      this.logger.warn(`Unhandled commands for text: "${msg.text}"`);
      this.logger.debug(msg);
    }
    return null;
  }
}

ショートコマンドモジュール


LIBS/ショートコマンド/サーバ/src/lib/ショートコマンド.モジュールです.TS
import {
  BotCommandsModule,
  BOT_COMMANDS_PROVIDER,
} from '@kaufman-bot/core/server';
import { DynamicModule, Module } from '@nestjs/common';
import { getText } from 'class-validator-multi-lang';
import { TranslatesModule } from 'nestjs-translates';
import {
  ShortCommandsConfig,
  SHORT_COMMANDS_CONFIG,
} from './short-commands-config/short-commands.config';
import { ShortCommandsService } from './short-commands-services/short-commands.service';

@Module({
  imports: [TranslatesModule, BotCommandsModule],
  exports: [TranslatesModule, BotCommandsModule],
})
export class ShortCommandsModule {
  static forRoot(config: Pick<ShortCommandsConfig, 'commands'>): DynamicModule {
    return {
      module: ShortCommandsModule,
      providers: [
        {
          provide: SHORT_COMMANDS_CONFIG,
          useValue: <ShortCommandsConfig>{
            title: getText('Short commands'),
            name: 'scmd',
            usage: [getText('scmd state'), getText('scmd help')],
            descriptions: getText(
              'Shortened versions of commands for quick launch'
            ),
            spyWords: [getText('scmd')],
            commands: config.commands,
          },
        },
        {
          provide: BOT_COMMANDS_PROVIDER,
          useClass: ShortCommandsService,
        },
      ],
    };
  }
}

ファイルの準備


npm run generate


すべての単語を翻訳



変換po辞書のjsonへの準備ファイル


npm run generate


ショートコマンドをモジュールに追加する


アプリ/サーバ/src/app/app.モジュールです.TS
@Module({
  imports: [
    ...
    BotCommandsModule.forRoot(),
    LanguageSwitherModule.forRoot(),
    DebugMessagesModule.forRoot(),
    ShortCommandsModule.forRoot({
      commands: {
        en: {
          joke: 'get jokes',
          'quote|thought|wisdom': 'get quotes',
          'facts|fact|history': 'get facts',
        },
        ru: {
          'joke|jokes|шутка|шутки|пошути|шути|рассмеши|смешинки|смешинка':
            'get jokes',
          'quote|thought|wisdom|цитата|цитаты|цитируй|мысль|мудрость|залечи':
            'get quotes',
          'facts|fact|history|факт|факты|история': 'get facts',
        },
      },
    }),
    ...
  ],
  ...
})
export class AppModule {}

新しいロジックをチェックしてください


共通ヘルプメッセージ

短いコマンドの状態を取得する

短いコマンドの使用方法を試してください

再帰文脈処理


私はこのロジックを作成するすべてのプロセスを書いていません.なぜなら、それは非常に多くのコードを持っているからです

アップデート


LIBS/ジョークジェネレータ/サーバ/src/lib/ジョークジェネレータジェネレータ/ジョークジェネレータ.サービスTS
import {
  BotCommandsEnum,
  BotCommandsProvider,
  BotCommandsProviderActionMsg,
  BotCommandsProviderActionResultType,
  BotСommandsToolsService,
  OnContextBotCommands,
} from '@kaufman-bot/core/server';
import { ScraperService, ScraperConfig,
  SCRAPER_CONFIG, } from '@kaufman-bot/html-scraper/server';
import { DEFAULT_LANGUAGE } from '@kaufman-bot/language-swither/server';
import { Injectable } from '@nestjs/common';
import { TranslatesStorage } from 'nestjs-translates';

@Injectable()
export class JokesGeneratorService
  implements BotCommandsProvider, OnContextBotCommands
{
  constructor(
    @Inject(SCRAPER_CONFIG)
    private readonly scraperConfig: ScraperConfig,
    private readonly scraperService: ScraperService,
    private readonly botСommandsToolsService: BotСommandsToolsService,
    private readonly translatesStorage: TranslatesStorage
  ) {}

  async onContextBotCommands<
    TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg
  >(msg: TMsg): Promise<BotCommandsProviderActionResultType<TMsg>> {
    const locale = msg.from?.language_code;
    if (
      this.botСommandsToolsService.checkCommands(
        msg.text,
        [getText('more'), getText('next')],
        locale
      )
    ) {
      msg.text = `${BotCommandsEnum.get} ${this.scraperConfig.name}`;
      return {
        type: 'message',
        message: msg,
      };
    }
    return null;
  }

  async onHelp<
    TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg
  >(msg: TMsg) {
    const locale = msg.from?.language_code;
    if (
      Object.keys(this.translatesStorage.translates).find((key) =>
        locale?.includes(key)
      ) &&
      !locale?.includes(DEFAULT_LANGUAGE)
    ) {
      return null;
    }
    return await this.scraperService.onHelp(msg);
  }

  async onMessage<
    TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg
  >(msg: TMsg): Promise<BotCommandsProviderActionResultType<TMsg>> {
    const locale = msg.from?.language_code;
    if (
      Object.keys(this.translatesStorage.translates).find((key) =>
        locale?.includes(key)
      ) &&
      !locale?.includes(DEFAULT_LANGUAGE)
    ) {
      return null;
    }
    if (
      this.botСommandsToolsService.checkCommands(
        msg.text,
        [...Object.keys(BotCommandsEnum)],
        locale
      )
    ) {
      const result = await this.scraperService.onMessage(msg);
      try {
        if (result?.type === 'text') {
          return {
            type: 'text',
            message: msg,
            text: result.text.split('\\"').join('"').split('\n').join(' '),
          };
        }
        return result;
      } catch (err) {
        console.debug(result);
        console.error(err, err.stack);
        throw err;
      }
    }
    return null;
  }
}

JumesgeneratorModuleを更新する


LIBS/ジョークジェネレータ/サーバ/src/lib/ジョークジェネレータ.モジュールです.TS
import {
  BotCommandsModule,
  BOT_COMMANDS_PROVIDER,
} from '@kaufman-bot/core/server';
import { ScraperModule } from '@kaufman-bot/html-scraper/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 { JokesGeneratorService } from './jokes-generator-services/jokes-generator.service';
import { RuJokesGeneratorService } from './jokes-generator-services/ru-jokes-generator.service';

@Module({
  imports: [TranslatesModule, BotCommandsModule],
  exports: [TranslatesModule, BotCommandsModule],
})
export class JokesGeneratorModule {
  static forRoot(): DynamicModule {
    return {
      module: JokesGeneratorModule,
      imports: [
        CustomInjectorModule.forFeature({
          imports: [
            ScraperModule.forRoot({
              title: getText('Jokes generator'),
              name: 'jokes',
              descriptions: getText(
                'Command to generate text with a random jokes'
              ),
              usage: [getText('get joke'), getText('jokes help')],
              contextUsage: [getText('more'), getText('next')],
              contentSelector: '#joke > table > tbody > tr > td',
              spyWords: [getText('jokes'), getText('joke')],
              removeWords: [getText('get'), getText('please')],
              uri: 'https://randstuff.ru/joke/',
              contentCodepage: 'utf8',
            }),
          ],
          providers: [
            {
              provide: BOT_COMMANDS_PROVIDER,
              useClass: RuJokesGeneratorService,
            },
          ],
          exports: [ScraperModule],
        }),
        CustomInjectorModule.forFeature({
          imports: [
            ScraperModule.forRoot({
              title: getText('Jokes generator'),
              name: 'jokes',
              descriptions: getText(
                'Command to generate text with a random jokes'
              ),
              usage: [getText('get joke'), getText('jokes help')],
              contextUsage: [getText('more'), getText('next')],
              contentSelector: 'data > joke',
              spyWords: [getText('jokes'), getText('joke')],
              removeWords: [getText('get'), getText('please')],
              uri: 'https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,religious,political,racist,sexist,explicit&type=single&format=xml',
              contentCodepage: 'utf8',
            }),
          ],
          providers: [
            {
              provide: BOT_COMMANDS_PROVIDER,
              useClass: JokesGeneratorService,
            },
          ],
          exports: [ScraperModule],
        }),
      ],
    };
  }
}

新しいロジックをチェックしてください


最後のアクティブなコマンドの情報を保存して、レスポンスを生成するために使用する例

コンテキストモードで使用するための単語

次のポストでは、例の再帰的な仕事で簡単なコマンドモジュールを追加します.