Hide system commands from users and add bot description to Telegram bot on NestJS
Tags: kaufmanbot, nestjs, telegam, description
Published: 2022-04-13 12:52:13

## Links

https://github.com/EndyKaufman/kaufman-bot - source code of bot

https://telegram.me/DevelopKaufmanBot - current bot in telegram

## Add category property and usage with admins 

### Append admin property to bot command config
_libs/core/server/src/lib/bot-commands/bot-commands-config/bot-commands.config.ts_
```ts
export const BOT_COMMANDS_CONFIG = Symbol('BOT_COMMANDS_CONFIG');

export interface BotCommandsConfig {
  admins: string[];
  maxRecursiveDepth?: number;
  prepareCommandString?: (command: string) => string;
}

```

### Append enum for basic types
_libs/core/server/src/lib/bot-commands/bot-commands-types/bot-commands-enum.ts_
```ts
import { getText } from 'class-validator-multi-lang';

export const BotCommandsEnum = {
  help: getText('help'),
  get: getText('get'),
  state: getText('state'),
  reset: getText('reset'),
};

export const BotCommandsCategory = {
  system: getText('system'),
  user: getText('user'),
};

```

### Append category property to help message interface
_libs/core/server/src/lib/bot-commands/bot-commands-types/bot-commands-tools-types.interface.ts_
```ts
export interface BotCommandsToolsGenerateHelpMessageOptions {
  locale: string;
  name: string;
  descriptions: string;
  usage: string[];
  contextUsage?: string[];
  customHelpFields?: { [field: string]: string[] };
  category: string;
}

```

