引言
大家好 在本文中,我将介绍使用NodeJS的电报消息传递服务和VK社交网络的聊天机器人。
在这一点上,许多读者应该提出类似的内容:“多长时间!” 或“又是什么?!”
是的,类似的出版物已经备受关注。 但是,尽管如此,我认为这篇文章还是有用的。 简要介绍一下该机器人的技术实现:
- 作为该应用程序的框架, NestJS框架越来越受欢迎。
- 用于与Telegram API交互的telegraf库。
- 用于与VK API交互的node-vk-bot-api库。
- 用于组织数据存储层的Typeorm库。
- 使用mocha和chai 断言库进行测试。
- CI使用Travis CI进行测试,并使用GitHub Actions部署Docker映像。
作为附带工作,让我们尝试与Viber成为我们的机器人朋友,使其在多种消息传递服务中通用。
对于那些想知道它带来了什么的人,欢迎猫。
问题陈述
作为有益的体育锻炼,我更喜欢足球。 当然,在业余水平上。 作为vk,viber和电报中几个频道和聊天的成员,您通常必须看到以下图片:

我们在这里看到的是:
- “ P”添加了自己。
- 在他之后,又增加了3个人,其中有一个“ C”。
- “ P”添加了他的同志“ Dimon”。
- 此后,“ P”并不是太懒惰,并计算出目前已经有多达5个人。
- 但是,由于我也决定跑步并添加自己,因此此信息已不再适用。
在特别被忽略的情况下,人们可以打“ +”号然后取消其参与,邀请不参加聊天的朋友,或者注册其他聊天参与者并进行其他活动。 最后,这导致了一个事实,当每个人最终都参加比赛时,事实证明:
- Vasya设置+1不仅意味着他本人,还意味着他的朋友Gosh。
- Kolya写道他会来,但组织者Spiridon用他的眼睛认为只是优点。
结果,我们的团队数量可能不相等,“额外的天哪”并出乎意料地出现了科里亚。
为了避免此类冲突,我编写了一个简单的机器人,该机器人可以直观地显示即将到来的游戏中参与者的组成,并且可以方便地在其中添加/删除。
因此,在我的基本实现中,我认为该机器人应该能够:
- 嵌入到任意数量的聊天中。 分别存储每个聊天的状态。
- 使用命令/ event_add,使用某些信息和一个空球员列表创建一个新的活动事件。 先前的活动事件应变为非活动状态。
- / event_remove命令应取消当前活动的事件。
- / add命令应将一个新成员添加到当前活动的事件中。 此外,如果调用该命令时没有任何其他文本,则应该添加键入该命令的用户。 否则,将添加一个人,该人的名字在调用命令时指定。
- / remove命令根据为/ add命令描述的规则从参与者列表中删除一个人。
- / info命令允许您查看谁已经注册了游戏。
- 最好让漫游器以与播放器配置相同的语言(或默认情况下为英语)进行响应。
要快速查看最终发生的情况,您可以立即转到最后一部分“结果和结论”。 好吧,如果您对实现细节感兴趣,那么下面将对这些信息进行足够详细的描述。
设计开发
在设计应用程序时,我想到了以下方案:

