├── templates
├── ru
│ ├── event_info
│ │ ├── no_event.hbs
│ │ └── success.hbs
│ ├── event_remove
│ │ ├── no_event.hbs
│ │ └── success.hbs
│ ├── player_add
│ │ ├── no_event.hbs
│ │ ├── already_added.hbs
│ │ └── success.hbs
│ ├── player_remove
│ │ ├── no_event.hbs
│ │ ├── no_player.hbs
│ │ └── success.hbs
│ └── event_add
│ │ ├── invalid_date_past.hbs
│ │ ├── invalid_date.hbs
│ │ └── success.hbs
└── en
│ ├── event_info
│ ├── no_event.hbs
│ └── success.hbs
│ ├── event_remove
│ ├── no_event.hbs
│ └── success.hbs
│ ├── player_add
│ ├── no_event.hbs
│ ├── already_added.hbs
│ └── success.hbs
│ ├── player_remove
│ ├── no_event.hbs
│ ├── no_player.hbs
│ └── success.hbs
│ └── event_add
│ ├── invalid_date_past.hbs
│ ├── invalid_date.hbs
│ └── success.hbs
├── src
├── common
│ ├── constants.ts
│ ├── event-bus.service.ts
│ ├── utils.ts
│ ├── common.module.ts
│ ├── config.service.ts
│ └── template.service.ts
├── ping.controller.ts
├── message
│ ├── i-message.ts
│ └── base.message.ts
├── actions
│ ├── statuses.ts
│ ├── actions.module.ts
│ ├── player.helper.ts
│ ├── event_remove.action.ts
│ ├── event_add.action.ts
│ ├── event_info.action.ts
│ ├── player_add.action.ts
│ ├── player_remove.action.ts
│ └── base.action.ts
├── storage
│ ├── storage.module.ts
│ ├── models
│ │ ├── player.ts
│ │ ├── chat.ts
│ │ └── event.ts
│ ├── db-connection.ts
│ └── storage.service.ts
├── app.module.ts
├── telegram
│ ├── telegram.module.ts
│ ├── telegram.message.ts
│ └── telegram.service.ts
├── main.ts
└── vk
│ ├── vk.module.ts
│ ├── vk-callback.controller.ts
│ ├── vk.message.ts
│ └── vk.service.ts
├── .prettierrc
├── tsconfig.build.json
├── .gitignore
├── .dockerignore
├── Dockerfile
├── .travis.yml
├── test
├── stubs
│ ├── template.service.stub.ts
│ ├── context.stub.ts
│ └── actions.module.stub.ts
├── helpers
│ └── db-helper.ts
├── actions
│ ├── event_remove.action.spec.ts
│ ├── event_info.action.spec.ts
│ ├── event_add.action.spec.ts
│ ├── player_remove.action.spec.ts
│ └── player_add.action.spec.ts
└── integration
│ └── scenarios.spec.ts
├── tsconfig.json
├── .nycrc
├── tslint.json
├── git
├── commitlint.js
└── commitizen.js
├── SECURITY.md
├── .github
└── workflows
│ ├── dockerimage.yml
│ └── snyk_container-analysis.yml
├── LICENSE
├── README.md
└── package.json
/templates/ru/event_info/no_event.hbs:
--------------------------------------------------------------------------------
1 | 🚫 Нет назначенных игр 🚫
--------------------------------------------------------------------------------
/templates/ru/event_remove/no_event.hbs:
--------------------------------------------------------------------------------
1 | 🚫 Нет назначенных игр 🚫
--------------------------------------------------------------------------------
/templates/ru/player_add/no_event.hbs:
--------------------------------------------------------------------------------
1 | 🚫 Нет назначенных игр 🚫
--------------------------------------------------------------------------------
/templates/ru/player_remove/no_event.hbs:
--------------------------------------------------------------------------------
1 | 🚫 Нет назначенных игр 🚫
--------------------------------------------------------------------------------
/templates/en/event_info/no_event.hbs:
--------------------------------------------------------------------------------
1 | 🚫 No active events were found 🚫
--------------------------------------------------------------------------------
/templates/en/event_remove/no_event.hbs:
--------------------------------------------------------------------------------
1 | 🚫 No active events were found 🚫
--------------------------------------------------------------------------------
/templates/en/player_add/no_event.hbs:
--------------------------------------------------------------------------------
1 | 🚫 No active events were found 🚫
--------------------------------------------------------------------------------
/templates/en/player_remove/no_event.hbs:
--------------------------------------------------------------------------------
1 | 🚫 No active events were found 🚫
--------------------------------------------------------------------------------
/templates/ru/player_remove/no_player.hbs:
--------------------------------------------------------------------------------
1 | 🚷 Игрок {{name}} не найден 🚷
--------------------------------------------------------------------------------
/templates/en/player_remove/no_player.hbs:
--------------------------------------------------------------------------------
1 | 🚷 Player {{name}} was not found 🚷
--------------------------------------------------------------------------------
/templates/ru/event_remove/success.hbs:
--------------------------------------------------------------------------------
1 | ⚽️🚫 Игра {{date}} была отменена 🚫⚽️
--------------------------------------------------------------------------------
/templates/ru/player_add/already_added.hbs:
--------------------------------------------------------------------------------
1 | 🚷 Игрок {{name}} уже есть в списке 🚷
--------------------------------------------------------------------------------
/templates/en/event_remove/success.hbs:
--------------------------------------------------------------------------------
1 | ⚽️🚫 Event on {{date}} has been canceled 🚫⚽️
--------------------------------------------------------------------------------
/templates/en/player_add/already_added.hbs:
--------------------------------------------------------------------------------
1 | 🚷 Player {{name}} has been already invited 🚷
--------------------------------------------------------------------------------
/src/common/constants.ts:
--------------------------------------------------------------------------------
1 | export enum VK_MODES {
2 | LONG_POLLING = 'polling',
3 | CALLBACK = 'callback'
4 | }
--------------------------------------------------------------------------------
/templates/en/event_add/invalid_date_past.hbs:
--------------------------------------------------------------------------------
1 | ❗️Invalid date❗️
2 |
3 | 🕰 Event date must be in future
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "tabWidth": 4,
5 | "bracketSpacing": false,
6 | }
--------------------------------------------------------------------------------
/templates/ru/event_add/invalid_date_past.hbs:
--------------------------------------------------------------------------------
1 | ❗️Неправильная дата❗️
2 |
3 | 🕰 Дата события должна быть в будущем
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/templates/en/event_add/invalid_date.hbs:
--------------------------------------------------------------------------------
1 | ❗️Invalid date format❗️
2 |
3 | 🕰 Allowed date example: 2019-12-06 09:30
--------------------------------------------------------------------------------
/templates/ru/event_add/invalid_date.hbs:
--------------------------------------------------------------------------------
1 | ❗️Неправильный формат даты❗️
2 |
3 | 🕰 Допустимый формат: 2019-12-06 09:30
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .history
2 | .idea
3 | *.env
4 | !.env.dist
5 | *.sqlite
6 | .nyc_output
7 | node_modules
8 | dist
9 | coverage
10 |
11 | article.md
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .github
2 | .history
3 | .vscode
4 | *.sqlite
5 | git
6 | node_modules
7 | npm-debug.log
8 | coverage
9 | src
10 | test
11 | LICENSE
12 | README.md
13 | ts*.json
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:10
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY package*.json ./
6 |
7 | RUN npm ci --only=production
8 |
9 | COPY . .
10 |
11 | EXPOSE 3000
12 |
13 | CMD [ "node", "dist/main.js" ]
--------------------------------------------------------------------------------
/templates/en/event_info/success.hbs:
--------------------------------------------------------------------------------
1 | ⚽️🗓 Event Info Date: {{date}}
2 |
3 | ℹ️ List of players:
4 | {{#each players}}
5 | {{index}}: {{name}}
6 | {{/each}}
7 |
8 | Total: {{total}}
--------------------------------------------------------------------------------
/templates/ru/event_info/success.hbs:
--------------------------------------------------------------------------------
1 | 🗓 Информация о игре Дата: {{date}}
2 |
3 | ℹ️ Список игроков:
4 | {{#each players}}
5 | {{index}}: {{name}}
6 | {{/each}}
7 |
8 | Итого: {{total}}
--------------------------------------------------------------------------------
/src/ping.controller.ts:
--------------------------------------------------------------------------------
1 | import {Controller, Get} from '@nestjs/common';
2 |
3 | @Controller()
4 | export class PingController {
5 | @Get('/ping')
6 | getHello(): string {
7 | return 'ok';
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/templates/ru/player_add/success.hbs:
--------------------------------------------------------------------------------
1 | ✅ Игрок {{name}} примет участие в игре ✅
2 |
3 | ℹ️ Список игроков:
4 | {{#each players}}
5 | {{index}}: {{name}}
6 | {{/each}}
7 |
8 | Итого: {{total}}
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '10'
4 | - '12'
5 | - '14'
6 | script:
7 | - npm run lint
8 | - npm run build
9 | - npm run test:cov
10 | after_script:
11 | - npm install -g codecov
12 | - codecov
--------------------------------------------------------------------------------
/templates/en/player_add/success.hbs:
--------------------------------------------------------------------------------
1 | ✅ Player {{name}} will take part in the game ✅
2 |
3 | ℹ️ List of players:
4 | {{#each players}}
5 | {{index}}: {{name}}
6 | {{/each}}
7 |
8 | Total: {{total}}
--------------------------------------------------------------------------------
/templates/en/player_remove/success.hbs:
--------------------------------------------------------------------------------
1 | ❌ Player {{name}} canceled his participation ❌
2 |
3 | ℹ️ List of players:
4 | {{#each players}}
5 | {{index}}: {{name}}
6 | {{/each}}
7 |
8 | Total: {{total}}
--------------------------------------------------------------------------------
/templates/ru/player_remove/success.hbs:
--------------------------------------------------------------------------------
1 | ❌ Игрок {{name}} отменил свое участие в игре ❌
2 |
3 | ℹ️ Список игроков:
4 | {{#each players}}
5 | {{index}}: {{name}}
6 | {{/each}}
7 |
8 | Итого: {{total}}
--------------------------------------------------------------------------------
/test/stubs/template.service.stub.ts:
--------------------------------------------------------------------------------
1 | import {IParams} from '../../src/common/template.service';
2 |
3 | export class TemplateServiceStub {
4 | public apply(params: IParams, data: any): string {
5 | return JSON.stringify({params, data});
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/templates/ru/event_add/success.hbs:
--------------------------------------------------------------------------------
1 | ⚽️Новая игра⚽️
2 |
3 | 🗓 Дата: {{date}}
4 |
5 | 1️⃣ Принять участие: /add
6 | 2️⃣ Пригласить другого: /add username
7 | 3️⃣ Отменить свою заявку на участие: /remove
8 | 4️⃣ Посмотреть список участников: /info
9 |
--------------------------------------------------------------------------------
/templates/en/event_add/success.hbs:
--------------------------------------------------------------------------------
1 | ⚽️New Event is coming⚽️
2 |
3 | 🗓 Date: {{date}}
4 |
5 | 1️⃣ Apply to upcoming event: /add
6 | 2️⃣ Invite player: /add username
7 | 3️⃣ Cancel your invitation request: /remove
8 | 4️⃣ View information about upcoming event: /info
--------------------------------------------------------------------------------
/src/message/i-message.ts:
--------------------------------------------------------------------------------
1 | export interface IMessage {
2 | chatId: number;
3 | lang: string;
4 | text: string;
5 | fullText: string;
6 | command: string;
7 | name: string;
8 | getReplyStatus: () => string;
9 | getReplyData: () => any;
10 | setStatus: (status: string) => IMessage;
11 | withData: (data: any) => IMessage;
12 | answer: (args: any) => any;
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "target": "es2017",
9 | "sourceMap": true,
10 | "outDir": "./dist",
11 | "baseUrl": "./",
12 | "incremental": true
13 | },
14 | "exclude": ["node_modules", "dist"]
15 | }
16 |
--------------------------------------------------------------------------------
/src/actions/statuses.ts:
--------------------------------------------------------------------------------
1 | export const STATUS_SUCCESS: string = 'success';
2 | export const STATUS_FAIL: string = 'fail';
3 | export const STATUS_NO_EVENT: string = 'no_event';
4 | export const STATUS_ALREADY_ADDED: string = 'already_added';
5 | export const STATUS_NO_PLAYER: string = 'no_player';
6 | export const STATUS_INVALID_DATE: string = 'invalid_date';
7 | export const STATUS_INVALID_DATE_PAST: string = 'invalid_date_past';
8 |
--------------------------------------------------------------------------------
/src/storage/storage.module.ts:
--------------------------------------------------------------------------------
1 | import {Module} from '@nestjs/common';
2 |
3 | import {CommonModule} from '../common/common.module';
4 | import {getConnection} from './db-connection';
5 | import {StorageService} from './storage.service';
6 |
7 | @Module({
8 | imports: [CommonModule, getConnection()],
9 | providers: [StorageService],
10 | exports: [StorageService],
11 | })
12 | export class StorageModule {}
13 |
--------------------------------------------------------------------------------
/src/common/event-bus.service.ts:
--------------------------------------------------------------------------------
1 | import {EventEmitter} from 'events';
2 |
3 | export class AppEmitter extends EventEmitter {
4 | public readonly EVENT_ADD: string = 'event_add';
5 | public readonly EVENT_INFO: string = 'event_info';
6 | public readonly EVENT_REMOVE: string = 'event_remove';
7 | public readonly PLAYER_ADD: string = 'player_add';
8 | public readonly PLAYER_REMOVE: string = 'player_remove';
9 | }
10 |
--------------------------------------------------------------------------------
/src/storage/models/player.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | Column,
5 | ManyToOne,
6 | JoinColumn,
7 | } from 'typeorm';
8 | import {Event} from './event';
9 |
10 | @Entity()
11 | export class Player {
12 | @PrimaryGeneratedColumn()
13 | id: number;
14 |
15 | @Column()
16 | name: string;
17 |
18 | @ManyToOne(type => Event, event => event.players)
19 | @JoinColumn()
20 | event: Event;
21 | }
22 |
--------------------------------------------------------------------------------
/.nycrc:
--------------------------------------------------------------------------------
1 | {
2 | "cache": false,
3 | "check-coverage": false,
4 | "extension": [
5 | ".ts"
6 | ],
7 | "include": [
8 | "src/**/*.ts"
9 | ],
10 | "exclude": [
11 | "coverage/**",
12 | "node_modules/**",
13 | "**/*.d.ts",
14 | "**/*.spec.ts"
15 | ],
16 | "report-dir": "./coverage",
17 | "sourceMap": true,
18 | "reporter": [
19 | "html",
20 | "text",
21 | "text-summary",
22 | "lcov"
23 | ],
24 | "all": true,
25 | "instrument": true
26 | }
--------------------------------------------------------------------------------
/src/storage/models/chat.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | Column,
5 | OneToMany,
6 | JoinColumn,
7 | Index,
8 | } from 'typeorm';
9 | import {Event} from './event';
10 |
11 | @Entity()
12 | export class Chat {
13 | @PrimaryGeneratedColumn()
14 | id: number;
15 |
16 | @Index({unique: true})
17 | @Column()
18 | chatId: number;
19 |
20 | @OneToMany(type => Event, event => event.chat)
21 | @JoinColumn()
22 | events: Event[];
23 | }
24 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TelegramModule } from './telegram/telegram.module';
3 | import { VKModule } from './vk/vk.module';
4 | import { ActionsModule } from './actions/actions.module';
5 | import { PingController } from './ping.controller';
6 |
7 | @Module({
8 | imports: [
9 | TelegramModule,
10 | VKModule,
11 | ActionsModule
12 | ],
13 | controllers: [PingController],
14 | providers: [],
15 | })
16 | export class AppModule { }
17 |
--------------------------------------------------------------------------------
/src/common/utils.ts:
--------------------------------------------------------------------------------
1 | import * as moment from 'moment';
2 |
3 | export const parseEventDate = (str: string = '') => {
4 | str = str.trim();
5 |
6 | return moment(str).isValid() ? moment.utc(str).toDate() : null;
7 | };
8 |
9 | export const isDateInPast = (date: Date) => {
10 | const currentDate = moment.utc();
11 | return moment.utc(date).isBefore(currentDate);
12 | };
13 |
14 | export const formatEventDate = (date: Date | string): string => {
15 | return moment.utc(date).format('DD-MM-YYYY HH:mm');
16 | };
17 |
--------------------------------------------------------------------------------
/src/telegram/telegram.module.ts:
--------------------------------------------------------------------------------
1 | import {Module, OnModuleInit} from '@nestjs/common';
2 | import {CommonModule} from '../common/common.module';
3 | import {TelegramService} from './telegram.service';
4 |
5 | @Module({
6 | imports: [CommonModule],
7 | providers: [TelegramService],
8 | exports: [TelegramService],
9 | })
10 | export class TelegramModule implements OnModuleInit {
11 | constructor(private readonly telegramService: TelegramService) {}
12 |
13 | async onModuleInit() {
14 | await this.telegramService.launch();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/test/helpers/db-helper.ts:
--------------------------------------------------------------------------------
1 | import {StorageService} from '../../src/storage/storage.service';
2 | import {Chat} from '../../src/storage/models/chat';
3 | import {Event} from '../../src/storage/models/event';
4 | import {Player} from '../../src/storage/models/player';
5 |
6 | export const clearDatabase = async (
7 | storageService: StorageService,
8 | ): Promise => {
9 | await storageService.connection.getRepository(Player).clear();
10 | await storageService.connection.getRepository(Event).clear();
11 | await storageService.connection.getRepository(Chat).clear();
12 | };
13 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": ["tslint:recommended"],
4 | "jsRules": {
5 | "no-unused-expression": true
6 | },
7 | "rules": {
8 | "quotemark": [true, "single"],
9 | "member-access": [false],
10 | "ordered-imports": [false],
11 | "max-line-length": [true, 150],
12 | "member-ordering": [false],
13 | "interface-name": [false],
14 | "arrow-parens": false,
15 | "object-literal-sort-keys": false
16 | },
17 | "rulesDirectory": [],
18 | "linterOptions": {
19 | "exclude": [
20 | "git"
21 | ]
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/storage/models/event.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | Column,
5 | OneToMany,
6 | ManyToOne,
7 | JoinColumn,
8 | } from 'typeorm';
9 | import {Chat} from './chat';
10 | import {Player} from './player';
11 |
12 | @Entity()
13 | export class Event {
14 | @PrimaryGeneratedColumn()
15 | id: number;
16 |
17 | @Column()
18 | date: Date;
19 |
20 | @Column()
21 | active: boolean;
22 |
23 | @ManyToOne(type => Chat, chat => chat.events)
24 | chat: Chat;
25 |
26 | @OneToMany(type => Player, player => player.event)
27 | @JoinColumn()
28 | players: Player[];
29 | }
30 |
--------------------------------------------------------------------------------
/git/commitlint.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | 'body-leading-blank': [2, 'always'],
4 | 'footer-leading-blank': [2, 'always'],
5 | 'header-max-length': [2, 'always', 72],
6 | 'scope-case': [2, 'always', 'lower-case'],
7 | 'subject-empty': [2, 'never'],
8 | 'subject-full-stop': [2, 'never', '.'],
9 | 'type-case': [2, 'always', 'lower-case'],
10 | 'type-empty': [2, 'never'],
11 | 'type-enum': [
12 | 2,
13 | 'always',
14 | [
15 | 'ci',
16 | 'feat',
17 | 'fix',
18 | 'other',
19 | 'revert'
20 | ]
21 | ]
22 | }
23 | };
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Use this section to tell people about which versions of your project are
6 | currently being supported with security updates.
7 |
8 | | Version | Supported |
9 | | ------- | ------------------ |
10 | | 5.1.x | :white_check_mark: |
11 | | 5.0.x | :x: |
12 | | 4.0.x | :white_check_mark: |
13 | | < 4.0 | :x: |
14 |
15 | ## Reporting a Vulnerability
16 |
17 | Use this section to tell people how to report a vulnerability.
18 |
19 | Tell them where to go, how often they can expect to get an update on a
20 | reported vulnerability, what to expect if the vulnerability is accepted or
21 | declined, etc.
22 |
--------------------------------------------------------------------------------
/src/common/common.module.ts:
--------------------------------------------------------------------------------
1 | import {Module, Logger} from '@nestjs/common';
2 | import {ConfigService} from './config.service';
3 | import {AppEmitter} from './event-bus.service';
4 | import {TemplateService} from './template.service';
5 |
6 | @Module({
7 | providers: [
8 | {
9 | provide: ConfigService,
10 | useValue: new ConfigService(),
11 | },
12 | {
13 | provide: AppEmitter,
14 | useValue: new AppEmitter(),
15 | },
16 | {
17 | provide: Logger,
18 | useValue: new Logger(),
19 | },
20 | TemplateService,
21 | ],
22 | exports: [ConfigService, AppEmitter, Logger, TemplateService],
23 | })
24 | export class CommonModule {}
25 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 | import * as dotenv from 'dotenv';
3 |
4 | import {NestFactory} from '@nestjs/core';
5 | import {AppModule} from './app.module';
6 | import {CommonModule} from './common/common.module';
7 | import {ConfigService} from './common/config.service';
8 |
9 | async function start() {
10 | dotenv.config();
11 |
12 | const app = await NestFactory.create(AppModule);
13 | const config = app.select(CommonModule).get(ConfigService, {strict: true});
14 | await app.listen(config.get('PORT'));
15 |
16 | process.on('unhandledRejection', (reason, promise) => {
17 | // tslint:disable-next-line: no-console
18 | console.error('Unhandled Rejection at:', promise, 'reason:', reason);
19 | });
20 | }
21 |
22 | start();
23 |
--------------------------------------------------------------------------------
/.github/workflows/dockerimage.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 |
9 | build:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | env:
14 | LOGIN: ${{ secrets.DOCKER_LOGIN }}
15 | NAME: ${{ secrets.DOCKER_NAME }}
16 |
17 | steps:
18 | - uses: actions/checkout@v1
19 | - name: Install Node.js
20 | uses: actions/setup-node@v1
21 | - name: Login to docker.io
22 | run: echo ${{ secrets.DOCKER_PWD }} | docker login -u ${{ secrets.DOCKER_LOGIN }} --password-stdin
23 | - name: npm install and build
24 | run: npm ci && npm run build
25 | - name: Build the Docker image
26 | run: docker build -t $LOGIN/$NAME:${GITHUB_REF:10} -f Dockerfile .
27 | - name: Push image to docker.io
28 | run: docker push $LOGIN/$NAME:${GITHUB_REF:10}
29 |
--------------------------------------------------------------------------------
/src/actions/actions.module.ts:
--------------------------------------------------------------------------------
1 | import {Module} from '@nestjs/common';
2 | import {CommonModule} from '../common/common.module';
3 | import {StorageModule} from '../storage/storage.module';
4 |
5 | import {PlayerHelper} from './player.helper';
6 | import {EventAddAction} from './event_add.action';
7 | import {EventInfoAction} from './event_info.action';
8 | import {EventRemoveAction} from './event_remove.action';
9 | import {PlayerAddAction} from './player_add.action';
10 | import {PlayerRemoveAction} from './player_remove.action';
11 |
12 | @Module({
13 | imports: [CommonModule, StorageModule],
14 | providers: [
15 | EventAddAction,
16 | EventInfoAction,
17 | EventRemoveAction,
18 | PlayerAddAction,
19 | PlayerRemoveAction,
20 | PlayerHelper,
21 | ],
22 | exports: [],
23 | })
24 | export class ActionsModule {}
25 |
--------------------------------------------------------------------------------
/src/storage/db-connection.ts:
--------------------------------------------------------------------------------
1 | import {TypeOrmModule} from '@nestjs/typeorm';
2 | import {DynamicModule} from '@nestjs/common';
3 |
4 | const commonOptions = {
5 | entities: [__dirname + '/models/*{.ts,.js}'],
6 | synchronize: true,
7 | logging: false,
8 | };
9 |
10 | const connections = {
11 | development: {
12 | type: 'sqlite',
13 | database: 'development.sqlite',
14 | ...commonOptions,
15 | },
16 | test: {
17 | type: 'sqlite',
18 | database: 'test.sqlite',
19 | ...commonOptions,
20 | },
21 | production: {
22 | type: 'postgres',
23 | url: process.env.DATABASE_URL,
24 | ...commonOptions,
25 | },
26 | };
27 |
28 | export const getConnection = (): DynamicModule => {
29 | const env: string = process.env.NODE_ENV || 'development';
30 | const connectionOptions = connections[env];
31 |
32 | return TypeOrmModule.forRoot(connectionOptions);
33 | };
34 |
--------------------------------------------------------------------------------
/src/vk/vk.module.ts:
--------------------------------------------------------------------------------
1 | import {Logger, Module, OnModuleInit} from '@nestjs/common';
2 | import { ConfigService } from 'src/common/config.service';
3 | import { VK_MODES } from 'src/common/constants';
4 | import {CommonModule} from '../common/common.module';
5 | import { VKCallbackController } from './vk-callback.controller';
6 | import {VKService} from './vk.service';
7 |
8 | @Module({
9 | imports: [CommonModule],
10 | controllers: [VKCallbackController],
11 | providers: [VKService],
12 | exports: [VKService],
13 | })
14 | export class VKModule implements OnModuleInit {
15 | constructor(
16 | private readonly logger: Logger,
17 | private readonly config: ConfigService,
18 | private readonly vkService: VKService
19 | ) {}
20 |
21 | onModuleInit() {
22 | if (this.config.get('VK_MODE') === VK_MODES.LONG_POLLING) {
23 | this.vkService.launch();
24 | } else {
25 | this.logger.warn('Long-Polling API disabled. Only callbacks allowed')
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/vk/vk-callback.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Logger, Next, Post, Req, Res } from '@nestjs/common';
2 | import { NextFunction, Request, Response } from 'express';
3 | import { ConfigService } from 'src/common/config.service';
4 | import { VK_MODES } from 'src/common/constants';
5 | import { VKService } from './vk.service';
6 |
7 | @Controller()
8 | export class VKCallbackController {
9 | constructor(
10 | private readonly logger: Logger,
11 | private readonly config: ConfigService,
12 | private readonly vkService: VKService
13 | ) {}
14 |
15 | @Post('/vk/callback')
16 | public async handleCallback(@Req() request: Request, @Res() response: Response, @Next() next: NextFunction) {
17 | if (this.config.get('VK_MODE') === VK_MODES.CALLBACK) {
18 | this.vkService.getBot().webhookCallback(request, response, next);
19 | } else {
20 | this.logger.warn('Callback API disabled. Only long-polling allowed')
21 | return 'Callback API disabled';
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/actions/player.helper.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@nestjs/common';
2 | import {StorageService} from '../storage/storage.service';
3 | import {Event} from '../storage/models/event';
4 | import {Player} from '../storage/models/player';
5 | import {IMessage} from '../message/i-message';
6 |
7 | @Injectable()
8 | export class PlayerHelper {
9 | protected storageService: StorageService;
10 |
11 | constructor(storageService: StorageService) {
12 | this.storageService = storageService;
13 | }
14 |
15 | public getPlayerName(message: IMessage) {
16 | const name = message.text.trim();
17 | return name.length > 0 ? name : message.name;
18 | }
19 |
20 | public async getPlayersList(event: Event) {
21 | const players: Player[] = await this.storageService.getPlayers(event);
22 |
23 | return {
24 | total: players.length,
25 | players: players.map((player, index) => ({
26 | index: index + 1,
27 | name: player.name,
28 | })),
29 | };
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/git/commitizen.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = {
4 | types: [
5 | { value: "ci", name: "ci: CI settings" },
6 | { value: "feat", name: "feat: Add new feature" },
7 | { value: "fix", name: "fix: Issue fix" },
8 | { value: "other", name: "other: Other changes"},
9 | { value: "revert", name: "revert: Revert to previous code version" }
10 | ],
11 |
12 | scopes: [
13 | { name: "source" },
14 | { name: "tests" }
15 | ],
16 |
17 | scopeOverrides: {
18 | content: [
19 | {name: 'source'},
20 | ],
21 | tests: [
22 | {name: 'tests'}
23 | ]
24 | },
25 |
26 | messages: {
27 | type: "What changes do you make ?",
28 | scope: "Select scope which your changes are related to:",
29 | customScope: "Select you scope (optional):",
30 | subject: "Write commit subject:",
31 | body: 'Write detailed commit description:',
32 | confirmCommit: "Do you satisfied by given result ?"
33 | },
34 |
35 | allowCustomScopes: true,
36 | allowBreakingChanges: false,
37 | footerPrefix: "Meta:",
38 | subjectLimit: 120
39 | };
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Andrey Kuznetsov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/vk/vk.message.ts:
--------------------------------------------------------------------------------
1 | import {BaseMessage} from '../message/base.message';
2 | import {IMessage} from '../message/i-message';
3 |
4 | export class VKMessage extends BaseMessage implements IMessage {
5 | private ctx: any;
6 |
7 | constructor(ctx) {
8 | super();
9 |
10 | this.ctx = ctx;
11 |
12 | const {message} = this.ctx;
13 | this.chatId = this.getChatId(this.ctx);
14 | this.fullText = message.text;
15 | this.command = this.ctx.command;
16 | this.text = this.fullText.replace(`/${this.command}`, '');
17 | this.lang = 'ru';
18 | this.firstName = message.from.first_name;
19 | this.lastName = message.from.last_name;
20 | }
21 |
22 | public answer(args: any) {
23 | const answer: string = `${args}`.replace(/<\/?(strong|i)>/gm, '');
24 | this.ctx.reply(answer);
25 | }
26 |
27 | private getChatId({message, group_id, bot}): number {
28 | const peerId: number = +`${message.peer_id}`.replace(/[0-9]0+/, '');
29 | const groupId: number = group_id || bot.settings.group_id;
30 | return peerId + groupId;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/common/config.service.ts:
--------------------------------------------------------------------------------
1 | import * as envalid from 'envalid';
2 | import { VK_MODES } from './constants';
3 |
4 | export class ConfigService {
5 | private readonly envConfig: Record;
6 |
7 | constructor() {
8 | this.envConfig = envalid.cleanEnv(process.env, {
9 | PORT: envalid.port({default: 3000}),
10 | TELEGRAM_BOT_TOKEN: envalid.str(),
11 | TELEGRAM_USE_PROXY: envalid.bool({default: false}),
12 | TELEGRAM_PROXY_HOST: envalid.host(),
13 | TELEGRAM_PROXY_PORT: envalid.port(),
14 | TELEGRAM_PROXY_LOGIN: envalid.str(),
15 | TELEGRAM_PROXY_PASSWORD: envalid.str(),
16 | VK_TOKEN: envalid.str(),
17 | VK_CONFIRMATION: envalid.str(),
18 | VK_MODE: envalid.str({choices: [VK_MODES.LONG_POLLING, VK_MODES.CALLBACK]})
19 | });
20 | }
21 |
22 | /**
23 | *
24 | * Returns value from config by it key
25 | * @param {string} key
26 | * @returns {string}
27 | *
28 | * @memberOf ConfigService
29 | */
30 | public get(key: string): string {
31 | return this.envConfig[key];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/test/stubs/context.stub.ts:
--------------------------------------------------------------------------------
1 | import {TelegramMessage} from '../../src/telegram/telegram.message';
2 | import moment = require('moment');
3 |
4 | export const createContextStub = (params: any, callback) => {
5 | const {
6 | lang = 'en',
7 | chatId = 1,
8 | firstName,
9 | lastName,
10 | text = '',
11 | command = '',
12 | } = params;
13 |
14 | return new TelegramMessage({
15 | command,
16 | update: {
17 | message: {
18 | chat: {
19 | id: chatId,
20 | },
21 | from: {
22 | language_code: lang,
23 | first_name: firstName,
24 | last_name: lastName,
25 | },
26 | text,
27 | },
28 | },
29 | replyWithHTML: (...args) => callback(...args),
30 | });
31 | };
32 |
33 | export const createEventAddContextStub = (params: any, callback) => {
34 | const defaultDate = moment.utc().add(2, 'days').format('YYYY-MM-DD HH:mm');
35 |
36 | params.command = 'event_add';
37 | params.text = params.text || `/event_add ${defaultDate}`;
38 |
39 | return createContextStub(params, callback);
40 | };
41 |
--------------------------------------------------------------------------------
/src/actions/event_remove.action.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@nestjs/common';
2 |
3 | import * as statuses from './statuses';
4 | import {formatEventDate} from '../common/utils';
5 | import {BaseAction} from './base.action';
6 | import {Chat} from '../storage/models/chat';
7 | import {Event} from '../storage/models/event';
8 | import {IMessage} from '../message/i-message';
9 |
10 | @Injectable()
11 | export class EventRemoveAction extends BaseAction {
12 | protected setEvent(): void {
13 | this.event = this.appEmitter.EVENT_REMOVE;
14 | }
15 |
16 | protected async doAction(chat: Chat, message: IMessage): Promise {
17 | const activeEvent: Event = await this.storageService.findChatActiveEvent(
18 | chat,
19 | );
20 | await this.storageService.markChatEventsInactive(chat);
21 |
22 | if (activeEvent) {
23 | this.logger.log(`Active event with id=${activeEvent.id} date=${activeEvent.date} for chat ${chat.id} was removed`);
24 |
25 | return message.setStatus(statuses.STATUS_SUCCESS).withData({
26 | date: formatEventDate(activeEvent.date),
27 | });
28 | } else {
29 | return message.setStatus(statuses.STATUS_NO_EVENT);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/test/stubs/actions.module.stub.ts:
--------------------------------------------------------------------------------
1 | import {Test, TestingModule} from '@nestjs/testing';
2 |
3 | import {CommonModule} from '../../src/common/common.module';
4 | import {StorageModule} from '../../src/storage/storage.module';
5 | import {TemplateService} from '../../src/common/template.service';
6 | import {TemplateServiceStub} from './template.service.stub';
7 |
8 | import {EventAddAction} from '../../src/actions/event_add.action';
9 | import {EventRemoveAction} from '../../src/actions/event_remove.action';
10 | import {EventInfoAction} from '../../src/actions/event_info.action';
11 | import {PlayerAddAction} from '../../src/actions/player_add.action';
12 | import {PlayerRemoveAction} from '../../src/actions/player_remove.action';
13 | import {PlayerHelper} from '../../src/actions/player.helper';
14 |
15 | let moduleStub: TestingModule;
16 |
17 | export const createModuleStub = async (): Promise => {
18 | moduleStub =
19 | moduleStub ||
20 | (await Test.createTestingModule({
21 | imports: [CommonModule, StorageModule],
22 | providers: [
23 | EventAddAction,
24 | EventRemoveAction,
25 | EventInfoAction,
26 | PlayerAddAction,
27 | PlayerRemoveAction,
28 | PlayerHelper,
29 | ],
30 | })
31 | .overrideProvider(TemplateService)
32 | .useClass(TemplateServiceStub)
33 | .compile());
34 |
35 | return moduleStub;
36 | };
37 |
--------------------------------------------------------------------------------
/src/actions/event_add.action.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@nestjs/common';
2 |
3 | import * as statuses from './statuses';
4 | import {parseEventDate, isDateInPast, formatEventDate} from '../common/utils';
5 | import {BaseAction} from './base.action';
6 | import {Chat} from '../storage/models/chat';
7 | import {Event} from '../storage/models/event';
8 | import {IMessage} from '../message/i-message';
9 |
10 | @Injectable()
11 | export class EventAddAction extends BaseAction {
12 | protected setEvent(): void {
13 | this.event = this.appEmitter.EVENT_ADD;
14 | }
15 |
16 | protected async doAction(chat: Chat, message: IMessage): Promise {
17 | const eventDate: Date = parseEventDate(message.text.trim());
18 |
19 | if (!eventDate) {
20 | return message.setStatus(statuses.STATUS_INVALID_DATE);
21 | }
22 |
23 | if (isDateInPast(eventDate)) {
24 | this.logger.warn(`Date of event ${eventDate} was given as date from past`);
25 | return message.setStatus(statuses.STATUS_INVALID_DATE_PAST);
26 | }
27 |
28 | await this.storageService.markChatEventsInactive(chat);
29 | const event: Event = await this.storageService.appendChatActiveEvent(
30 | chat,
31 | eventDate,
32 | );
33 |
34 | this.logger.log(`New active event with date=${eventDate} for chat ${chat.id} was created`);
35 |
36 | return message.setStatus(statuses.STATUS_SUCCESS).withData({
37 | date: formatEventDate(event.date),
38 | });
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.github/workflows/snyk_container-analysis.yml:
--------------------------------------------------------------------------------
1 | # A sample workflow which checks out the code, builds a container
2 | # image using Docker and scans that image for vulnerabilities using
3 | # Snyk. The results are then uploaded to GitHub Security Code Scanning
4 | #
5 | # For more examples, including how to limit scans to only high-severity
6 | # issues, monitor images for newly disclosed vulnerabilities in Snyk and
7 | # fail PR checks for new vulnerabilities, see https://github.com/snyk/actions/
8 |
9 | name: Snyk Container
10 | on: push
11 | jobs:
12 | snyk:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Build a Docker image
17 | run: docker build -t your/image-to-test .
18 | - name: Run Snyk to check Docker image for vulnerabilities
19 | # Snyk can be used to break the build when it detects vulnerabilities.
20 | # In this case we want to upload the issues to GitHub Code Scanning
21 | continue-on-error: true
22 | uses: snyk/actions/docker@master
23 | env:
24 | # In order to use the Snyk Action you will need to have a Snyk API token.
25 | # More details in https://github.com/snyk/actions#getting-your-snyk-token
26 | # or you can signup for free at https://snyk.io/login
27 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
28 | with:
29 | image: your/image-to-test
30 | args: --file=Dockerfile
31 | - name: Upload result to GitHub Code Scanning
32 | uses: github/codeql-action/upload-sarif@v2
33 | with:
34 | sarif_file: snyk.sarif
35 |
--------------------------------------------------------------------------------
/src/vk/vk.service.ts:
--------------------------------------------------------------------------------
1 | import * as VkBot from 'node-vk-bot-api';
2 |
3 | import {Injectable} from '@nestjs/common';
4 | import {ConfigService} from '../common/config.service';
5 | import {AppEmitter} from '../common/event-bus.service';
6 | import {VKMessage} from './vk.message';
7 |
8 | @Injectable()
9 | export class VKService {
10 | private bot: VkBot;
11 |
12 | constructor(config: ConfigService, appEmitter: AppEmitter) {
13 | const token: string = config.get('VK_TOKEN');
14 | const confirmation: string = config.get('VK_CONFIRMATION');
15 |
16 | this.bot = new VkBot({token, confirmation});
17 |
18 | this.getCommandEventMapping(appEmitter).forEach(([command, event]) => {
19 | this.bot.command(`/${command}`, async ctx => {
20 | const [from] = await this.bot.execute('users.get', {
21 | user_ids: ctx.message.from_id,
22 | });
23 | ctx.command = command;
24 | ctx.message.from = from;
25 | appEmitter.emit(event, new VKMessage(ctx));
26 | });
27 | });
28 | }
29 |
30 | public getBot(): VkBot {
31 | return this.bot;
32 | }
33 |
34 | public launch(): void {
35 | this.bot.startPolling();
36 | }
37 |
38 | /**
39 | * Returns mapping structure that links commands and corresponded events
40 | * @private
41 | * @param {AppEmitter} appEmitter
42 | * @returns {Array<[string, string]>}
43 | * @memberOf VKService
44 | */
45 | private getCommandEventMapping(
46 | appEmitter: AppEmitter,
47 | ): [string, string][] {
48 | return [
49 | ['event_add', appEmitter.EVENT_ADD],
50 | ['event_remove', appEmitter.EVENT_REMOVE],
51 | ['info', appEmitter.EVENT_INFO],
52 | ['add', appEmitter.PLAYER_ADD],
53 | ['remove', appEmitter.PLAYER_REMOVE],
54 | ];
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/message/base.message.ts:
--------------------------------------------------------------------------------
1 | import {IMessage} from '../message/i-message';
2 |
3 | export class BaseMessage implements IMessage {
4 | public chatId: number;
5 | public lang: string;
6 | public text: string;
7 | public fullText: string;
8 | public command: string;
9 |
10 | protected firstName: string;
11 | protected lastName: string;
12 |
13 | protected replyStatus: string;
14 | protected replyData: any;
15 |
16 | /**
17 | * Retunrn name of player
18 | * @readonly
19 | * @type {string}
20 | * @memberOf BaseMessage
21 | */
22 | get name(): string {
23 | const firstName: string = this.firstName || '';
24 | const lastName: string = this.lastName || '';
25 |
26 | return `${firstName} ${lastName}`.trim();
27 | }
28 |
29 | /**
30 | * Returns status of answer (reply)
31 | * @returns {string}
32 | * @memberOf BaseMessage
33 | */
34 | public getReplyStatus(): string {
35 | return this.replyStatus;
36 | }
37 |
38 | /**
39 | * Returns data for answer (reply)
40 | * @returns {string}
41 | * @memberOf BaseMessage
42 | */
43 | public getReplyData(): any {
44 | return this.replyData;
45 | }
46 |
47 | /**
48 | * Sets status of answer
49 | * @param {string} status
50 | * @returns {IMessage}
51 | *
52 | * @memberOf BaseMessage
53 | */
54 | public setStatus(status: string): IMessage {
55 | this.replyStatus = status;
56 | return this;
57 | }
58 |
59 | /**
60 | * Attaches data for answer
61 | * @param {*} data
62 | * @returns {IMessage}
63 | * @memberOf BaseMessage
64 | */
65 | public withData(data: any): IMessage {
66 | this.replyData = data;
67 | return this;
68 | }
69 |
70 | /**
71 | * @param {*} args
72 | * @returns {string}
73 | * @memberOf BaseMessage
74 | */
75 | public answer(args: any): any {
76 | throw new Error('not implemented');
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/telegram/telegram.message.ts:
--------------------------------------------------------------------------------
1 | import { TelegrafContext } from 'telegraf/typings/context';
2 | import { Message } from 'telegraf/typings/telegram-types';
3 | import {BaseMessage} from '../message/base.message';
4 | import {IMessage} from '../message/i-message';
5 |
6 | export class TelegramMessage extends BaseMessage implements IMessage {
7 | private ctx: any;
8 |
9 | constructor(ctx: any) {
10 | super();
11 |
12 | this.ctx = ctx;
13 |
14 | const {message} = this.ctx.update;
15 | this.chatId = this.adjustChatId(message.chat.id);
16 | this.fullText = message.text;
17 | this.command = this.ctx.command;
18 | this.text = this.extractText(this.fullText);
19 |
20 | this.lang = message.from.language_code;
21 | this.firstName = message.from.first_name;
22 | this.lastName = message.from.last_name;
23 | }
24 |
25 | public answer(args: any): Promise {
26 | return this.ctx.replyWithHTML(args);
27 | }
28 |
29 | /**
30 | * Telegram chat identifier may be greater or less then max 4 byte integer value
31 | * which is used as type for related column in postgresql
32 | * @private
33 | * @param {number} chatId
34 | * @return number
35 | * @memberof TelegramMessage
36 | */
37 | private adjustChatId(chatId: number): number {
38 | if (Math.abs(chatId) < 2147483648) {
39 | return chatId;
40 | }
41 |
42 | // tslint:disable-next-line: no-bitwise
43 | return this.adjustChatId(chatId >>> 1)
44 | }
45 |
46 | /**
47 | * Trim command name and bot name e.g @MyBot which can be appear on some devices
48 | * @param fullText
49 | */
50 | private extractText(fullText: string): string {
51 | let text: string = fullText.replace(`/${this.command}`, '');
52 |
53 | if (this.ctx.botInfo && this.ctx.botInfo.username) {
54 | text = text.replace(new RegExp(`@?${this.ctx.botInfo.username}`), '');
55 | }
56 |
57 | return text;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/actions/event_info.action.ts:
--------------------------------------------------------------------------------
1 | import {Injectable, Logger} from '@nestjs/common';
2 |
3 | import * as statuses from './statuses';
4 | import {formatEventDate} from '../common/utils';
5 | import {ConfigService} from '../common/config.service';
6 | import {AppEmitter} from '../common/event-bus.service';
7 | import {TemplateService} from '../common/template.service';
8 | import {StorageService} from '../storage/storage.service';
9 | import {BaseAction} from './base.action';
10 | import {PlayerHelper} from './player.helper';
11 | import {Chat} from '../storage/models/chat';
12 | import {Event} from '../storage/models/event';
13 | import {Player} from '../storage/models/player';
14 | import {IMessage} from '../message/i-message';
15 |
16 | @Injectable()
17 | export class EventInfoAction extends BaseAction {
18 | private playerHelper: PlayerHelper;
19 |
20 | constructor(
21 | config: ConfigService,
22 | appEmitter: AppEmitter,
23 | logger: Logger,
24 | templateService: TemplateService,
25 | playerHelper: PlayerHelper,
26 | storageService: StorageService,
27 | ) {
28 | super(config, appEmitter, logger, templateService, storageService);
29 |
30 | this.playerHelper = playerHelper;
31 | }
32 |
33 | protected setEvent(): void {
34 | this.event = this.appEmitter.EVENT_INFO;
35 | }
36 |
37 | protected async doAction(chat: Chat, message: IMessage): Promise {
38 | const activeEvent: Event = await this.storageService.findChatActiveEvent(
39 | chat,
40 | );
41 |
42 | if (!activeEvent) {
43 | this.logger.warn(`No active events for chat with id=${chat.id} were found`);
44 | return message.setStatus(statuses.STATUS_NO_EVENT);
45 | }
46 |
47 | const players: Player[] = await this.storageService.getPlayers(
48 | activeEvent,
49 | );
50 |
51 | return message.setStatus(statuses.STATUS_SUCCESS).withData({
52 | date: formatEventDate(activeEvent.date),
53 | ...(await this.playerHelper.getPlayersList(activeEvent)),
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/common/template.service.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as fs from 'fs';
3 | import * as handlebars from 'handlebars';
4 | import {readDirDeepSync} from 'read-dir-deep';
5 | import {Injectable, Logger} from '@nestjs/common';
6 |
7 | export interface IParams {
8 | action: string;
9 | status: string;
10 | lang?: string;
11 | }
12 |
13 | @Injectable()
14 | export class TemplateService {
15 | private readonly DEFAULT_LANG: string = 'en';
16 | private readonly TEMPLATE_PATH: string = 'templates';
17 |
18 | private logger: Logger;
19 | private templatesMap: Map string>;
20 |
21 | constructor(logger: Logger) {
22 | this.logger = logger;
23 |
24 | this.load();
25 | }
26 |
27 | public apply(params: IParams, data: any): string {
28 | this.logger.log(
29 | `apply template: ${params.action} ${params.status} ${params.lang}`,
30 | );
31 |
32 | let template = this.getTemplate(params);
33 |
34 | if (!template) {
35 | params.lang = this.DEFAULT_LANG;
36 | template = this.getTemplate(params);
37 | }
38 |
39 | if (!template) {
40 | throw new Error('template-not-found');
41 | }
42 |
43 | return template(data);
44 | }
45 |
46 | private getTemplate(params: IParams): (data: any) => string {
47 | const {lang, action, status} = params;
48 | return this.templatesMap.get(this.getTemplateKey(lang, action, status));
49 | }
50 |
51 | private load() {
52 | const templatesDir: string = path.join(
53 | process.cwd(),
54 | this.TEMPLATE_PATH,
55 | );
56 | const templateFileNames: string[] = readDirDeepSync(templatesDir);
57 |
58 | this.templatesMap = templateFileNames.reduce((acc, fileName) => {
59 | const template = fs.readFileSync(fileName, {encoding: 'utf-8'});
60 |
61 | const [, lang, action, status] = fileName
62 | .replace(/\.hbs$/, '')
63 | .split('/');
64 | return acc.set(
65 | this.getTemplateKey(lang, action, status),
66 | handlebars.compile(template),
67 | );
68 | }, new Map());
69 | }
70 |
71 | private getTemplateKey(
72 | lang: string,
73 | action: string,
74 | status: string,
75 | ): string {
76 | return `${lang}-${action}-${status}`;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/actions/player_add.action.ts:
--------------------------------------------------------------------------------
1 | import {Injectable, Logger} from '@nestjs/common';
2 |
3 | import * as statuses from './statuses';
4 | import {ConfigService} from '../common/config.service';
5 | import {AppEmitter} from '../common/event-bus.service';
6 | import {TemplateService} from '../common/template.service';
7 | import {StorageService} from '../storage/storage.service';
8 | import {BaseAction} from './base.action';
9 | import {PlayerHelper} from './player.helper';
10 | import {Chat} from '../storage/models/chat';
11 | import {Event} from '../storage/models/event';
12 | import {Player} from '../storage/models/player';
13 | import {IMessage} from '../message/i-message';
14 |
15 | @Injectable()
16 | export class PlayerAddAction extends BaseAction {
17 | private playerHelper: PlayerHelper;
18 |
19 | constructor(
20 | config: ConfigService,
21 | appEmitter: AppEmitter,
22 | logger: Logger,
23 | templateService: TemplateService,
24 | playerHelper: PlayerHelper,
25 | storageService: StorageService,
26 | ) {
27 | super(config, appEmitter, logger, templateService, storageService);
28 |
29 | this.playerHelper = playerHelper;
30 | }
31 |
32 | protected setEvent(): void {
33 | this.event = this.appEmitter.PLAYER_ADD;
34 | }
35 |
36 | protected async doAction(chat: Chat, message: IMessage): Promise {
37 | const activeEvent: Event = await this.storageService.findChatActiveEvent(
38 | chat,
39 | );
40 |
41 | if (!activeEvent) {
42 | this.logger.warn(`No active events for chat with id=${chat.id} were found`);
43 | return message.setStatus(statuses.STATUS_NO_EVENT);
44 | }
45 |
46 | const name: string = this.playerHelper.getPlayerName(message);
47 | const existedPlayer: Player = await this.storageService.findPlayer(
48 | activeEvent,
49 | name,
50 | );
51 |
52 | if (existedPlayer) {
53 | return message
54 | .setStatus(statuses.STATUS_ALREADY_ADDED)
55 | .withData({name});
56 | }
57 |
58 | const newPlayer: Player = await this.storageService.addPlayer(
59 | activeEvent,
60 | name,
61 | );
62 |
63 | this.logger.log(`Player with id=${newPlayer.id} name=${newPlayer.name} has been added to event ${activeEvent.id}`);
64 |
65 | return message.setStatus(statuses.STATUS_SUCCESS).withData({
66 | name: newPlayer.name,
67 | ...(await this.playerHelper.getPlayersList(activeEvent)),
68 | });
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/actions/player_remove.action.ts:
--------------------------------------------------------------------------------
1 | import {Injectable, Logger} from '@nestjs/common';
2 |
3 | import * as statuses from './statuses';
4 | import {ConfigService} from '../common/config.service';
5 | import {AppEmitter} from '../common/event-bus.service';
6 | import {TemplateService} from '../common/template.service';
7 | import {StorageService} from '../storage/storage.service';
8 | import {BaseAction} from './base.action';
9 | import {PlayerHelper} from './player.helper';
10 | import {Chat} from '../storage/models/chat';
11 | import {Event} from '../storage/models/event';
12 | import {Player} from '../storage/models/player';
13 | import {IMessage} from '../message/i-message';
14 |
15 | @Injectable()
16 | export class PlayerRemoveAction extends BaseAction {
17 | private playerHelper: PlayerHelper;
18 |
19 | constructor(
20 | config: ConfigService,
21 | appEmitter: AppEmitter,
22 | logger: Logger,
23 | templateService: TemplateService,
24 | playerHelper: PlayerHelper,
25 | storageService: StorageService,
26 | ) {
27 | super(config, appEmitter, logger, templateService, storageService);
28 |
29 | this.playerHelper = playerHelper;
30 | }
31 |
32 | protected setEvent(): void {
33 | this.event = this.appEmitter.PLAYER_REMOVE;
34 | }
35 |
36 | protected async doAction(chat: Chat, message: IMessage): Promise {
37 | const activeEvent: Event = await this.storageService.findChatActiveEvent(
38 | chat,
39 | );
40 |
41 | if (!activeEvent) {
42 | this.logger.warn(`No active events for chat with id=${chat.id} were found`);
43 | return message.setStatus(statuses.STATUS_NO_EVENT);
44 | }
45 |
46 | const name: string = this.playerHelper.getPlayerName(message);
47 | const existedPlayer: Player = await this.storageService.findPlayer(
48 | activeEvent,
49 | name,
50 | );
51 |
52 | if (!existedPlayer) {
53 | this.logger.warn(`No existed players for event with id=${activeEvent.id} and name=${name} were found`);
54 | return message
55 | .setStatus(statuses.STATUS_NO_PLAYER)
56 | .withData({name});
57 | }
58 |
59 | await this.storageService.removePlayer(existedPlayer);
60 |
61 | this.logger.log(`Player with id=${existedPlayer.id} name=${existedPlayer.name} has been removed from event ${activeEvent.id}`);
62 |
63 | return message.setStatus(statuses.STATUS_SUCCESS).withData({
64 | name,
65 | ...(await this.playerHelper.getPlayersList(activeEvent)),
66 | });
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # football-chat-bot-2
2 | Football chat bot build with NestJS framework
3 |
4 | [](https://travis-ci.org/tormozz48/football-chat-bot-2)
5 | [](https://codecov.io/gh/tormozz48/football-chat-bot-2)
6 |
7 | ## Install
8 |
9 | 1. Clone repository to your local filesystem from github:
10 | ```bash
11 | git clone https://github.com/tormozz48/football-chat-bot-2.git
12 | ```
13 | 2. Install npm dependencies:
14 | ```bash
15 | npm install
16 | ```
17 | 3. Set up all environment variables.
18 | Also you can create `.env` file and fill them with environment variables described in "Configuration" section.
19 |
20 | 1. Launch application server:
21 | ```bash
22 | npm start
23 | ```
24 |
25 | ## Configuration
26 |
27 | Configuration is performed by modifying these environment variables:
28 |
29 | * `TELEGRAM_BOT_TOKEN` - unique telegram bot token string
30 | * `TELEGRAM_USE_PROXY` - true/false. If set to true then application will establish connection with telegram server via proxy.
31 | * `TELEGRAM_PROXY_HOST` - proxy server host
32 | * `TELEGRAM_PROXY_PORT` - proxy server port
33 | * `TELEGRAM_PROXY_LOGIN` - proxy auth login
34 | * `TELEGRAM_PROXY_PASSWORD` - proxy auth password
35 | * `VK_TOKEN` - unique vk bot token string
36 | * `DATABASE_URL` - connection url fot database. Used for production environment.
37 |
38 | ## Development
39 |
40 | Configured commands:
41 | * `npm run build` - compile TypeScript source code into js distributive.
42 | * `npm run format` - perform code formatting via prettier tool.
43 | * `npm start` - run application server
44 | * `npm start:dev` - run application in "watch mode". Restart after source code chages.
45 | * `npm start:debug` - run application in both "watch" and "debug" modes.
46 | * `npm start:prod` - run application in production mode.
47 | * `npm run lint` - perform code linting via tslint tool.
48 | * `npm test` - run tests.
49 | * `npm test:watch` - run tests in "watch mode".
50 | * `npm test:cov` - run tests and calculate code coverage.
51 |
52 | ## Third-party software
53 |
54 | * [NestJS](https://docs.nestjs.com/) - is a framework for building efficient, scalable Node.js server-side applications.
55 | * [TypeORM](https://typeorm.io/#/) - TypeORM is an ORM that can run in NodeJS, Browser, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo, and Electron platforms and can be used with TypeScript and JavaScript (ES5, ES6, ES7, ES8).
56 | * [Telegraf](https://telegraf.js.org/#/) - modern Telegram Bot Framework for Node.js.
57 | * [Handlebars](http://handlebarsjs.com/) - Minimal templating on steroids.
58 |
--------------------------------------------------------------------------------
/src/telegram/telegram.service.ts:
--------------------------------------------------------------------------------
1 | import Telegraf from 'telegraf';
2 |
3 | import {Injectable} from '@nestjs/common';
4 | import * as SocksAgent from 'socks5-https-client/lib/Agent';
5 | import {ConfigService} from '../common/config.service';
6 | import {AppEmitter} from '../common/event-bus.service';
7 | import {TelegramMessage} from './telegram.message';
8 | import { User } from 'telegraf/typings/telegram-types';
9 | import { TelegrafContext } from 'telegraf/typings/context';
10 |
11 | @Injectable()
12 | export class TelegramService {
13 | private bot: Telegraf;
14 |
15 | constructor(config: ConfigService, appEmitter: AppEmitter) {
16 | const botToken: string = config.get('TELEGRAM_BOT_TOKEN');
17 |
18 | this.bot = config.get('TELEGRAM_USE_PROXY')
19 | ? new Telegraf(botToken, {
20 | telegram: {agent: this.getProxy(config)},
21 | })
22 | : new Telegraf(botToken);
23 |
24 | this.getCommandEventMapping(appEmitter).forEach(([command, event]) => {
25 | this.bot.command(command, (ctx: TelegrafContext & {command?: string}) => {
26 | ctx.command = command;
27 | appEmitter.emit(event, new TelegramMessage(ctx));
28 | });
29 | });
30 | }
31 |
32 | public async launch(): Promise {
33 | const botInfo: User = await this.bot.telegram.getMe();
34 | if (botInfo) {
35 | this.bot.options.username = botInfo.username
36 | }
37 |
38 | this.bot.launch();
39 | }
40 |
41 | /**
42 | * Returns proxy instance for telegram bot
43 | * @private
44 | * @param {ConfigService} config
45 | * @returns {SocksAgent}
46 | * @memberOf TelegramService
47 | */
48 | private getProxy(config: ConfigService): SocksAgent {
49 | return new SocksAgent({
50 | socksHost: config.get('TELEGRAM_PROXY_HOST'),
51 | socksPort: config.get('TELEGRAM_PROXY_PORT'),
52 | socksUsername: config.get('TELEGRAM_PROXY_LOGIN'),
53 | socksPassword: config.get('TELEGRAM_PROXY_PASSWORD'),
54 | });
55 | }
56 |
57 | /**
58 | * Returns mapping structure that links commands and corresponded events
59 | * @private
60 | * @param {AppEmitter} appEmitter
61 | * @returns {Array<[string, string]>}
62 | * @memberOf TelegramService
63 | */
64 | private getCommandEventMapping(
65 | appEmitter: AppEmitter,
66 | ): [string, string][] {
67 | return [
68 | ['event_add', appEmitter.EVENT_ADD],
69 | ['event_remove', appEmitter.EVENT_REMOVE],
70 | ['info', appEmitter.EVENT_INFO],
71 | ['add', appEmitter.PLAYER_ADD],
72 | ['remove', appEmitter.PLAYER_REMOVE],
73 | ];
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/actions/base.action.ts:
--------------------------------------------------------------------------------
1 | import {Injectable, Logger} from '@nestjs/common';
2 | import {IMessage} from '../message/i-message';
3 | import {ConfigService} from '../common/config.service';
4 | import {AppEmitter} from '../common/event-bus.service';
5 | import {TemplateService} from '../common/template.service';
6 | import {StorageService} from '../storage/storage.service';
7 | import {Chat} from '../storage/models/chat';
8 |
9 | @Injectable()
10 | export class BaseAction {
11 | protected readonly appEmitter: AppEmitter;
12 | protected readonly config: ConfigService;
13 | protected readonly logger: Logger;
14 |
15 | protected readonly templateService: TemplateService;
16 | protected readonly storageService: StorageService;
17 |
18 | protected event: string;
19 |
20 | constructor(
21 | config: ConfigService,
22 | appEmitter: AppEmitter,
23 | logger: Logger,
24 | templateService: TemplateService,
25 | storageService: StorageService,
26 | ) {
27 | this.config = config;
28 | this.logger = logger;
29 |
30 | this.appEmitter = appEmitter;
31 | this.templateService = templateService;
32 | this.storageService = storageService;
33 |
34 | this.setEvent();
35 |
36 | this.logger.log(`subscribe on "${this.event}" event`);
37 | this.appEmitter.on(this.event, this.handleEvent.bind(this));
38 | }
39 |
40 | /**
41 | * Set event for action.
42 | * This method must be overrided in child class
43 | * @protected
44 | * @memberOf BaseAction
45 | */
46 | protected setEvent(): void {
47 | throw new Error('not implemented');
48 | }
49 |
50 | /**
51 | * Implements action logic.
52 | * This method must be overrided in child class
53 | * @protected
54 | * @param {Chat} chat
55 | * @param {IMessage} message
56 | * @returns {Promise}
57 | * @memberOf BaseAction
58 | */
59 | protected async doAction(chat: Chat, message: IMessage): Promise {
60 | throw new Error('not implemented');
61 | }
62 |
63 | private async handleEvent(message: IMessage) {
64 | try {
65 | this.logger.log(`"${this.event}" event received`);
66 |
67 | const chatId: number = message.chatId;
68 | const chat: Chat = await this.storageService.ensureChat(chatId);
69 | message = await this.doAction(chat, message);
70 |
71 | message.answer(
72 | this.templateService.apply(
73 | {
74 | action: this.event,
75 | status: message.getReplyStatus(),
76 | lang: message.lang,
77 | },
78 | message.getReplyData(),
79 | ),
80 | );
81 | } catch (error) {
82 | this.logger.error(error);
83 | message.answer(error.message);
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "football-chat-bot-2",
3 | "version": "0.2.6",
4 | "description": "Footbal chat bot build with NestJS framework",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "prebuild": "rimraf dist",
8 | "build": "nest build",
9 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
10 | "start": "nest start",
11 | "start:dev": "nest start --watch",
12 | "start:debug": "nest start --debug --watch",
13 | "start:prod": "node dist/main",
14 | "lint": "tslint -p tsconfig.json -c tslint.json",
15 | "test": "mocha -r ts-node/register test/**/*.spec.ts",
16 | "test:watch": "npm test -- --watch --extension *.ts",
17 | "test:cov": "nyc npm run test",
18 | "env:generate": "node gen-env.js"
19 | },
20 | "husky": {
21 | "hooks": {
22 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS -g './git/commitlint.js'"
23 | }
24 | },
25 | "config": {
26 | "commitizen": {
27 | "path": "node_modules/cz-customizable"
28 | },
29 | "cz-customizable": {
30 | "config": "./git/commitizen.js"
31 | }
32 | },
33 | "repository": {
34 | "type": "git",
35 | "url": "git+https://github.com/tormozz48/football-chat-bot-2.git"
36 | },
37 | "keywords": [
38 | "football",
39 | "chat",
40 | "bot",
41 | "telegram",
42 | "vk",
43 | "bot",
44 | "vk-bot",
45 | "vk-api",
46 | "nestjs",
47 | "nodejs",
48 | "typescript",
49 | "typeorm",
50 | "sqlite3",
51 | "postgresql"
52 | ],
53 | "author": "andrey.kuznetsov48@yandex.ru",
54 | "license": "MIT",
55 | "bugs": {
56 | "url": "https://github.com/tormozz48/football-chat-bot-2/issues"
57 | },
58 | "homepage": "https://github.com/tormozz48/football-chat-bot-2#readme",
59 | "publishConfig": {
60 | "registry": "https://npm.pkg.github.com/"
61 | },
62 | "dependencies": {
63 | "@nestjs/cli": "^8.2.8",
64 | "@nestjs/common": "^7.6.18",
65 | "@nestjs/core": "^7.6.18",
66 | "@nestjs/platform-express": "^7.6.18",
67 | "@nestjs/typeorm": "^7.1.4",
68 | "@types/dotenv": "^8.2.0",
69 | "@types/express": "^4.17.17",
70 | "@types/handlebars": "^4.1.0",
71 | "@types/moment": "^2.13.0",
72 | "@types/node": "^14.18.37",
73 | "dotenv": "^8.6.0",
74 | "envalid": "^6.0.2",
75 | "events-extra": "^1.0.4",
76 | "handlebars": "^4.7.7",
77 | "handlebars-helpers": "^0.10.0",
78 | "lodash": "^4.17.21",
79 | "moment": "^2.29.2",
80 | "node-vk-bot-api": "github:tormozz48/node-vk-bot-api",
81 | "nyc": "^15.1.0",
82 | "pg": "^8.10.0",
83 | "read-dir-deep": "^7.0.1",
84 | "reflect-metadata": "^0.1.13",
85 | "rimraf": "^3.0.2",
86 | "rxjs": "^6.6.7",
87 | "socks5-https-client": "^1.2.1",
88 | "sql.js": "^1.8.0",
89 | "sqlite3": "^5.1.6",
90 | "telegraf": "^3.38.0",
91 | "ts-node": "^9.0.0",
92 | "tsconfig-paths": "^3.14.2",
93 | "typeorm": "^0.3.12",
94 | "typescript": "^4.9.5"
95 | },
96 | "devDependencies": {
97 | "@commitlint/cli": "^11.0.0",
98 | "@nestjs/testing": "^7.4.4",
99 | "@types/chai": "^4.2.12",
100 | "@types/mocha": "^8.0.3",
101 | "chai": "^4.2.0",
102 | "commitizen": "^4.2.1",
103 | "cz-customizable": "^6.3.0",
104 | "envdot": "0.0.3",
105 | "husky": "^4.3.0",
106 | "mocha": "^8.1.3",
107 | "prettier": "^2.1.2",
108 | "ts-loader": "^8.0.4",
109 | "tslint": "^6.1.3",
110 | "uuid": "^8.3.0"
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/test/actions/event_remove.action.spec.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import {expect} from 'chai';
3 |
4 | import {
5 | createContextStub,
6 | createEventAddContextStub,
7 | } from '../stubs/context.stub';
8 | import {createModuleStub} from '../stubs/actions.module.stub';
9 | import {clearDatabase} from '../helpers/db-helper';
10 |
11 | import {AppEmitter} from '../../src/common/event-bus.service';
12 | import {StorageService} from '../../src/storage/storage.service';
13 | import * as statuses from '../../src/actions/statuses';
14 | import {Chat} from '../../src/storage/models/chat';
15 | import {Event} from '../../src/storage/models/event';
16 | import {IParams} from '../../src/common/template.service';
17 |
18 | describe('EventRemoveAction', () => {
19 | let appEmitter: AppEmitter;
20 | let storageService: StorageService;
21 |
22 | before(async () => {
23 | const testModule = await createModuleStub();
24 |
25 | appEmitter = testModule.get(AppEmitter);
26 | storageService = testModule.get(StorageService);
27 | });
28 |
29 | beforeEach(async () => {
30 | await clearDatabase(storageService);
31 | });
32 |
33 | describe('handle event', () => {
34 | it('should create chat if it does not exist yet', async () => {
35 | const chatCountBefore: number = await storageService.connection
36 | .getRepository(Chat)
37 | .count();
38 | expect(chatCountBefore).to.equal(0);
39 |
40 | await new Promise(resolve => {
41 | const ctx = createContextStub({}, resolve);
42 | appEmitter.emit(appEmitter.EVENT_REMOVE, ctx);
43 | });
44 |
45 | const chatCountAfter: number = await storageService.connection
46 | .getRepository(Chat)
47 | .count();
48 | expect(chatCountAfter).to.equal(1);
49 | });
50 |
51 | it('should return no_event response if active event was not found', async () => {
52 | const jsonRes: string = await new Promise(resolve => {
53 | const ctx = createContextStub({}, resolve);
54 | appEmitter.emit(appEmitter.EVENT_REMOVE, ctx);
55 | });
56 |
57 | const {params}: {params: IParams} = JSON.parse(jsonRes);
58 | expect(params.status).to.equal(statuses.STATUS_NO_EVENT);
59 | });
60 |
61 | describe('active event exists', () => {
62 | let events: Event[];
63 |
64 | beforeEach(async () => {
65 | await new Promise(resolve => {
66 | const ctx = createEventAddContextStub({}, resolve);
67 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
68 | });
69 | });
70 |
71 | it('should remove active event', async () => {
72 | events = await storageService.connection
73 | .getRepository(Event)
74 | .find({});
75 | expect(events).to.be.lengthOf(1);
76 | expect(events[0].active).to.equal(true);
77 |
78 | await new Promise(resolve => {
79 | const ctx = createContextStub({}, resolve);
80 | appEmitter.emit(appEmitter.EVENT_REMOVE, ctx);
81 | });
82 |
83 | events = await storageService.connection
84 | .getRepository(Event)
85 | .find({});
86 | expect(events).to.be.lengthOf(1);
87 | expect(events[0].active).to.equal(false);
88 | });
89 |
90 | it('should return success result', async () => {
91 | const jsonRes: string = await new Promise(resolve => {
92 | const ctx = createContextStub({}, resolve);
93 | appEmitter.emit(appEmitter.EVENT_REMOVE, ctx);
94 | });
95 |
96 | const {params}: {params: IParams} = JSON.parse(jsonRes);
97 | expect(params.status).to.equal(statuses.STATUS_SUCCESS);
98 | });
99 | });
100 | });
101 | });
102 |
--------------------------------------------------------------------------------
/test/actions/event_info.action.spec.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import {expect} from 'chai';
3 |
4 | import {
5 | createContextStub,
6 | createEventAddContextStub,
7 | } from '../stubs/context.stub';
8 | import {createModuleStub} from '../stubs/actions.module.stub';
9 | import {clearDatabase} from '../helpers/db-helper';
10 |
11 | import {AppEmitter} from '../../src/common/event-bus.service';
12 | import {StorageService} from '../../src/storage/storage.service';
13 | import * as statuses from '../../src/actions/statuses';
14 | import {Chat} from '../../src/storage/models/chat';
15 | import {IParams} from '../../src/common/template.service';
16 |
17 | describe('EventAddAction', () => {
18 | let appEmitter: AppEmitter;
19 | let storageService: StorageService;
20 |
21 | before(async () => {
22 | const testModule = await createModuleStub();
23 |
24 | appEmitter = testModule.get(AppEmitter);
25 | storageService = testModule.get(StorageService);
26 | });
27 |
28 | beforeEach(async () => {
29 | await clearDatabase(storageService);
30 | });
31 |
32 | describe('handle event', () => {
33 | it('should create chat if it does not exist yet', async () => {
34 | const chatCountBefore: number = await storageService.connection
35 | .getRepository(Chat)
36 | .count();
37 | expect(chatCountBefore).to.equal(0);
38 |
39 | await new Promise(resolve => {
40 | const ctx = createContextStub({}, resolve);
41 | appEmitter.emit(appEmitter.EVENT_INFO, ctx);
42 | });
43 |
44 | const chatCountAfter: number = await storageService.connection
45 | .getRepository(Chat)
46 | .count();
47 | expect(chatCountAfter).to.equal(1);
48 | });
49 |
50 | it('should return no_event response if active event was not found', async () => {
51 | const jsonRes: string = await new Promise(resolve => {
52 | const ctx = createContextStub({}, resolve);
53 | appEmitter.emit(appEmitter.EVENT_INFO, ctx);
54 | });
55 |
56 | const {params}: {params: IParams} = JSON.parse(jsonRes);
57 | expect(params.status).to.equal(statuses.STATUS_NO_EVENT);
58 | });
59 |
60 | describe('active event exists', () => {
61 | beforeEach(async () => {
62 | await new Promise(resolve => {
63 | const ctx = createEventAddContextStub({}, resolve);
64 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
65 | });
66 | });
67 |
68 | it('should return success result if active event was found', async () => {
69 | const jsonRes: string = await new Promise(resolve => {
70 | const ctx = createContextStub({}, resolve);
71 | appEmitter.emit(appEmitter.EVENT_INFO, ctx);
72 | });
73 |
74 | const {params}: {params: IParams} = JSON.parse(jsonRes);
75 | expect(params.status).to.equal(statuses.STATUS_SUCCESS);
76 | });
77 |
78 | it('should include date of event into response', async () => {
79 | const jsonRes: string = await new Promise(resolve => {
80 | const ctx = createContextStub({}, resolve);
81 | appEmitter.emit(appEmitter.EVENT_INFO, ctx);
82 | });
83 |
84 | const {data}: any = JSON.parse(jsonRes);
85 | expect(data.date).to.match(/^\d{2}-\d{2}-\d{4}\s\d{2}:\d{2}$/);
86 | });
87 |
88 | it('should include list of event members', async () => {
89 | const jsonRes: string = await new Promise(resolve => {
90 | const ctx = createContextStub({}, resolve);
91 | appEmitter.emit(appEmitter.EVENT_INFO, ctx);
92 | });
93 |
94 | const {data}: any = JSON.parse(jsonRes);
95 | expect(data.total).to.equal(0);
96 | expect(data.players).to.eql([]);
97 | });
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/src/storage/storage.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@nestjs/common';
2 | import {InjectConnection} from '@nestjs/typeorm';
3 | import {Connection, Repository, UpdateResult} from 'typeorm';
4 |
5 | import {Chat} from './models/chat';
6 | import {Event} from './models/event';
7 | import {Player} from './models/player';
8 |
9 | @Injectable()
10 | export class StorageService {
11 | private chatRepository: Repository;
12 | private eventRepository: Repository;
13 | private playerRepository: Repository;
14 |
15 | constructor(@InjectConnection() private readonly dbConnection: Connection) {
16 | this.chatRepository = this.dbConnection.getRepository(Chat);
17 | this.eventRepository = this.dbConnection.getRepository(Event);
18 | this.playerRepository = this.dbConnection.getRepository(Player);
19 | }
20 |
21 | public get connection() {
22 | return this.dbConnection;
23 | }
24 |
25 | /**
26 | * Finds chat for given chatId
27 | * If chat is not exists then creates it and returns it
28 | * @param {number} chatId
29 | * @returns {Promise}
30 | * @memberOf StorageService
31 | */
32 | public async ensureChat(chatId: number): Promise {
33 | let chat: Chat = await this.chatRepository.findOne({chatId});
34 |
35 | if (chat) {
36 | return chat;
37 | }
38 |
39 | chat = new Chat();
40 | chat.chatId = chatId;
41 |
42 | return this.chatRepository.save(chat);
43 | }
44 |
45 | /**
46 | * Updates all chat events. Makes them inactive
47 | * @param {Chat} chat
48 | * @returns {Promise}
49 | * @memberOf StorageService
50 | */
51 | public markChatEventsInactive(chat: Chat): Promise {
52 | return this.dbConnection
53 | .createQueryBuilder()
54 | .update(Event)
55 | .set({active: false})
56 | .where({chat})
57 | .execute();
58 | }
59 |
60 | /**
61 | * Creates new active event and append it to chat
62 | * @param {Chat} chat
63 | * @param {Date} date
64 | * @returns {Promise}
65 | * @memberOf StorageService
66 | */
67 | public appendChatActiveEvent(chat: Chat, date: Date): Promise {
68 | const event: Event = new Event();
69 | event.chat = chat;
70 | event.active = true;
71 | event.date = date;
72 |
73 | return this.eventRepository.save(event);
74 | }
75 |
76 | /**
77 | * Finds active event for given chat and returns it
78 | * @param {Chat} chat
79 | * @returns {(Promise)}
80 | * @memberOf StorageService
81 | */
82 | public findChatActiveEvent(chat: Chat): Promise {
83 | return this.eventRepository.findOne({where: {chat, active: true}});
84 | }
85 |
86 | /**
87 | * Returns list of players for given event
88 | * @param {Event} event
89 | * @returns {Promise}
90 | * @memberOf StorageService
91 | */
92 | public getPlayers(event: Event): Promise {
93 | return this.playerRepository.find({where: {event}});
94 | }
95 |
96 | /**
97 | * Finds player by his name for given event
98 | * @param {Event} event
99 | * @param {string} name
100 | * @returns {(Promise)}
101 | * @memberOf StorageService
102 | */
103 | public findPlayer(event: Event, name: string): Promise {
104 | return this.playerRepository.findOne({where: {event, name}});
105 | }
106 |
107 | /**
108 | * Creates new player with given name for event
109 | * @param {Event} event
110 | * @param {string} name
111 | * @returns {Promise}
112 | * @memberOf StorageService
113 | */
114 | public addPlayer(event: Event, name: string): Promise {
115 | const player: Player = new Player();
116 | player.event = event;
117 | player.name = name;
118 |
119 | return this.playerRepository.save(player);
120 | }
121 |
122 | /**
123 | * Removes given player
124 | * @param {Player} player
125 | * @returns {Promise}
126 | * @memberOf StorageService
127 | */
128 | public removePlayer(player: Player): Promise {
129 | return this.playerRepository.remove(player);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/test/integration/scenarios.spec.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import {expect} from 'chai';
3 |
4 | import {
5 | createContextStub,
6 | createEventAddContextStub,
7 | } from '../stubs/context.stub';
8 | import {createModuleStub} from '../stubs/actions.module.stub';
9 | import {clearDatabase} from '../helpers/db-helper';
10 |
11 | import {AppEmitter} from '../../src/common/event-bus.service';
12 | import {StorageService} from '../../src/storage/storage.service';
13 | import {Event} from '../../src/storage/models/event';
14 | import {Player} from '../../src/storage/models/player';
15 |
16 | describe('complex cases', () => {
17 | let appEmitter: AppEmitter;
18 | let storageService: StorageService;
19 |
20 | before(async () => {
21 | const testModule = await createModuleStub();
22 |
23 | appEmitter = testModule.get(AppEmitter);
24 | storageService = testModule.get(StorageService);
25 | });
26 |
27 | beforeEach(async () => {
28 | await clearDatabase(storageService);
29 | });
30 |
31 | function addPlayer_(name, chatId = 1) {
32 | return new Promise(resolve => {
33 | appEmitter.emit(
34 | appEmitter.PLAYER_ADD,
35 | createContextStub({text: name, chatId}, resolve),
36 | );
37 | });
38 | }
39 |
40 | let events: Event[];
41 | let players: Player[];
42 |
43 | beforeEach(async () => {
44 | await new Promise(resolve => {
45 | appEmitter.emit(
46 | appEmitter.EVENT_ADD,
47 | createEventAddContextStub({}, resolve),
48 | );
49 | });
50 |
51 | await addPlayer_('player-1-1');
52 | await addPlayer_('player-1-2');
53 |
54 | events = await storageService.connection
55 | .getRepository(Event)
56 | .find({where: {active: true}});
57 | expect(events).to.be.lengthOf(1);
58 |
59 | players = await storageService.connection
60 | .getRepository(Player)
61 | .find({where: {event: events[0]}});
62 |
63 | expect(players).to.be.lengthOf(2);
64 | expect(players[0].name).to.equal('player-1-1');
65 | expect(players[1].name).to.equal('player-1-2');
66 | });
67 |
68 | it('should create empty players set for each created event', async () => {
69 | await new Promise(resolve => {
70 | appEmitter.emit(
71 | appEmitter.EVENT_ADD,
72 | createEventAddContextStub({}, resolve),
73 | );
74 | });
75 |
76 | await addPlayer_('player-2-1');
77 |
78 | events = await storageService.connection
79 | .getRepository(Event)
80 | .find({where: {active: true}});
81 | expect(events).to.be.lengthOf(1);
82 |
83 | players = await storageService.connection
84 | .getRepository(Player)
85 | .find({where: {event: events[0]}});
86 |
87 | expect(players).to.be.lengthOf(1);
88 | expect(players[0].name).to.equal('player-2-1');
89 |
90 | players = await storageService.getPlayers(events[0]);
91 |
92 | expect(players).to.be.lengthOf(1);
93 | expect(players[0].name).to.equal('player-2-1');
94 | });
95 |
96 | it('should add players with same names to different events', async () => {
97 | await new Promise(resolve => {
98 | appEmitter.emit(
99 | appEmitter.EVENT_ADD,
100 | createEventAddContextStub({}, resolve),
101 | );
102 | });
103 |
104 | await addPlayer_('player-1-1');
105 | await addPlayer_('player-1-2');
106 |
107 | events = await storageService.connection
108 | .getRepository(Event)
109 | .find({where: {active: true}});
110 |
111 | players = await storageService.getPlayers(events[0]);
112 |
113 | expect(players).to.be.lengthOf(2);
114 | expect(players[0].name).to.equal('player-1-1');
115 | expect(players[1].name).to.equal('player-1-2');
116 | });
117 |
118 | it('should have own active event for different chats', async () => {
119 | await new Promise(resolve => {
120 | const ctx = createEventAddContextStub({chatId: 2}, resolve);
121 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
122 | });
123 |
124 | events = await storageService.connection
125 | .getRepository(Event)
126 | .find({relations: ['chat']});
127 |
128 | expect(events[0].chat.chatId).to.equal(1);
129 | expect(events[0].active).to.equal(true);
130 | expect(events[1].chat.chatId).to.equal(2);
131 | expect(events[1].active).to.equal(true);
132 |
133 | await addPlayer_('player-2-1', 2);
134 | await addPlayer_('player-2-2', 2);
135 |
136 | const jsonRes1: string = await new Promise(resolve => {
137 | const ctx = createContextStub({chatId: 1}, resolve);
138 | appEmitter.emit(appEmitter.EVENT_INFO, ctx);
139 | });
140 | const data1: any = JSON.parse(jsonRes1).data;
141 |
142 | const jsonRes2: string = await new Promise(resolve => {
143 | const ctx = createContextStub({chatId: 2}, resolve);
144 | appEmitter.emit(appEmitter.EVENT_INFO, ctx);
145 | });
146 | const data2: any = JSON.parse(jsonRes2).data;
147 |
148 | expect(data1.players).to.eql([
149 | {index: 1, name: 'player-1-1'},
150 | {index: 2, name: 'player-1-2'},
151 | ]);
152 | expect(data2.players).to.eql([
153 | {index: 1, name: 'player-2-1'},
154 | {index: 2, name: 'player-2-2'},
155 | ]);
156 | });
157 | });
158 |
--------------------------------------------------------------------------------
/test/actions/event_add.action.spec.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import {expect} from 'chai';
3 |
4 | import {
5 | createContextStub,
6 | createEventAddContextStub,
7 | } from '../stubs/context.stub';
8 | import {createModuleStub} from '../stubs/actions.module.stub';
9 | import {clearDatabase} from '../helpers/db-helper';
10 |
11 | import {AppEmitter} from '../../src/common/event-bus.service';
12 | import {StorageService} from '../../src/storage/storage.service';
13 | import {Chat} from '../../src/storage/models/chat';
14 | import {Event} from '../../src/storage/models/event';
15 |
16 | describe('EventAddAction', () => {
17 | let appEmitter: AppEmitter;
18 | let storageService: StorageService;
19 |
20 | async function assertNoEvents_() {
21 | const eventsAmount = await storageService.connection
22 | .getRepository(Event)
23 | .count();
24 |
25 | expect(eventsAmount).to.equal(0);
26 | }
27 |
28 | before(async () => {
29 | const testModule = await createModuleStub();
30 |
31 | appEmitter = testModule.get(AppEmitter);
32 | storageService = testModule.get(StorageService);
33 | });
34 |
35 | beforeEach(async () => {
36 | await clearDatabase(storageService);
37 | });
38 |
39 | describe('handle event', () => {
40 | it('should create chat if it does not exist yet', async () => {
41 | await assertNoEvents_();
42 |
43 | await new Promise(resolve => {
44 | const ctx = createEventAddContextStub({}, resolve);
45 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
46 | });
47 |
48 | const chatCountAfter: number = await storageService.connection
49 | .getRepository(Chat)
50 | .count();
51 | expect(chatCountAfter).to.equal(1);
52 | });
53 |
54 | it('should create new event and mark it as active', async () => {
55 | await assertNoEvents_();
56 |
57 | await new Promise(resolve => {
58 | const ctx = createEventAddContextStub({}, resolve);
59 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
60 | });
61 |
62 | const events: Event[] = await storageService.connection
63 | .getRepository(Event)
64 | .find({});
65 |
66 | expect(events).to.be.lengthOf(1);
67 | expect(events[0].active).to.equal(true);
68 | });
69 |
70 | it('should return information about created event', async () => {
71 | const jsonRes: string = await new Promise(resolve => {
72 | const ctx = createEventAddContextStub({}, resolve);
73 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
74 | });
75 |
76 | const {data} = JSON.parse(jsonRes);
77 | expect(data.date).to.match(/^\d{2}-\d{2}-\d{4}\s\d{2}:\d{2}$/);
78 | });
79 |
80 | describe('it should not allow to create event', () => {
81 | it('without date', async () => {
82 | const jsonRes: string = await new Promise(resolve => {
83 | const ctx = createEventAddContextStub({
84 | text: '/event_add',
85 | }, resolve);
86 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
87 | });
88 |
89 | const {params} = JSON.parse(jsonRes);
90 | expect(params.status).to.equal('invalid_date');
91 | await assertNoEvents_();
92 | });
93 |
94 | it('with date given in invalid format', async () => {
95 | const jsonRes: string = await new Promise(resolve => {
96 | const ctx = createEventAddContextStub({
97 | text: '/event_add aa-2a-b',
98 | }, resolve);
99 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
100 | });
101 |
102 | const {params} = JSON.parse(jsonRes);
103 | expect(params.status).to.equal('invalid_date');
104 | await assertNoEvents_();
105 | });
106 |
107 | it('with date in past', async () => {
108 | const jsonRes: string = await new Promise(resolve => {
109 | const ctx = createEventAddContextStub({
110 | text: '/event_add 01-01-1970 00:01',
111 | }, resolve);
112 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
113 | });
114 |
115 | const {params} = JSON.parse(jsonRes);
116 | expect(params.status).to.equal('invalid_date_past');
117 | await assertNoEvents_();
118 | });
119 | });
120 |
121 | describe('existed events', () => {
122 | let events: Event[];
123 |
124 | beforeEach(async () => {
125 | await assertNoEvents_();
126 |
127 | await new Promise(resolve => {
128 | const ctx = createEventAddContextStub({}, resolve);
129 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
130 | });
131 |
132 | events = await storageService.connection
133 | .getRepository(Event)
134 | .find({});
135 | expect(events.length).to.equal(1);
136 | expect(events[0].active).to.equal(true);
137 | });
138 |
139 | it('should make all existed events inactive', async () => {
140 | await new Promise(resolve => {
141 | const ctx = createEventAddContextStub({}, resolve);
142 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
143 | });
144 |
145 | events = await storageService.connection
146 | .getRepository(Event)
147 | .find({});
148 | expect(events.length).to.equal(2);
149 | expect(events[0].active).to.equal(false);
150 | expect(events[1].active).to.equal(true);
151 | });
152 |
153 | it('should not deactivate existed event if current event date is invalid', async () => {
154 | await new Promise(resolve => {
155 | const ctx = createEventAddContextStub({
156 | text: '/event_add',
157 | }, resolve);
158 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
159 | });
160 |
161 | events = await storageService.connection
162 | .getRepository(Event)
163 | .find({});
164 | expect(events.length).to.equal(1);
165 | expect(events[0].active).to.equal(true);
166 | });
167 | });
168 | });
169 | });
170 |
--------------------------------------------------------------------------------
/test/actions/player_remove.action.spec.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import {expect} from 'chai';
3 |
4 | import {
5 | createContextStub,
6 | createEventAddContextStub,
7 | } from '../stubs/context.stub';
8 | import {createModuleStub} from '../stubs/actions.module.stub';
9 | import {clearDatabase} from '../helpers/db-helper';
10 |
11 | import {AppEmitter} from '../../src/common/event-bus.service';
12 | import {StorageService} from '../../src/storage/storage.service';
13 | import * as statuses from '../../src/actions/statuses';
14 | import {Chat} from '../../src/storage/models/chat';
15 | import {Player} from '../../src/storage/models/player';
16 | import {IParams} from '../../src/common/template.service';
17 |
18 | describe('PlayerRemoveAction', () => {
19 | let appEmitter: AppEmitter;
20 | let storageService: StorageService;
21 |
22 | before(async () => {
23 | const testModule = await createModuleStub();
24 |
25 | appEmitter = testModule.get(AppEmitter);
26 | storageService = testModule.get(StorageService);
27 | });
28 |
29 | beforeEach(async () => {
30 | await clearDatabase(storageService);
31 | });
32 |
33 | describe('handle event', () => {
34 | it('should create chat if it does not exist yet', async () => {
35 | const chatCountBefore: number = await storageService.connection
36 | .getRepository(Chat)
37 | .count();
38 | expect(chatCountBefore).to.equal(0);
39 |
40 | await new Promise(resolve => {
41 | const ctx = createContextStub({lang: 'en', chatId: 1}, resolve);
42 | appEmitter.emit(appEmitter.PLAYER_REMOVE, ctx);
43 | });
44 |
45 | const chatCountAfter: number = await storageService.connection
46 | .getRepository(Chat)
47 | .count();
48 | expect(chatCountAfter).to.equal(1);
49 | });
50 |
51 | it('should return no_event response if active event was not found', async () => {
52 | const jsonRes: string = await new Promise(resolve => {
53 | const ctx = createContextStub({lang: 'en', chatId: 1}, resolve);
54 | appEmitter.emit(appEmitter.PLAYER_REMOVE, ctx);
55 | });
56 |
57 | const {params}: {params: IParams} = JSON.parse(jsonRes);
58 | expect(params.status).to.equal(statuses.STATUS_NO_EVENT);
59 | });
60 |
61 | describe('active event exists', () => {
62 | beforeEach(async () => {
63 | await new Promise(resolve => {
64 | const ctx = createEventAddContextStub(
65 | {lang: 'en', chatId: 1},
66 | resolve,
67 | );
68 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
69 | });
70 | });
71 |
72 | it('should return no_player response if player was not found by given name', async () => {
73 | const jsonRes: string = await new Promise(resolve => {
74 | const ctx = createContextStub(
75 | {text: 'John Smith'},
76 | resolve,
77 | );
78 | appEmitter.emit(appEmitter.PLAYER_REMOVE, ctx);
79 | });
80 |
81 | const {params}: {params: IParams} = JSON.parse(jsonRes);
82 | expect(params.status).to.equal(statuses.STATUS_NO_PLAYER);
83 | });
84 |
85 | describe('player exist', () => {
86 | async function assertExistedPlayer_() {
87 | const players: Player[] = await storageService.connection
88 | .getRepository(Player)
89 | .find();
90 | expect(players[0].name).to.equal('John Smith');
91 | }
92 |
93 | async function assertNotExistedPlayer_() {
94 | const playersCount: number = await storageService.connection
95 | .getRepository(Player)
96 | .count();
97 |
98 | expect(playersCount).to.equal(0);
99 | }
100 |
101 | beforeEach(async () => {
102 | await new Promise(resolve => {
103 | const ctx = createContextStub(
104 | {text: 'John Smith'},
105 | resolve,
106 | );
107 | appEmitter.emit(appEmitter.PLAYER_ADD, ctx);
108 | });
109 | });
110 |
111 | it('should remove player with given name', async () => {
112 | await assertExistedPlayer_();
113 |
114 | await new Promise(resolve => {
115 | const ctx = createContextStub(
116 | {text: 'John Smith'},
117 | resolve,
118 | );
119 | appEmitter.emit(appEmitter.PLAYER_REMOVE, ctx);
120 | });
121 |
122 | await assertNotExistedPlayer_();
123 | });
124 |
125 | it('should remove message owner as player if name was not set', async () => {
126 | await assertExistedPlayer_();
127 |
128 | await new Promise(resolve => {
129 | const ctx = createContextStub(
130 | {firstName: 'John', lastName: 'Smith'},
131 | resolve,
132 | );
133 | appEmitter.emit(appEmitter.PLAYER_REMOVE, ctx);
134 | });
135 |
136 | await assertNotExistedPlayer_();
137 | });
138 |
139 | it('should return success result', async () => {
140 | const jsonRes: string = await new Promise(resolve => {
141 | const ctx = createContextStub(
142 | {text: 'John Smith'},
143 | resolve,
144 | );
145 | appEmitter.emit(appEmitter.PLAYER_REMOVE, ctx);
146 | });
147 |
148 | const {params}: {params: IParams} = JSON.parse(jsonRes);
149 | expect(params.status).to.equal(statuses.STATUS_SUCCESS);
150 | });
151 |
152 | it('should inlude name of removed player into result', async () => {
153 | const jsonRes: string = await new Promise(resolve => {
154 | const ctx = createContextStub(
155 | {text: 'John Smith'},
156 | resolve,
157 | );
158 | appEmitter.emit(appEmitter.PLAYER_REMOVE, ctx);
159 | });
160 |
161 | const {data} = JSON.parse(jsonRes);
162 | expect(data.name).to.equal('John Smith');
163 | });
164 |
165 | it('should include list of players into result', async () => {
166 | await new Promise(resolve => {
167 | const ctx = createContextStub(
168 | {text: 'Jack Wayne'},
169 | resolve,
170 | );
171 | appEmitter.emit(appEmitter.PLAYER_ADD, ctx);
172 | });
173 |
174 | const jsonRes: string = await new Promise(resolve => {
175 | const ctx = createContextStub(
176 | {text: 'John Smith'},
177 | resolve,
178 | );
179 | appEmitter.emit(appEmitter.PLAYER_REMOVE, ctx);
180 | });
181 |
182 | const {data} = JSON.parse(jsonRes);
183 | expect(data.total).to.equal(1);
184 | expect(data.players[0].index).to.equal(1);
185 | expect(data.players[0].name).to.equal('Jack Wayne');
186 | });
187 | });
188 | });
189 | });
190 | });
191 |
--------------------------------------------------------------------------------
/test/actions/player_add.action.spec.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import {expect} from 'chai';
3 |
4 | import {
5 | createContextStub,
6 | createEventAddContextStub,
7 | } from '../stubs/context.stub';
8 | import {createModuleStub} from '../stubs/actions.module.stub';
9 | import {clearDatabase} from '../helpers/db-helper';
10 |
11 | import {AppEmitter} from '../../src/common/event-bus.service';
12 | import {StorageService} from '../../src/storage/storage.service';
13 | import * as statuses from '../../src/actions/statuses';
14 | import {Chat} from '../../src/storage/models/chat';
15 | import {Player} from '../../src/storage/models/player';
16 | import {IParams} from '../../src/common/template.service';
17 |
18 | describe('PlayerAddAction', () => {
19 | let appEmitter: AppEmitter;
20 | let storageService: StorageService;
21 |
22 | before(async () => {
23 | const testModule = await createModuleStub();
24 |
25 | appEmitter = testModule.get(AppEmitter);
26 | storageService = testModule.get(StorageService);
27 | });
28 |
29 | beforeEach(async () => {
30 | await clearDatabase(storageService);
31 | });
32 |
33 | describe('handle event', () => {
34 | it('should create chat if it does not exist yet', async () => {
35 | const chatCountBefore: number = await storageService.connection
36 | .getRepository(Chat)
37 | .count();
38 | expect(chatCountBefore).to.equal(0);
39 |
40 | await new Promise(resolve => {
41 | const ctx = createContextStub({}, resolve);
42 | appEmitter.emit(appEmitter.PLAYER_ADD, ctx);
43 | });
44 |
45 | const chatCountAfter: number = await storageService.connection
46 | .getRepository(Chat)
47 | .count();
48 | expect(chatCountAfter).to.equal(1);
49 | });
50 |
51 | it('should return no_event response if active event was not found', async () => {
52 | const jsonRes: string = await new Promise(resolve => {
53 | const ctx = createContextStub({}, resolve);
54 | appEmitter.emit(appEmitter.PLAYER_ADD, ctx);
55 | });
56 |
57 | const {params}: {params: IParams} = JSON.parse(jsonRes);
58 | expect(params.status).to.equal(statuses.STATUS_NO_EVENT);
59 | });
60 |
61 | describe('active event exists', () => {
62 | beforeEach(async () => {
63 | await new Promise(resolve => {
64 | const ctx = createEventAddContextStub({}, resolve);
65 | appEmitter.emit(appEmitter.EVENT_ADD, ctx);
66 | });
67 | });
68 |
69 | it('should add player with given name', async () => {
70 | await new Promise(resolve => {
71 | const ctx = createContextStub(
72 | {text: 'John Smith'},
73 | resolve,
74 | );
75 | appEmitter.emit(appEmitter.PLAYER_ADD, ctx);
76 | });
77 |
78 | const players: Player[] = await storageService.connection
79 | .getRepository(Player)
80 | .find();
81 | expect(players[0].name).to.equal('John Smith');
82 | });
83 |
84 | describe('should add message owner as player ', () => {
85 | it('if name was not set', async () => {
86 | await new Promise(resolve => {
87 | const ctx = createContextStub(
88 | {firstName: 'John', lastName: 'Smith'},
89 | resolve,
90 | );
91 | appEmitter.emit(appEmitter.PLAYER_ADD, ctx);
92 | });
93 |
94 | const players: Player[] = await storageService.connection
95 | .getRepository(Player)
96 | .find();
97 | expect(players[0].name).to.equal('John Smith');
98 | });
99 |
100 | it('and use his first name only if last name was not set', async () => {
101 | await new Promise(resolve => {
102 | const ctx = createContextStub(
103 | {firstName: 'John'},
104 | resolve,
105 | );
106 | appEmitter.emit(appEmitter.PLAYER_ADD, ctx);
107 | });
108 |
109 | const players: Player[] = await storageService.connection
110 | .getRepository(Player)
111 | .find();
112 | expect(players[0].name).to.equal('John');
113 | });
114 |
115 | it('and use his last name only if first name was not set', async () => {
116 | await new Promise(resolve => {
117 | const ctx = createContextStub(
118 | {lastName: 'Smith'},
119 | resolve,
120 | );
121 | appEmitter.emit(appEmitter.PLAYER_ADD, ctx);
122 | });
123 |
124 | const players: Player[] = await storageService.connection
125 | .getRepository(Player)
126 | .find();
127 | expect(players[0].name).to.equal('Smith');
128 | });
129 | });
130 |
131 | it('should return success result', async () => {
132 | const jsonRes: string = await new Promise(resolve => {
133 | const ctx = createContextStub(
134 | {firstName: 'John', lastName: 'Smith'},
135 | resolve,
136 | );
137 | appEmitter.emit(appEmitter.PLAYER_ADD, ctx);
138 | });
139 |
140 | const {params}: {params: IParams} = JSON.parse(jsonRes);
141 | expect(params.status).to.equal(statuses.STATUS_SUCCESS);
142 | });
143 |
144 | it('should include name of added player into result', async () => {
145 | const jsonRes: string = await new Promise(resolve => {
146 | const ctx = createContextStub(
147 | {firstName: 'John', lastName: 'Smith'},
148 | resolve,
149 | );
150 | appEmitter.emit(appEmitter.PLAYER_ADD, ctx);
151 | });
152 |
153 | const {data} = JSON.parse(jsonRes);
154 | expect(data.name).to.equal('John Smith');
155 | });
156 |
157 | it('should include list of players into result', async () => {
158 | const jsonRes: string = await new Promise(resolve => {
159 | const ctx = createContextStub(
160 | {firstName: 'John', lastName: 'Smith'},
161 | resolve,
162 | );
163 | appEmitter.emit(appEmitter.PLAYER_ADD, ctx);
164 | });
165 |
166 | const {data} = JSON.parse(jsonRes);
167 | expect(data.total).to.equal(1);
168 | expect(data.players[0].index).to.equal(1);
169 | expect(data.players[0].name).to.equal('John Smith');
170 | });
171 |
172 | it('should return already added response if player with given name has already been added', async () => {
173 | await new Promise(resolve => {
174 | const ctx = createContextStub(
175 | {firstName: 'John', lastName: 'Smith'},
176 | resolve,
177 | );
178 | appEmitter.emit(appEmitter.PLAYER_ADD, ctx);
179 | });
180 |
181 | const jsonRes: string = await new Promise(resolve => {
182 | const ctx = createContextStub(
183 | {firstName: 'John', lastName: 'Smith'},
184 | resolve,
185 | );
186 | appEmitter.emit(appEmitter.PLAYER_ADD, ctx);
187 | });
188 |
189 | const {params}: {params: IParams} = JSON.parse(jsonRes);
190 | expect(params.status).to.equal(statuses.STATUS_ALREADY_ADDED);
191 | });
192 | });
193 | });
194 | });
195 |
--------------------------------------------------------------------------------