Bot gegen Gebühr. Mit neuen Technologien zum Fußball gehen

Einleitung


Hallo allerseits. In diesem Artikel beschreibe ich meinen Chatbot für den Telegrammnachrichtendienst und das soziale Netzwerk von VK unter Verwendung von NodeJS.


An dieser Stelle sollten viele Leser etwas ausbrechen: "Wie lange!" oder "Was nochmal?!"
Ja, ähnliche Veröffentlichungen gab es bereits auf einer habr darunter. Trotzdem glaube ich, dass der Artikel nützlich sein wird. Kurz darauf, was die technische Implementierung des Bots darstellt:


  1. Als Framework für die Anwendung gewinnt das NestJS- Framework zunehmend an Beliebtheit.
  2. Die Telegraf- Bibliothek für die Interaktion mit der Telegramm-API.
  3. Die node-vk-bot-api- Bibliothek für die Interaktion mit der VK-API.
  4. Typeorm- Bibliothek zum Organisieren der Datenspeicherschicht.
  5. Tests mit Mokka und der Chai- Assert- Bibliothek.
  6. CI mit Travis CI zum Testen und GitHub-Aktionen zum Bereitstellen von Docker-Images.

Als Nebenjob wollen wir versuchen, unsere Bot-Freunde mit Viber zusammenzubringen, um es so universell für die Verwendung in mehreren Messaging-Diensten zu machen.


Für diejenigen, die wissen wollen, was daraus wurde, ist cat willkommen.


Erklärung des Problems


Als nützliche körperliche Aktivität bevorzuge ich Fußball. Natürlich auf Amateurebene. Als Mitglied mehrerer Kanäle und Chats in VK, Viber und Telegramm muss man oft folgendes Bild sehen:


Bild


Was wir hier sehen:


  1. "P" fügte sich hinzu.
  2. Nach ihm kamen 3 weitere Personen hinzu, darunter ein bestimmtes "C".
  3. "P" fügte hinzu, sein Kamerad namens "Dimon".
  4. Danach war "P" nicht zu faul und errechnete, dass es im Moment bereits 5 Leute gibt.
  5. Diese Informationen waren jedoch nicht lange relevant, da ich mich auch dazu entschlossen habe, sie selbst auszuführen und hinzuzufügen.

In besonders vernachlässigten Fällen können Personen ein „+“ setzen und dann ihre Teilnahme stornieren, Freunde einladen, die nicht aus dem Chat kommen, oder sich für andere Chat-Teilnehmer anmelden und andere Aktivitäten ausführen. Am Ende führt dies dazu, dass, wenn irgendwann jeder zum Spielen kommt, sich herausstellt, dass:


  1. Vasya hat +1 gesetzt, was nicht nur ihn selbst, sondern auch seinen Freund Gosh bedeutet.
  2. Kolya schrieb, dass er kommen würde, aber der Veranstalter Spiridon betrachtete mit seinen Augen nur die Pluspunkte.

Infolgedessen haben wir möglicherweise eine ungleiche Anzahl von Teams, "Extra Gosh" und unerwartet erschien Kolya.


Um solche Kollisionen zu vermeiden, habe ich einen einfachen Bot geschrieben, mit dem sich die Zusammensetzung der Teilnehmer im kommenden Spiel visuell darstellen lässt und der bequem hinzugefügt / gelöscht werden kann.


In meiner Grundimplementierung sollte der Bot meiner Meinung nach in der Lage sein:


  1. In beliebig viele Chats eingebettet sein. Speichern Sie Ihren Status für jeden Chat separat.
  2. Erstellen Sie mit dem Befehl / event_add ein neues aktives Ereignis mit bestimmten Informationen und einer leeren Liste von Spielern. Das zuvor aktive Ereignis sollte inaktiv werden.
  3. Der Befehl / event_remove sollte das derzeit aktive Ereignis abbrechen.
  4. Der Befehl / add sollte dem aktuell aktiven Ereignis ein neues Mitglied hinzufügen. Wenn der Befehl ohne zusätzlichen Text aufgerufen wird, sollte außerdem derjenige hinzugefügt werden, der den Befehl eingegeben hat. Andernfalls wird eine Person hinzugefügt, deren Name beim Aufruf des Befehls angegeben wird.
  5. Mit dem Befehl / remove wird eine Person gemäß den für den Befehl / add beschriebenen Regeln aus der Teilnehmerliste entfernt.
  6. Mit dem Befehl / info können Sie sehen, wer sich bereits für das Spiel angemeldet hat.
  7. Es ist äußerst wünschenswert, dass der Bot in derselben Sprache wie der konfigurierte Player antwortet (oder standardmäßig in Englisch).