在这里,主要思想是将与各种消息传递系统一起使用的所有细节引入适当的适配器中,并封装与实现相应API的库进行交互的逻辑,从而将数据处理并将其转换为单一形式。
界面和消息模型
对于消息,可以设计以下IMessage接口, 该类可以通过存储用于各种系统的传入消息数据的类以及使用该数据的方法来满足:
export interface IMessage { chatId: number; lang: string; text: string; fullText: string; command: string; name: string; getReplyStatus: () => string; getReplyData: () => any; setStatus: (status: string) => IMessage; withData: (data: any) => IMessage; answer: (args: any) => string | void; }
实现此接口的基类BaseMessage具有以下形式:
基本消息 import {IMessage} from '../message/i-message'; export class BaseMessage implements IMessage { public chatId: number; public lang: string; public text: string; public fullText: string; public command: string; protected firstName: string; protected lastName: string; protected replyStatus: string; protected replyData: any; get name(): string { const firstName: string = this.firstName || ''; const lastName: string = this.lastName || ''; return `${firstName} ${lastName}`.trim(); } public getReplyStatus(): string { return this.replyStatus; } public getReplyData(): any { return this.replyData; } public setStatus(status: string): IMessage { this.replyStatus = status; return this; } public withData(data: any): IMessage { this.replyData = data; return this; } public answer(args: any): string | void { throw new Error('not implemented'); } } }
电报的消息类:
电报消息 import {BaseMessage} from '../message/base.message'; import {IMessage} from '../message/i-message'; export class TelegramMessage extends BaseMessage implements IMessage { private ctx: any; constructor(ctx) { super(); this.ctx = ctx; const {message} = this.ctx.update; this.chatId = message.chat.id; this.fullText = message.text; this.command = this.ctx.command; this.text = this.fullText.replace(`/${this.command}`, ''); this.lang = message.from.language_code; this.firstName = message.from.first_name; this.lastName = message.from.last_name; } public answer(args: any): string | void { return this.ctx.replyWithHTML(args); } }
对于VK:
VK消息 import {BaseMessage} from '../message/base.message'; import {IMessage} from '../message/i-message'; export class VKMessage extends BaseMessage implements IMessage { private ctx: any; constructor(ctx) { super(); this.ctx = ctx; const {message} = this.ctx; this.chatId = this.getChatId(this.ctx); this.fullText = message.text; this.command = this.ctx.command; this.text = this.fullText.replace(`/${this.command}`, ''); this.lang = 'ru'; this.firstName = message.from.first_name; this.lastName = message.from.last_name; } public answer(args: any) { const answer: string = `${args}`.replace(/<\/?(strong|i)>/gm, ''); this.ctx.reply(answer); } private getChatId({message, bot}): number { const peerId: number = +`${message.peer_id}`.replace(/[0-9]0+/, ''); const groupId: number = bot.settings.group_id; return peerId + groupId; } }
您要注意的是:
chatId是聊天的唯一标识符。 对于Telegram,它明确地出现在每个消息的结构中。 但是,对于VK,没有明确的此类标识符。 怎么样 要回答这个问题,您需要了解Bot在VK中的功能。 在此系统中,组通信中的漫游器代表其启动的社区。 即 每个漫游器消息都包含一个group_id社区标识符。 另外,在本例中,消息中包含peer_id (更多详细信息,请参见),作为组对话的标识符。 基于这两个标识符,您可以构建您的聊天标识符,例如,如下所示:
private getChatId({message, bot}): number { const peerId: number = +(`${message.peer_id}`.replace(/[0-9]0+/, '')); const groupId: number = bot.settings.group_id; return peerId + groupId; }
此方法当然不是完美的,但适用于当前任务。
fullText和text 。 如变量名所示 , fullText包含消息的全文 ,而text仅包含命令名称后的部分。
这很方便,因为它允许您使用现成的文本字段来解析其中包含的辅助信息,例如事件的日期或播放器的名称。
- 与Telegram不同,VK消息不支持用于突出显示
<b>
或<i>
类的文本的标记集,因此,在响应时,必须使用正则表达式删除这些标记。
接收和发送消息的适配器
在为电报和VK消息创建接口和数据结构之后,就该实现用于处理传入事件的服务了。