### Update bot command tools service
_libs/core/server/src/lib/bot-commands/bot-commands-services/bot-commands-tools.service.ts_
```ts
import { Injectable } from '@nestjs/common';
import { getText } from 'class-validator-multi-lang';
import { isMatch } from 'micromatch';
import { render } from 'mustache';
import { CustomInject } from 'nestjs-custom-injector';
import { TranslatesService, TranslatesStorage } from 'nestjs-translates';
import {
  BotCommandsConfig,
  BOT_COMMANDS_CONFIG,
} from '../bot-commands-config/bot-commands.config';
import { BotCommandsCategory } from '../bot-commands-types/bot-commands-enum';
import { BotCommandsProviderActionMsg } from '../bot-commands-types/bot-commands-provider.interface';
import {
  BotCommandsToolsInterceptor,
  BOT_COMMANDS_TOOLS_INTERCEPTOR,
} from '../bot-commands-types/bot-commands-tools-interceptor.interface';
import { BotCommandsToolsGenerateHelpMessageOptions } from '../bot-commands-types/bot-commands-tools-types.interface';

@Injectable()
export class BotCommandsToolsService {
  @CustomInject(BOT_COMMANDS_TOOLS_INTERCEPTOR, {
    multi: true,
  })
  private botCommandsToolsInterceptors?: BotCommandsToolsInterceptor[];

  @CustomInject(BOT_COMMANDS_CONFIG)
  private botCommandsConfig?: BotCommandsConfig;

  private lowerCaseTranslates?: TranslatesStorage['translates'];

  constructor(
    private readonly translatesStorage: TranslatesStorage,
    private readonly translatesService: TranslatesService
  ) {}

  isAdmin<
    TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg
  >(msg: TMsg) {
    const admins =
      this.botCommandsConfig?.admins.map((admin) =>
        String(admin).trim().toLocaleLowerCase()
      ) || [];
    return (
      admins.includes(msg?.chat?.id.toString() || '') ||
      admins.includes(msg?.from?.id.toString() || '')
    );
  }

  generateHelpMessage<
    TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg
  >(msg: TMsg, options: BotCommandsToolsGenerateHelpMessageOptions) {
    const isAdmin = this.isAdmin(msg);

    if (options.category === BotCommandsCategory.system && !isAdmin) {
      return '';
    }

    if (
      this.botCommandsToolsInterceptors &&
      this.botCommandsToolsInterceptors.length > 0
    ) {
      for (
        let index = 0;
        index < this.botCommandsToolsInterceptors.length;
        index++
      ) {
        const botCommandsToolsInterceptor =
          this.botCommandsToolsInterceptors[index];
        if (botCommandsToolsInterceptor?.interceptHelpMessageOptions) {
          options =
            botCommandsToolsInterceptor.interceptHelpMessageOptions(options);
        }
      }
    }
    const usageWithLocalized = Array.from(
      new Set(
        [
          ...options.usage,
          ...options.usage.map((u) =>
            this.translatesService.translate(u, options.locale)
          ),
        ].filter(Boolean)
      )
    );
    const contextUsageWithLocalized = options.contextUsage
      ? Array.from(
          new Set(
            [
              ...options.contextUsage,
              ...options.contextUsage.map((u) =>
                this.translatesService.translate(u, options.locale)
              ),
            ].filter(Boolean)
          )
        )
      : null;

    const caption = options.name
      ? `__${this.translatesService.translate(options.name, options.locale)}${
          !isAdmin && options.category === BotCommandsCategory.user
            ? ''
            : ` \\(${this.translatesService.translate(
                options.category,
                options.locale
              )}\\)`
        }__`
      : '';
    const descriptions = options.descriptions
      ? this.translatesService.translate(options.descriptions, options.locale)
      : '';
    const usage =
      usageWithLocalized.length > 0
        ? `${this.translatesService.translate(
            getText('usage'),
            options.locale
          )}: ${usageWithLocalized.map((u) => `_${u}_`).join(', ')}`
        : '';
    const contextUsage =
      contextUsageWithLocalized && contextUsageWithLocalized.length > 0
        ? `${this.translatesService.translate(
            getText('usage with context'),
            options.locale
          )}: ${contextUsageWithLocalized.map((u) => `_${u}_`).join(', ')}`
        : '';
    const customHelpFields = Object.keys(options.customHelpFields || {}).map(
      (customHelpFieldKey) =>
        `${this.translatesService.translate(
          customHelpFieldKey,
          options.locale
        )}: ${(options.customHelpFields?.[customHelpFieldKey] || [])
          .map((u) => `_${u}_`)
          .join(', ')}`
    );

    const replayHelpMessage = [
      caption,
      descriptions,
      usage,
      contextUsage,
      ...customHelpFields,
    ]
      .filter(Boolean)
      .join('\n');
    return replayHelpMessage;
  }

  clearCommands(text: string | undefined, commands: string[], locale: string) {
    const words = (text || '').split(' ');
    const lowerCasedWords = words.map((c) => c.toLowerCase());
    const lowerCasedCommands = commands.map((c) => c.toLowerCase());
    lowerCasedCommands.forEach((command) => {
      lowerCasedWords.forEach((word, wordIndex) => {
        if (command === word) {
          words[wordIndex] = '';
        }
        if (`/${command}` === word) {
          words[wordIndex] = '';
        }
        if (this.translateByLowerCase(command, locale) === word) {
          words[wordIndex] = '';
        }
        if (`/${this.translateByLowerCase(command, locale)}` === word) {
          words[wordIndex] = '';
        }
      });
    });
    return words.join(' ').split('  ').join(' ').trim();
  }

  checkCommands(text: string | undefined, commands: string[], locale?: string) {
    const lowerCasedText = this.prepareCommandString(
      (text || '').toLocaleLowerCase()
    );
    const lowerCasedCommands = commands
      .map((c) => this.prepareCommandString(c).toLocaleLowerCase().split('|'))
      .reduce((acc, val) => acc.concat(val), []);
    if (
      lowerCasedCommands.find(
        (command) =>
          lowerCasedText.includes(command) ||
          lowerCasedText.includes(`/${command}`)
      )
    ) {
      return true;
    }
    if (
      lowerCasedCommands.find(
        (command) =>
          lowerCasedText.includes(
            this.prepareCommandString(
              this.translateByLowerCase(command, locale)
            )
          ) ||
          lowerCasedText.includes(
            `/${this.prepareCommandString(
              this.translateByLowerCase(command, locale)
            )}`
          )
      )
    ) {
      return true;
    }
    return false;
  }

  checkMicromatchCommands(text: string | undefined, commands: string[]) {
    const lowerCasedText = this.prepareCommandString(
      (text || '').toLocaleLowerCase()
    );
    const lowerCasedCommands = commands
      .map((c) => this.prepareCommandString(c.toLocaleLowerCase()).split('|'))
      .reduce((acc, val) => acc.concat(val), []);
    if (
      lowerCasedCommands.find((command) =>
        isMatch(lowerCasedText, `${command}`)
      )
    ) {
      return true;
    }
    return false;
  }

  prepareHelpString(text: string | undefined) {
    return (text || '').split('*').join('\\*');
  }

  getRandomItem<T>(items: T[]) {
    return items[Math.floor(Math.random() * items.length)];
  }

  capitalizeFirstLetter(text: string | undefined, locale: string) {
    const [first, ...rest] = (text || '').trim();
    return (first || '').toLocaleUpperCase(locale) + rest.join('');
  }

  getLocale<
    TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg
  >(msg: TMsg, defaultValue: string) {
    let locale = msg?.from?.language_code;
    if (
      !locale ||
      !Object.keys(this.translatesStorage.translates).find((key) =>
        locale?.includes(key)
      )
    ) {
      locale = defaultValue;
    }
    return locale;
  }

  private translateByLowerCase(
    key: string,
    locale?: string,
    context: unknown = {}
  ) {
    this.initLowerCaseTranslates();
    const lowerCaseKey = key.toLowerCase();
    if (!this.lowerCaseTranslates) {
      throw new Error(`lowerCaseTranslates not set`);
    }
    const value =
      (locale && this.lowerCaseTranslates?.[locale]?.[lowerCaseKey]) ||
      lowerCaseKey;
    return value ? render(value, context) : value;
  }

  private prepareCommandString(command: string): string {
    if (this.botCommandsConfig?.prepareCommandString) {
      return this.botCommandsConfig.prepareCommandString(command);
    }
    return command || '';
  }

  private initLowerCaseTranslates() {
    if (!this.lowerCaseTranslates) {
      this.lowerCaseTranslates = {};
      Object.keys(this.translatesStorage.translates).forEach(
        (translateLocale) => {
          if (!this.lowerCaseTranslates) {
            throw new Error(`lowerCaseTranslates not set`);
          }
          this.lowerCaseTranslates[translateLocale] = {};
          Object.keys(
            this.translatesStorage.translates[translateLocale]
          ).forEach((translateKey) => {
            if (!this.lowerCaseTranslates?.[translateLocale]) {
              throw new Error(
                `lowerCaseTranslates by locale "${translateLocale}" not set`
              );
            }
            this.lowerCaseTranslates[translateLocale][
              translateKey.toLowerCase()
            ] =
              this.translatesStorage.translates[translateLocale][
                translateKey
              ].toLowerCase();
          });
        }
      );
    }
  }
}

```