Um schnell zu sehen, was am Ende passiert ist, können Sie sofort zum letzten Abschnitt "Ergebnis und Schlussfolgerungen" gehen. Wenn Sie an Implementierungsdetails interessiert sind, werden diese Informationen im Folgenden ausreichend detailliert beschrieben.


Design und Entwicklung


Beim Entwerfen der Anwendung tauchte in meinem Kopf das folgende Schema auf:


Bild


Hier bestand die Hauptidee darin, alle Einzelheiten der Arbeit mit verschiedenen Nachrichtensystemen in die entsprechenden Adapter zu integrieren und die Logik der Interaktion mit Bibliotheken zusammenzufassen, die die entsprechenden APIs implementieren, Daten verarbeiten und in ein einziges Formular bringen.


Schnittstellen- und Nachrichtenmodelle


Für Nachrichten konnte die folgende IMessage- Schnittstelle entworfen werden, die von Klassen erfüllt wird, die eingehende Nachrichtendaten für verschiedene Systeme und Methoden zum Arbeiten mit diesen Daten speichern:


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

Die Basisklasse BaseMessage , die diese Schnittstelle implementiert, hat die folgende Form:


Basisnachricht
 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'); } } } 

Nachrichtenklasse für Telegramm:


Telegrammnachricht
 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); } } 

und für VK:


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

Was Sie beachten sollten:


  1. chatId ist die eindeutige Kennung für den Chat. Für Telegramm kommt es explizit in die Struktur jeder Nachricht. Bei VK gibt es eine solche Kennung jedoch nicht explizit. Wie zu sein Um diese Frage zu beantworten, müssen Sie verstehen, wie der Bot in VK funktioniert. In diesem System handelt ein Bot in der Gruppenkorrespondenz im Namen der Community, für die er gestartet wurde. Das heißt Jede Bot-Nachricht enthält eine group_id- Community- ID . Zusätzlich kommt peer_id in der Nachricht (weitere Details können hier gelesen werden ) als die Kennung der Gruppenkonversation in unserem Fall. Basierend auf diesen beiden IDs können Sie Ihre Chat-ID wie folgt erstellen:


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

    Diese Methode ist sicherlich nicht perfekt, aber auf die aktuelle Aufgabe anwendbar.


  2. fullText und text . Wie der Name der Variablen angibt , enthält fullText den vollständigen Text der Nachricht, während text nur den Teil enthält, der nach dem Namen des Befehls steht.



Dies ist praktisch, da Sie das vorgefertigte Textfeld verwenden können, um die darin enthaltenen Zusatzinformationen zu analysieren, z. B. das Datum des Ereignisses oder den Namen des Players.


  1. Im Gegensatz zu Telegramm unterstützen VK-Nachrichten keine Tags zum Hervorheben von Text wie <b> oder <i> . Daher müssen diese Tags beim Antworten mit regulären Ausdrücken gelöscht werden.

Adapter zum Empfangen und Senden von Nachrichten


Nach der Erstellung der Schnittstellen- und Datenstrukturen für Telegramm- und VK-Nachrichten ist es an der Zeit, Dienste für die Verarbeitung eingehender Ereignisse zu implementieren.


Bild


Diese Dienste sollten Module von Drittanbietern für die Interaktion mit den Telegramm- und VK-APIs initialisieren und Mechanismen für lange Abfragen starten sowie eine Verbindung zwischen eingehenden Befehlen und internen Ereignissen herstellen, die im System auftreten, wenn Daten von Messaging-Diensten empfangen werden.


Telegrammservice
 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], ]; } } 

VKService
 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], ]; } } 