这些服务必须初始化第三方模块,以与Telegram和VK API进行交互,并启动长轮询机制,并且在从消息传递服务接收数据时,输入命令与系统中发生的内部事件之间具有连接。
电报服务 import Telegraf from 'telegraf'; import {Injectable} from '@nestjs/common'; import * as SocksAgent from 'socks5-https-client/lib/Agent'; import {ConfigService} from '../common/config.service'; import {AppEmitter} from '../common/event-bus.service'; import {TelegramMessage} from './telegram.message'; @Injectable() export class TelegramService { private bot: Telegraf<any>; constructor(config: ConfigService, appEmitter: AppEmitter) { const botToken: string = config.get('TELEGRAM_BOT_TOKEN'); this.bot = config.get('TELEGRAM_USE_PROXY') ? new Telegraf(botToken, { telegram: {agent: this.getProxy(config)}, }) : new Telegraf(botToken); this.getCommandEventMapping(appEmitter).forEach(([command, event]) => { this.bot.command(command, ctx => { ctx.command = command; appEmitter.emit(event, new TelegramMessage(ctx)); }); }); } public launch(): void { this.bot.launch(); } private getProxy(config: ConfigService): SocksAgent { return new SocksAgent({ socksHost: config.get('TELEGRAM_PROXY_HOST'), socksPort: config.get('TELEGRAM_PROXY_PORT'), socksUsername: config.get('TELEGRAM_PROXY_LOGIN'), socksPassword: config.get('TELEGRAM_PROXY_PASSWORD'), }); } private getCommandEventMapping( appEmitter: AppEmitter, ): Array<[string, string]> { return [ ['event_add', appEmitter.EVENT_ADD], ['event_remove', appEmitter.EVENT_REMOVE], ['info', appEmitter.EVENT_INFO], ['add', appEmitter.PLAYER_ADD], ['remove', appEmitter.PLAYER_REMOVE], ]; } }
VK服务 import * as VkBot from 'node-vk-bot-api'; import {Injectable} from '@nestjs/common'; import {ConfigService} from '../common/config.service'; import {AppEmitter} from '../common/event-bus.service'; import {VKMessage} from './vk.message'; @Injectable() export class VKService { private bot: VkBot<any>; constructor(config: ConfigService, appEmitter: AppEmitter) { const botToken: string = config.get('VK_TOKEN'); this.bot = new VkBot(botToken); this.getCommandEventMapping(appEmitter).forEach(([command, event]) => { this.bot.command(`/${command}`, async ctx => { const [from] = await this.bot.execute('users.get', { user_ids: ctx.message.from_id, }); ctx.message.from = from; ctx.command = command; appEmitter.emit(event, new VKMessage(ctx)); }); }); } public launch(): void { this.bot.startPolling(); } private getCommandEventMapping( appEmitter: AppEmitter, ): Array<[string, string]> { return [ ['event_add', appEmitter.EVENT_ADD], ['event_remove', appEmitter.EVENT_REMOVE], ['info', appEmitter.EVENT_INFO], ['add', appEmitter.PLAYER_ADD], ['remove', appEmitter.PLAYER_REMOVE], ]; } }
您要注意的是:
- 由于访问Telegram服务(hi Roskomnadzor)的某些问题,有必要选择使用提供socks5-https-client软件包的代理。
- 处理事件时,会将字段与命令的值一起添加到上下文中,该命令的值包含接收到该消息的文本。
- 对于VK,您必须使用单独的API调用分别下载发送消息的用户的数据:
const [from] = await this.bot.execute('users.get', { user_ids: ctx.message.from_id, });
资料模型
为了实现机器人的声明功能,三个模型就足够了:
Chat
-聊天或对话。Event
-安排在特定日期和时间的聊天事件。Player
-参加活动的参与者或参与者。