### Append category property to all commands as example of quotes
_libs/quotes-generator/server/src/lib/quotes-generator.module.ts_
```ts
import {
  BotCommandsCategory,
  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 { TranslatesModule } from 'nestjs-translates';
import { QuotesGeneratorService } from './quotes-generator-services/quotes-generator.service';

@Module({
  imports: [TranslatesModule, BotCommandsModule],
  exports: [TranslatesModule, BotCommandsModule],
})
export class QuotesGeneratorModule {
  static forRoot(): DynamicModule {
    return {
      module: QuotesGeneratorModule,
      imports: [
        ScraperModule.forRoot({
          title: getText('Quotes generator'),
          name: 'quotes',
          descriptions: getText(
            'Command to generate text with a random quotes'
          ),
          usage: [
            getText('get quote'),
            getText('get quotes'),
            getText('quotes help'),
          ],
          contextUsage: [getText('more'), getText('next')],
          contentSelector:
            'forismatic > quote > quotetext, forismatic > quote > quoteauthor',
          spyWords: [getText('quotes'), getText('quote')],
          removeWords: [getText('get'), getText('please')],
          uri: 'https://api.forismatic.com/api/1.0/?method=getQuote&format=xml&lang={{locale}}',
          contentCodepage: 'utf8',
          headers: [{}],
          category: BotCommandsCategory.user,
        }),
      ],
      providers: [
        {
          provide: BOT_COMMANDS_PROVIDER,
          useClass: QuotesGeneratorService,
        },
      ],
      exports: [ScraperModule],
    };
  }
}

```
![Append category property to all commands as example of quotes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jl3w1rthkt55t0h38ukb.png)
 