Was Sie beachten sollten:


  1. Aufgrund bestimmter Probleme beim Zugriff auf den Telegrammdienst (hi Roskomnadzor) musste optional ein Proxy verwendet werden, der das Paket socks5-https-client bereitstellt .
  2. Bei der Verarbeitung eines Ereignisses wird dem Kontext ein Feld mit dem Wert des Befehls hinzugefügt, mit dessen Text die Nachricht empfangen wurde.
  3. Für VK müssen Sie die Daten des Benutzers, der die Nachricht gesendet hat, separat über einen separaten API-Aufruf herunterladen:
     const [from] = await this.bot.execute('users.get', { user_ids: ctx.message.from_id, }); 

Datenmodell


Um die deklarierte Funktionalität des Bots zu implementieren, genügten drei Modelle:


  1. Chat - Chat oder Unterhaltung.
  2. Event - Ein Ereignis für einen Chat, der für ein bestimmtes Datum und eine bestimmte Uhrzeit geplant ist.
  3. Player - Teilnehmer oder Spieler, der an dem Ereignis teilnimmt.

Datenschema


Implementierungen von Modellen unter Verwendung der typeorm- Bibliothek sind:


Chat
 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[]; } 

Ereignis
 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[]; } 

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

Hier ist es notwendig, den Mechanismus des Bots für verschiedene Teams im Hinblick auf die Manipulation von Chat- , Event- und Player- Modellen noch einmal zu diskutieren.


  1. Nach Erhalt eines Befehls wird chatId in der DB ein Chat mit der Chat- chatId . Wenn es keinen entsprechenden Datensatz gibt, wird er erstellt.
  2. Um die Logik zu vereinfachen, kann jeder Chat nur ein aktives Ereignis ( Ereignis ) haben. Das heißt Beim Empfang des /event_add wird im System ein neues aktives Ereignis mit dem angegebenen Datum erstellt.
    Das aktuell aktive Ereignis (falls vorhanden) wird inaktiv.
  3. /event_remove findet das aktive Ereignis im aktuellen Chat und deaktiviert es.
  4. /info findet ein aktives Ereignis im aktuellen Chat, zeigt Informationen zu diesem Ereignis und eine Liste der Spieler an ( Player ).
  5. /add und /remove arbeiten mit dem aktiven Event und fügen Spieler hinzu und entfernen sie daraus.

Der entsprechende Dienst zum Arbeiten mit Daten lautet wie folgt:


StorageService
 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); } } 

Vorlage Antwort


Zusätzlich zu dem beschriebenen StorageService- Dienst für die Arbeit mit Daten musste ein dedizierter TemplateService- Dienst implementiert werden, dessen Aufgaben derzeit folgende sind:


  1. Herunterladen und Kompilieren von Lenkervorlagen für Antwortoptionen für verschiedene Befehle in verschiedenen Sprachen.
  2. Auswahl einer geeigneten Vorlage in Abhängigkeit von aktuellem Ereignis, Antwortstatus und Benutzersprache.
  3. Füllen der Vorlage mit Daten, die als Ergebnis des Befehls erhalten wurden.

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