使用typeorm库的模型实现为:
聊天室 import { Entity, PrimaryGeneratedColumn, Column, OneToMany, JoinColumn, Index, } from 'typeorm'; import {Event} from './event'; @Entity() export class Chat { @PrimaryGeneratedColumn() id: number; @Index({unique: true}) @Column() chatId: number; @OneToMany(type => Event, event => event.chat) @JoinColumn() events: Event[]; }
大事记 import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, JoinColumn, } from 'typeorm'; import {Chat} from './chat'; import {Player} from './player'; @Entity() export class Event { @PrimaryGeneratedColumn() id: number; @Column() date: Date; @Column() active: boolean; @ManyToOne(type => Chat, chat => chat.events) chat: Chat; @OneToMany(type => Player, player => player.event) @JoinColumn() players: Player[]; }
播放器 import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, } from 'typeorm'; import {Event} from './event'; @Entity() export class Player { @PrimaryGeneratedColumn() id: number; @Column() name: string; @ManyToOne(type => Event, event => event.players) @JoinColumn() event: Event; }
在这里,有必要在操纵聊天 , 事件和玩家模型方面再次讨论该机器人针对各个团队的机制。
- 收到任何命令后,将使用
chatId
检查数据库中是否存在聊天。 如果没有相应的记录,则创建它。 - 为了简化逻辑,每次聊天只能有一个活动事件( Event )。 即
/event_add
命令时,将在系统中创建一个具有指定日期的新活动事件
当前活动事件(如果存在)变为非活动状态。 /event_remove
在当前聊天中找到活动事件并将其停用。/info
在当前聊天中查找一个活动事件,显示有关此事件的信息以及玩家列表( Player )。/add
和/remove
活动事件,并从中添加和删除播放器。
用于处理数据的相应服务如下:
存储服务 import {Injectable} from '@nestjs/common'; import {InjectConnection} from '@nestjs/typeorm'; import {Connection, Repository, UpdateResult} from 'typeorm'; import {Chat} from './models/chat'; import {Event} from './models/event'; import {Player} from './models/player'; @Injectable() export class StorageService { private chatRepository: Repository<Chat>; private eventRepository: Repository<Event>; private playerRepository: Repository<Player>; constructor(@InjectConnection() private readonly dbConnection: Connection) { this.chatRepository = this.dbConnection.getRepository(Chat); this.eventRepository = this.dbConnection.getRepository(Event); this.playerRepository = this.dbConnection.getRepository(Player); } public get connection() { return this.dbConnection; } public async ensureChat(chatId: number): Promise<Chat> { let chat: Chat = await this.chatRepository.findOne({chatId}); if (chat) { return chat; } chat = new Chat(); chat.chatId = chatId; return this.chatRepository.save(chat); } public markChatEventsInactive(chat: Chat): Promise<UpdateResult> { return this.dbConnection .createQueryBuilder() .update(Event) .set({active: false}) .where({chat}) .execute(); } public appendChatActiveEvent(chat: Chat, date: Date): Promise<Event> { const event: Event = new Event(); event.chat = chat; event.active = true; event.date = date; return this.eventRepository.save(event); } public findChatActiveEvent(chat: Chat): Promise<Event | null> { return this.eventRepository.findOne({where: {chat, active: true}}); } public getPlayers(event: Event): Promise<Player[]> { return this.playerRepository.find({where: {event}}); } public findPlayer(event: Event, name: string): Promise<Player | null> { return this.playerRepository.findOne({where: {event, name}}); } public addPlayer(event: Event, name: string): Promise<Player> { const player: Player = new Player(); player.event = event; player.name = name; return this.playerRepository.save(player); } public removePlayer(player: Player): Promise<Player> { return this.playerRepository.remove(player); } }
范本回应
除了上述用于处理数据的StorageService服务之外,还需要实现专用的TemplateService服务,该服务目前的任务是:
- 下载并编译车把模板,以获取不同语言的各种命令的答案选项。
- 根据当前事件,响应状态和用户语言选择合适的模板。
- 使用命令获得的数据填充模板。
模板服务 import * as path from 'path'; import * as fs from 'fs'; import * as handlebars from 'handlebars'; import {readDirDeepSync} from 'read-dir-deep'; import {Injectable, Logger} from '@nestjs/common'; export interface IParams { action: string; status: string; lang?: string; } @Injectable() export class TemplateService { private readonly DEFAULT_LANG: string = 'en'; private readonly TEMPLATE_PATH: string = 'templates'; private logger: Logger; private templatesMap: Map<string, (d: any) => string>; constructor(logger: Logger) { this.logger = logger; this.load(); } public apply(params: IParams, data: any): string { this.logger.log( `apply template: ${params.action} ${params.status} ${params.lang}`, ); let template = this.getTemplate(params); if (!template) { params.lang = this.DEFAULT_LANG; template = this.getTemplate(params); } if (!template) { throw new Error('template-not-found'); } return template(data); } private getTemplate(params: IParams): (data: any) => string { const {lang, action, status} = params; return this.templatesMap.get(this.getTemplateKey(lang, action, status)); } private load() { const templatesDir: string = path.join( process.cwd(), this.TEMPLATE_PATH, ); const templateFileNames: string[] = readDirDeepSync(templatesDir); this.templatesMap = templateFileNames.reduce((acc, fileName) => { const template = fs.readFileSync(fileName, {encoding: 'utf-8'}); const [, lang, action, status] = fileName .replace(/\.hbs$/, '') .split('/'); return acc.set( this.getTemplateKey(lang, action, status), handlebars.compile(template), ); }, new Map()); } private getTemplateKey( lang: string, action: string, status: string, ): string { return `${lang}-${action}-${status}`; } }
您要注意的是:
模板文件位于项目根目录下单独的./templates
目录中,并由语言,操作(命令)和响应状态构成。
- templates - en - event_add - invalid_date.hbs - success.hbs - event_info - ru
初始化应用程序后,所有响应模板都将加载到内存中,并使用唯一标识该模板的键填充Map:
private getTemplateKey(lang: string, action: string, status: string): string { return `${lang}-${action}-${status}`; }
当前,该系统具有2套模板:俄语和英语。
在缺少当前事件语言所需的模板的情况下,以默认语言(英语)提供后备广告。
车把模板本身,例如:
Player <strong>{{name}}</strong> will take part in the game List of players: {{#each players}} {{index}}: <i>{{name}}</i> {{/each}} Total: <strong>{{total}}</strong>
同时包含传统的占位符和一组有效的标记,用于格式化Telegram中的响应。
命令处理服务(操作)
现在已经介绍了基本支持服务,是时候继续实施该机器人的业务逻辑本身了。 这些模块之间的连接示意图如下:

BaseAction基类的任务包括:
- 从AppEmitter订阅特定事件。
- 事件处理的整体功能,即:
- 搜索将要处理的团队中现有的聊天室或创建新的聊天室
- 调用
doAction
模板方法doAction
该方法doAction
实现对于继承BaseAction的每个类都是单独的。 - 使用TemplateService将模板应用于生成的
doAction
响应。
基本动作 import {Injectable, Logger} from '@nestjs/common'; import {IMessage} from '../message/i-message'; import {ConfigService} from '../common/config.service'; import {AppEmitter} from '../common/event-bus.service'; import {TemplateService} from '../common/template.service'; import {StorageService} from '../storage/storage.service'; import {Chat} from '../storage/models/chat'; @Injectable() export class BaseAction { protected appEmitter: AppEmitter; protected config: ConfigService; protected logger: Logger; protected templateService: TemplateService; protected storageService: StorageService; protected event: string; constructor( config: ConfigService, appEmitter: AppEmitter, logger: Logger, templateService: TemplateService, storageService: StorageService, ) { this.config = config; this.logger = logger; this.appEmitter = appEmitter; this.templateService = templateService; this.storageService = storageService; this.setEvent(); this.logger.log(`subscribe on "${this.event}" event`); this.appEmitter.on(this.event, this.handleEvent.bind(this)); } protected setEvent(): void { throw new Error('not implemented'); } protected async doAction(chat: Chat, message: IMessage): Promise<IMessage> { throw new Error('not implemented'); } private async handleEvent(message: IMessage) { try { this.logger.log(`"${this.event}" event received`); const chatId: number = message.chatId; const chat: Chat = await this.storageService.ensureChat(chatId); message = await this.doAction(chat, message); message.answer( this.templateService.apply( { action: this.event, status: message.getReplyStatus(), lang: message.lang, }, message.getReplyData(), ), ); } catch (error) { this.logger.error(error); message.answer(error.message); } } }
BaseAction子类的任务是执行输入的doAction
方法:
- 在基类中创建或定义的聊天
- 符合IMessage协议的对象。
作为执行此方法的结果,还将返回IMessage ,但是IMessage具有确定的状态,用于选择正确的模板以及将参与响应模板的数据。
EventAddAction import {Injectable} from '@nestjs/common'; import * as statuses from './statuses'; import {parseEventDate, formatEventDate} from '../common/utils'; import {BaseAction} from './base.action'; import {Chat} from '../storage/models/chat'; import {Event} from '../storage/models/event'; import {IMessage} from '../message/i-message'; @Injectable() export class EventAddAction extends BaseAction { protected setEvent(): void { this.event = this.appEmitter.EVENT_ADD; } protected async doAction(chat: Chat, message: IMessage): Promise<IMessage> { await this.storageService.markChatEventsInactive(chat); const eventDate: Date = parseEventDate(message.text.trim()); if (!eventDate) { return message.setStatus(statuses.STATUS_INVALID_DATE); } const event: Event = await this.storageService.appendChatActiveEvent( chat, eventDate, ); return message.setStatus(statuses.STATUS_SUCCESS).withData({ date: formatEventDate(event.date), }); } }
EventRemoveAction import {Injectable} from '@nestjs/common'; import * as statuses from './statuses'; import {formatEventDate} from '../common/utils'; import {BaseAction} from './base.action'; import {Chat} from '../storage/models/chat'; import {Event} from '../storage/models/event'; import {IMessage} from '../message/i-message'; @Injectable() export class EventRemoveAction extends BaseAction { protected setEvent(): void { this.event = this.appEmitter.EVENT_REMOVE; } protected async doAction(chat: Chat, message: IMessage): Promise<IMessage> { const activeEvent: Event = await this.storageService.findChatActiveEvent( chat, ); await this.storageService.markChatEventsInactive(chat); if (activeEvent) { return message.setStatus(statuses.STATUS_SUCCESS).withData({ date: formatEventDate(activeEvent.date), }); } else { return message.setStatus(statuses.STATUS_NO_EVENT); } } }
EventInfoAction import {Injectable, Logger} from '@nestjs/common'; import * as statuses from './statuses'; import {formatEventDate} from '../common/utils'; import {ConfigService} from '../common/config.service'; import {AppEmitter} from '../common/event-bus.service'; import {TemplateService} from '../common/template.service'; import {StorageService} from '../storage/storage.service'; import {BaseAction} from './base.action'; import {PlayerHelper} from './player.helper'; import {Chat} from '../storage/models/chat'; import {Event} from '../storage/models/event'; import {Player} from '../storage/models/player'; import {IMessage} from '../message/i-message'; @Injectable() export class EventInfoAction extends BaseAction { private playerHelper: PlayerHelper; constructor( config: ConfigService, appEmitter: AppEmitter, logger: Logger, templateService: TemplateService, playerHelper: PlayerHelper, storageService: StorageService, ) { super(config, appEmitter, logger, templateService, storageService); this.playerHelper = playerHelper; } protected setEvent(): void { this.event = this.appEmitter.EVENT_INFO; } protected async doAction(chat: Chat, message: IMessage): Promise<IMessage> { const activeEvent: Event = await this.storageService.findChatActiveEvent( chat, ); if (!activeEvent) { return message.setStatus(statuses.STATUS_NO_EVENT); } const players: Player[] = await this.storageService.getPlayers( activeEvent, ); return message.setStatus(statuses.STATUS_SUCCESS).withData({ date: formatEventDate(activeEvent.date), ...(await this.playerHelper.getPlayersList(activeEvent)), }); } }
PlayerAddAction import {Injectable, Logger} from '@nestjs/common'; import * as statuses from './statuses'; import {ConfigService} from '../common/config.service'; import {AppEmitter} from '../common/event-bus.service'; import {TemplateService} from '../common/template.service'; import {StorageService} from '../storage/storage.service'; import {BaseAction} from './base.action'; import {PlayerHelper} from './player.helper'; import {Chat} from '../storage/models/chat'; import {Event} from '../storage/models/event'; import {Player} from '../storage/models/player'; import {IMessage} from '../message/i-message'; @Injectable() export class PlayerAddAction extends BaseAction { private playerHelper: PlayerHelper; constructor( config: ConfigService, appEmitter: AppEmitter, logger: Logger, templateService: TemplateService, playerHelper: PlayerHelper, storageService: StorageService, ) { super(config, appEmitter, logger, templateService, storageService); this.playerHelper = playerHelper; } protected setEvent(): void { this.event = this.appEmitter.PLAYER_ADD; } protected async doAction(chat: Chat, message: IMessage): Promise<IMessage> { const activeEvent: Event = await this.storageService.findChatActiveEvent( chat, ); if (!activeEvent) { return message.setStatus(statuses.STATUS_NO_EVENT); } const name: string = this.playerHelper.getPlayerName(message); const existedPlayer: Player = await this.storageService.findPlayer( activeEvent, name, ); if (existedPlayer) { return message .setStatus(statuses.STATUS_ALREADY_ADDED) .withData({name}); } const newPlayer: Player = await this.storageService.addPlayer( activeEvent, name, ); return message.setStatus(statuses.STATUS_SUCCESS).withData({ name: newPlayer.name, ...(await this.playerHelper.getPlayersList(activeEvent)), }); } }
PlayerRemoveAction import {Injectable, Logger} from '@nestjs/common'; import * as statuses from './statuses'; import {ConfigService} from '../common/config.service'; import {AppEmitter} from '../common/event-bus.service'; import {TemplateService} from '../common/template.service'; import {StorageService} from '../storage/storage.service'; import {BaseAction} from './base.action'; import {PlayerHelper} from './player.helper'; import {Chat} from '../storage/models/chat'; import {Event} from '../storage/models/event'; import {Player} from '../storage/models/player'; import {IMessage} from '../message/i-message'; @Injectable() export class PlayerRemoveAction extends BaseAction { private playerHelper: PlayerHelper; constructor( config: ConfigService, appEmitter: AppEmitter, logger: Logger, templateService: TemplateService, playerHelper: PlayerHelper, storageService: StorageService, ) { super(config, appEmitter, logger, templateService, storageService); this.playerHelper = playerHelper; } protected setEvent(): void { this.event = this.appEmitter.PLAYER_REMOVE; } protected async doAction(chat: Chat, message: IMessage): Promise<IMessage> { const activeEvent: Event = await this.storageService.findChatActiveEvent( chat, ); if (!activeEvent) { return message.setStatus(statuses.STATUS_NO_EVENT); } const name: string = this.playerHelper.getPlayerName(message); const existedPlayer: Player = await this.storageService.findPlayer( activeEvent, name, ); if (!existedPlayer) { return message .setStatus(statuses.STATUS_NO_PLAYER) .withData({name}); } await this.storageService.removePlayer(existedPlayer); return message.setStatus(statuses.STATUS_SUCCESS).withData({ name, ...(await this.playerHelper.getPlayersList(activeEvent)), }); } }
您要注意的是:
/add
和/remove
命令添加/删除名称在命令后面的播放器。 如果未指定名称,则会添加/删除呼叫团队的球员。- 响应
/add
, /remove
和/info
命令,将显示活动事件的玩家更新列表。
实现(1)和(2)所需的功能已移至特殊的辅助类:
PlayerHelper import {Injectable} from '@nestjs/common'; import {StorageService} from '../storage/storage.service'; import {Event} from '../storage/models/event'; import {Player} from '../storage/models/player'; import {IMessage} from '../message/i-message'; @Injectable() export class PlayerHelper { protected storageService: StorageService; constructor(storageService: StorageService) { this.storageService = storageService; } public getPlayerName(message: IMessage) { const name = message.text.trim(); return name.length > 0 ? name : message.name; } public async getPlayersList(event: Event) { const players: Player[] = await this.storageService.getPlayers(event); return { total: players.length, players: players.map((player, index) => ({ index: index + 1, name: player.name, })), }; } }
全部放在一起
如本文开头所述, NestJS框架用作应用程序框架。 一次,我很幸运能亲自参加这个奇妙图书馆创建者的报告 。
许多NodeJS样板文件,手册和库经常因不提供任何合理的初始化策略和模块之间的连接而带来麻烦。 随着应用程序的增长,在没有适当注意的情况下,代码库变成了模块之间的require-s混杂,通常甚至导致循环依赖或关系,而原则上不应这样做。 Dependency Injection awilix , NestJS .
DI , NodeJS , , .

.
TELEGRAM_BOT_TOKEN
— Telegram .TELEGRAM_USE_PROXY
— TelegramAPI.TELEGRAM_PROXY_HOST
— TelegramAPI.TELEGRAM_PROXY_PORT
— TelegramAPI.TELEGRAM_PROXY_LOGIN
— TelegramAPI.TELEGRAM_PROXY_PASSWORD
— TelegramAPI.VK_TOKEN
— VK .DATABASE_URL
— . production .
:
TELEGRAM_BOT_TOKEN
Telegram.VK_TOKEN
— , VK . 即 , . .DATABASE_URL
— production PostgreSQL. DBaaS elephantsql .
CI
" - — ". , TravisCI :
language: node_js node_js: - '8' - '10' - '12' script: - npm run lint - npm run build - npm run test:cov after_script: - npm install -g codecov - codecov
codecov .
Docker Docker Hub Gonf CI/CD GitHub Actions Python . Continuous Deployment, Github:
name: Release on: release: types: [published] jobs: build: runs-on: ubuntu-latest env: LOGIN: ${{ secrets.DOCKER_LOGIN }} NAME: ${{ secrets.DOCKER_NAME }} steps: - uses: actions/checkout@v1 - name: Install Node.js uses: actions/setup-node@v1 - name: Login to docker.io run: echo ${{ secrets.DOCKER_PWD }} | docker login -u ${{ secrets.DOCKER_LOGIN }} --password-stdin - name: npm install and build run: npm ci && npm run build - name: Build the Docker image run: docker build -t $LOGIN/$NAME:${GITHUB_REF:10} -f Dockerfile . - name: Push image to docker.io run: docker push $LOGIN/$NAME:${GITHUB_REF:10}
. Telegram :
.

.

.

VK <b>
<i>
.
.

.

Viber , . , Viber webhooks long-polling. , Viber . . (?) .
Telegram @Tormozz48bot
.
Github . , .
PS .