### Update app module for use admin from envs
_apps/server/src/app/app.module.ts_
```ts
import { BotInGroupsModule } from '@kaufman-bot/bot-in-groups/server';
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 { FirstMeetingModule } from '@kaufman-bot/first-meeting/server';
import { JokesGeneratorModule } from '@kaufman-bot/jokes-generator/server';
import {
  DEFAULT_LANGUAGE,
  LanguageSwitherModule,
} from '@kaufman-bot/language-swither/server';
import { QuotesGeneratorModule } from '@kaufman-bot/quotes-generator/server';
import { ShortCommandsModule } from '@kaufman-bot/short-commands/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';

const TELEGRAM_BOT_WEB_HOOKS_DOMAIN = env
  .get('TELEGRAM_BOT_WEB_HOOKS_DOMAIN')
  .asString();
const TELEGRAM_BOT_WEB_HOOKS_PATH = env
  .get('TELEGRAM_BOT_WEB_HOOKS_PATH')
  .asString();

@Module({
  imports: [
    TelegrafModule.forRoot({
      token: env.get('TELEGRAM_BOT_TOKEN').required().asString(),
      launchOptions: {
        dropPendingUpdates: true,
        ...(TELEGRAM_BOT_WEB_HOOKS_DOMAIN && TELEGRAM_BOT_WEB_HOOKS_PATH
          ? {
              webhook: {
                domain: TELEGRAM_BOT_WEB_HOOKS_DOMAIN,
                hookPath: TELEGRAM_BOT_WEB_HOOKS_PATH,
              },
            }
          : {}),
      },
    }),
    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', 'getText'),
          join(__dirname, 'assets', 'i18n', 'class-validator-messages'),
        ],
        vendorLocalePaths: [join(__dirname, 'assets', 'i18n')],
        locales: [DEFAULT_LANGUAGE, 'ru'],
      })
    ),
    DebugMessagesModule.forRoot(),
    BotCommandsModule.forRoot({
      admins: env.get('TELEGRAM_BOT_ADMINS').default('').asArray(','),
      prepareCommandString: (command?: string) =>
        (command || '').split('ё').join('е'),
    }),
    ShortCommandsModule.forRoot({
      commands: {
        en: {
          '*joke*': `get jokes`,
          '*quote*|*thought*|*wisdom*': 'get quotes',
          '*fact*|history': 'get facts',
          'forgot me': 'meet reset',
          '*what you can do*|faq': 'help',
          'disable debug': 'debug off',
          'enable debug': 'debug on',
        },
        ru: {
          '*joke*|*шутка|*шутку|*шутки|пошути*|шути|рассмеши|смешинки|смешинка':
            'get jokes',
          '*quote*|*thought|*wisdom*|цитата|дай цитату|цитируй|*мысль|*мудрость|*залечи*':
            'get quotes',
          '*fact*|history|история|*историю|*факты': 'get facts',
          'forgot me|забудь меня': 'meet reset',
          '*what you can do*|faq|*что ты умеешь*|справка': 'help',
          'disable debug|выключи дебаг': 'debug off',
          'enable debug|включи дебаг': 'debug on',
        },
      },
    }),
    BotInGroupsModule.forRoot({
      botNames: {
        en: ['Endy', 'Kaufman'],
        ru: ['Энди', 'Endy', 'Kaufman', 'Енди', 'Кауфман'],
      },
      botMeetingInformation: {
        en: [`Hello! I'm Endy 😉`, 'Hello!', 'Hello 🖖'],
        ru: [`Всем привет! я Энди 😉`, `Всем привет!`, 'Всем привет 🖖'],
      },
    }),
    LanguageSwitherModule.forRoot(),
    CurrencyConverterModule.forRoot(),
    FactsGeneratorModule.forRoot(),
    QuotesGeneratorModule.forRoot(),
    JokesGeneratorModule.forRoot(),
    FirstMeetingModule.forRoot({ botName: { en: 'Endy', ru: 'Энди' } }),
    DialogflowModule.forRoot({
      projectId: env.get('DIALOGFLOW_PROJECT_ID').required().asString(),
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

```

### Append TELEGRAM_BOT_ADMINS to env file
_.env.local_
```ts
TELEGRAM_BOT_TOKEN=1111111:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
TELEGRAM_BOT_WEB_HOOKS_DOMAIN=kaufman-bot.site15.ru
TELEGRAM_BOT_WEB_HOOKS_PATH=/webhook
TELEGRAM_BOT_ADMINS=102375526
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
```

### Prepare files and translate dictionaries
> npm run generate

### Translate all need words
![Translate all need words](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/812wdl5a3ir5kte7k5av.png)

### Convert po translate dictionaries files to json
> npm run generate

### Commit changes, wait end of deploy, for test without admin role
![Commit changes, wait end of deploy, for test without admin role](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aqo9vkbjdprzm2vm5p3j.png)

### Check from telegram, we must see only user commands
![Check from telegram, we must see only user commands](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mgerpfhwjb72rcwxxqyj.png)
 

### Append description of bot with @BotFather

Show bot info
![Show bot info](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mt11tacyo98c1i5pqgoo.png)
 
![Show bot info 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kx0r7edmfs1eayg3em3z.png)

Set description
```sh
Currency converter
convert 1 usd to eur, converter help

Facts generator
get facts, get fact, facts help

Quotes generator
get quote, get quotes, quotes help

Jokes generator
get joke, get jokes, jokes help
```
![Set description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4b8ebhfk06sd9ujktbup.png)

Result
![Set description result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/smplmfjo9k263xj8nmjq.png)
 
Set about text
![Set about text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/in69l9d64gcyypbl3vr0.png)

Result
![Set about text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0mu6l1jb3h60i5xbxcjb.png)
 
Set avatar
![Set avatar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wpymk6oozqwo2x45f7xl.png)
 
### Check from telegram

First open bot
![First open bot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/39e9j5ykm6hcooafd70q.png)

Show bot info
![Show bot info](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xonwhtwon063n9mq8emo.png)
 
Send help commands
![Send help commands](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gm5h4onz5cnmbc7ymzr2.png)
 
## Add admin id to env

### Get your id
![Get your id](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ke8ofsmkqouusa8k1bk8.png)

### Append env with this id to github settings
![Append env with this id to github settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h91n5a3idh6u3kjyca1a.png)
 
### Update deploy config
_.github/workflows/develop.deploy.yml_
```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
          export DEPLOY_DATE=$(date +'%Y.%m.%d %H:%M:%S')
          export DEPLOY_COMMIT=$GITHUB_SHA
          export DEPLOY_VERSION=$(node -pe "require('./package.json')['version']")
          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}}
          dokku config:set --no-restart kaufman-bot TELEGRAM_BOT_WEB_HOOKS_DOMAIN=${{secrets.TELEGRAM_BOT_WEB_HOOKS_DOMAIN}}
          dokku config:set --no-restart kaufman-bot TELEGRAM_BOT_WEB_HOOKS_PATH=${{secrets.TELEGRAM_BOT_WEB_HOOKS_PATH}}
          dokku config:set --no-restart kaufman-bot TELEGRAM_BOT_ADMINS=${{secrets.TELEGRAM_BOT_ADMINS}}
          dokku config:set --no-restart kaufman-bot DEPLOY_DATE="$DEPLOY_DATE"
          dokku config:set --no-restart kaufman-bot DEPLOY_COMMIT=$DEPLOY_COMMIT
          dokku config:set --no-restart kaufman-bot DEPLOY_VERSION=$DEPLOY_VERSION

  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}}

```

## Check new logic in telegram bot

### Show botinfo in personal user chat
![Show botinfo in personal user chat](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qlzvytfpyi3j79bjab5t.png)
 
### Show botinfo in groups chat
![Show botinfo in groups chat](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sfcw3otq1p72n22ofnqm.png)

### Show help in personal chat
![Show help in personal chat](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sbkyckki58tvax8iljqn.png)

### Show help in groups chat
![Show help in groups chat](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4p7qhceuxvgp8bmx59kr.png)
 

In next post I append semver and create changelog with released features and fixes, and maybe I close issue https://github.com/EndyKaufman/kaufman-bot/issues/73...