Was Sie beachten sollten:


  1. Vorlagendateien befinden sich in einem separaten Verzeichnis ./templates im Stammverzeichnis des Projekts und sind nach Sprache, Aktion (Befehl) und Antwortstatus strukturiert.


     - templates - en - event_add - invalid_date.hbs - success.hbs - event_info - ru 

    Bei der Initialisierung der Anwendung werden alle Antwortvorlagen in den Speicher geladen und füllen eine Karte mit Schlüsseln, die die Vorlage eindeutig identifizieren:


     private getTemplateKey(lang: string, action: string, status: string): string { return `${lang}-${action}-${status}`; } 

  2. Derzeit verfügt das System über zwei Vorlagensätze: für Russisch und Englisch.
    Fallback wird in der Standardsprache (Englisch) bereitgestellt, sofern die erforderliche Vorlage für die Sprache des aktuellen Ereignisses nicht vorhanden ist.


  3. Lenkervorlagen selbst, zum Beispiel:


     Player <strong>{{name}}</strong> will take part in the game List of players: {{#each players}} {{index}}: <i>{{name}}</i> {{/each}} Total: <strong>{{total}}</strong> 

    Sie enthalten sowohl herkömmliche Platzhalter als auch eine Reihe gültiger Tags zum Formatieren von Antworten in Telegram.



Befehlsverarbeitungsdienste (Aktionen)


Nach der Beschreibung der grundlegenden Support-Services ist es an der Zeit, die Geschäftslogik des Bots selbst zu implementieren. Der Zusammenhang zwischen den Modulen ist hier schematisch dargestellt:


Handlungen


Die Aufgaben der BaseAction- Basisklasse umfassen:


  1. Abonnieren Sie eine bestimmte Veranstaltung von AppEmitter .
  2. Die Gesamtfunktionalität der Ereignisverarbeitung, nämlich:
    • suche nach einem bestehenden oder erstelle einen neuen chat, in dessen kontext das team bearbeitet wird.
    • Ein Aufruf der doAction Vorlagenmethode, doAction Implementierung für jede Klasse der vererbenden BaseAction individuell ist .
    • Anwenden von Vorlagen auf die resultierende doAction Antwort mithilfe des TemplateService .

Basisaktion
 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); } } } 

Die Aufgabe der untergeordneten BaseAction- Klassen besteht darin, die doAction Methode der Eingabe auszuführen:


  1. Chat , der in der Basisklasse erstellt oder definiert wurde
  2. Ein Objekt, das dem IMessage- Protokoll entspricht.

Als Ergebnis der Ausführung dieser Methode wird auch IMessage zurückgegeben, jedoch mit dem festgelegten Status für die Auswahl der richtigen Vorlage und der Daten, die an der Antwortvorlage teilnehmen.


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)), }); } } 

Was Sie beachten sollten:


  1. Mit /remove Befehlen /add und /remove können Sie den Player hinzufügen / entfernen, dessen Name nach dem Befehl steht. Wenn der Name nicht angegeben ist, wird der Spieler, der das Team angerufen hat, hinzugefügt / entfernt.
  2. In Reaktion auf die Befehle /add , /remove und /info wird eine aktualisierte Liste der Spieler für das aktive Ereignis angezeigt.

Die zur Implementierung von (1) und (2) erforderliche Funktionalität wurde in eine spezielle Hilfsklasse verschoben:


SpielerHelfer
 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, })), }; } } 

Alles zusammen


Wie am Anfang des Artikels erwähnt, wird das NestJS- Framework als Anwendungsframework verwendet. Einmal hatte ich das Glück, persönlich an dem Bericht des Erstellers dieser wundervollen Bibliothek teilzunehmen.


Viele NodeJS-Boilerplates, -Handbücher und -Bibliotheken sündigen häufig dadurch, dass sie keine vernünftigen Initialisierungsstrategien und Verbindungen zwischen Modulen bieten. Mit dem Anwachsen der Anwendung wird die Codebasis bei fehlender Beachtung zu einem Mischmasch von Anforderungen zwischen Modulen, was häufig sogar zu zyklischen Abhängigkeiten oder Beziehungen führt, wo dies im Prinzip nicht der Fall sein sollte. Dependency Injection awilix , NestJS .


DI , NodeJS , , .




.


  1. TELEGRAM_BOT_TOKEN — Telegram .
  2. TELEGRAM_USE_PROXY — TelegramAPI.
  3. TELEGRAM_PROXY_HOST — TelegramAPI.
  4. TELEGRAM_PROXY_PORT — TelegramAPI.
  5. TELEGRAM_PROXY_LOGIN — TelegramAPI.
  6. TELEGRAM_PROXY_PASSWORD — TelegramAPI.
  7. VK_TOKEN — VK .
  8. DATABASE_URL — . production .

:


  1. TELEGRAM_BOT_TOKEN Telegram.
  2. VK_TOKEN — , VK . Das heißt , . .
  3. 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 :
.


demo1


.


demo2


.


demo3


VK <b> <i> .
.


demo4


.


demo6


Viber , . , Viber webhooks long-polling. , Viber . . (?) .


Telegram @Tormozz48bot .


Github . , .


PS .

Source: https://habr.com/ru/post/de483194/


All Articles