This commit is contained in:
Kirill Ivlev 2024-10-29 22:42:21 +04:00
commit 379b97392d
37 changed files with 17031 additions and 0 deletions

6
.dockerignore Normal file
View file

@ -0,0 +1,6 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
dist
.env

5
.env Normal file
View file

@ -0,0 +1,5 @@
TELEGRAM_TOKEN=6908527821:AAGedji_AxXikij9Pl8mxC0bOg28KVUvgFM
RMQ_URL=amqps://zilrbxbr:h3UUcl8A6mrZ2dNtZbx1K73PAtSJ3rST@sparrow.rmq.cloudamqp.com/zilrbxbr
RMQ_OUTBOX_Q=game_queue
RMQ_COMMANDS_OUTPUT_Q=messages_inbox
RMQ_PHOTOS_Q=tgd_photos

25
.eslintrc.js Normal file
View file

@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

35
.gitignore vendored Normal file
View file

@ -0,0 +1,35 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

4
.prettierrc Normal file
View file

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

63
Dockerfile Normal file
View file

@ -0,0 +1,63 @@
###################
# BUILD FOR LOCAL DEVELOPMENT
###################
FROM --platform=linux/amd64 node:18-alpine As development
# Create app directory
WORKDIR /usr/src/app
# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY --chown=node:node package*.json ./
# Install app dependencies using the `npm ci` command instead of `npm install`
RUN npm ci
# Bundle app source
COPY --chown=node:node . .
# Use the node user from the image (instead of the root user)
USER node
###################
# BUILD FOR PRODUCTION
###################
FROM --platform=linux/amd64 node:18-alpine As build
WORKDIR /usr/src/app
COPY --chown=node:node package*.json ./
# In order to run `npm run build` we need access to the Nest CLI which is a dev dependency. In the previous development stage we ran `npm ci` which installed all dependencies, so we can copy over the node_modules directory from the development image
COPY --chown=node:node --from=development /usr/src/app/node_modules ./node_modules
COPY --chown=node:node . .
# Run the build command which creates the production bundle
RUN npm run build
# Set NODE_ENV environment variable
ENV NODE_ENV production
# Running `npm ci` removes the existing node_modules directory and passing in --only=production ensures that only the production dependencies are installed. This ensures that the node_modules directory is as optimized as possible
RUN npm ci --only=production && npm cache clean --force
USER node
###################
# PRODUCTION
###################
FROM --platform=linux/amd64 node:18-alpine As production
WORKDIR /usr/src/app
# Copy the bundled code from the build stage to the production image
COPY --chown=node:node --from=build /usr/src/app/node_modules ./node_modules
COPY --chown=node:node --from=build /usr/src/app/dist ./dist
# Start the server using the production build
CMD [ "node", "dist/main.js" ]

73
README.md Normal file
View file

@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

8
nest-cli.json Normal file
View file

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

15857
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

76
package.json Normal file
View file

@ -0,0 +1,76 @@
{
"name": "tgd-telegram-service",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
"@nestjs/cqrs": "^10.2.6",
"@nestjs/microservices": "^10.2.8",
"@nestjs/platform-express": "^10.0.0",
"amqp-connection-manager": "^4.1.14",
"amqplib": "^0.10.3",
"nestjs-telegraf": "^2.7.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"telegraf": "^4.15.0"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View file

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

12
src/app.controller.ts Normal file
View file

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

25
src/app.module.ts Normal file
View file

@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import {ConfigModule, ConfigService} from "@nestjs/config";
import {TelegrafModule} from "nestjs-telegraf";
import {BotModule} from "./bot/bot.module";
import {sessionMiddleware} from "./middleware/session.middleware";
import {MessageController} from "./message.controller";
@Module({
imports: [ConfigModule.forRoot(),
BotModule,
TelegrafModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
token: configService.get<string>('TELEGRAM_TOKEN'),
include: [BotModule],
middlewares: [sessionMiddleware],
}),
inject: [ConfigService],
})],
controllers: [AppController, MessageController],
providers: [AppService],
})
export class AppModule {}

8
src/app.service.ts Normal file
View file

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'TGH Game Server!';
}
}

25
src/bot/EchoService.ts Normal file
View file

@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { InjectBot } from 'nestjs-telegraf';
import { Telegraf } from 'telegraf';
import { ExtraPoll, ExtraReplyMessage } from 'telegraf/typings/telegram-types';
import { Messages } from './tg.text';
@Injectable()
export class EchoService {
constructor(
@InjectBot() private bot: Telegraf,
) {}
public async test(chatId: number) {
await this.bot.telegram.sendMessage(chatId, 'test');
}
public async enterQuiz(chatId: number) {
const extra: ExtraReplyMessage = {};
await this.bot.telegram.sendMessage(chatId, '🥇 Да начнется битва!\n', {
reply_markup: {
keyboard: [[{ text: Messages.GO }]],
},
});
}
}

65
src/bot/bot.module.ts Normal file
View file

@ -0,0 +1,65 @@
import {BotUpdate} from './bot.update';
import {Module} from '@nestjs/common';
import {RegisterScene} from './scenes/register.scene';
import {RegisterNamePrompt} from './scenes/register.name.prompt';
import {EchoService} from './EchoService';
import {QuizScene} from './scenes/quiz.scene';
import {RegisterPhotoScene} from './scenes/register.photo.scene';
import {GlobalCommands} from './global-commands';
import {ClientProxyFactory, Transport} from '@nestjs/microservices';
import * as process from "process";
import {ConfigModule} from "@nestjs/config";
import AppConsts from "../constants";
const cmdHandles = [
//TgPostCardsToUserCommandHandler,
//TgCardSelectionSceneCommandHandler,
//RemoveCardFromUserCommandHandler,
];
@Module({
imports: [
ConfigModule,
],
providers: [
BotUpdate,
RegisterScene,
RegisterNamePrompt,
EchoService,
QuizScene,
RegisterPhotoScene,
GlobalCommands,
{
provide: AppConsts.GameServiceName,
useFactory: () =>
ClientProxyFactory.create({
transport: Transport.RMQ,
options: {
urls: [process.env.RMQ_URL],
queue: process.env.RMQ_OUTBOX_Q,
queueOptions: {
durable: false,
},
},
}),
},
{
provide: AppConsts.PhotoServiceName,
useFactory: () =>
ClientProxyFactory.create({
transport: Transport.RMQ,
options: {
urls: [process.env.RMQ_URL],
queue: process.env.RMQ_PHOTOS_Q,
queueOptions: {
durable: false,
},
},
}),
},
...cmdHandles,
],
exports: [EchoService],
})
export class BotModule {}

130
src/bot/bot.update.ts Normal file
View file

@ -0,0 +1,130 @@
import {
Command,
Ctx,
Hears,
Start,
Update,
Sender,
Help,
On, Message,
} from 'nestjs-telegraf';
import { UpdateType as TelegrafUpdateType } from 'telegraf/typings/telegram-types';
import { Context } from './context.interface';
import { ExtraReplyMessage } from 'telegraf/typings/telegram-types';
import { Markup } from 'telegraf';
import {
QUIZ_SCENE,
REGISTER_PHOTO_SCENE,
REGISTER_SCENE_ID,
} from './scenes/scenes.const';
import { Messages } from './tg.text';
import { GlobalCommands } from './global-commands';
import {Inject, Logger} from "@nestjs/common";
import AppConsts from "../constants";
import {ClientProxy} from "@nestjs/microservices";
import {catchError} from "rxjs";
@Update()
export class BotUpdate {
readonly numbers = Messages.answerNumbers;
private readonly logger = new Logger(BotUpdate.name);
constructor(
@Inject(AppConsts.GameServiceName) private gameService: ClientProxy,
private globalCmd: GlobalCommands,
) {
}
@Start()
async startCommand(@Ctx() ctx: Context) {
this.logger.verbose(`Sending GuestInfo to MQTT for ${ctx.message.from.first_name} / ${ctx.message.from.id}`);
this.gameService.send({cmd: 'GuestInfo'}, {user: ctx.from.id})
.pipe(catchError((val) => {
console.log(val);
return 'Error';
}),)
.subscribe(async (result) => {
if (result) {
await ctx.reply(
`🤟 Все путем, ты уже зарегистрирован, расслабься и жди указаний\r\nМожет быть`,
);
this.globalCmd.printCommands(ctx);
} else {
let reply = `👋 Привет, ${ctx.message.from.first_name}\\!\r\n`;
reply +=
'Я не вижу тебя в списке зарегистрированных участников, пройдем регистрацию?';
await ctx.replyWithMarkdownV2(reply, {
reply_markup: {
keyboard: [[{text: Messages.IM_IN}]],
},
});
}
});
}
@Hears(Messages.IM_IN)
async onRegisterCommand(@Ctx() ctx: Context): Promise<void> {
await ctx.scene.enter(REGISTER_SCENE_ID);
}
@Hears(Messages.GO)
async onGoCommand(@Ctx() ctx: Context) {
await ctx.scene.enter(QUIZ_SCENE);
}
@Command('photo')
async onPhotoCommand(@Ctx() ctx: Context) {
await ctx.scene.enter(REGISTER_PHOTO_SCENE);
}
@Command('cards')
async onCardCommand(@Ctx() ctx: Context) {
this.gameService.emit({ cmd: 'GetCards'}, { user: ctx.from.id, inline: false});
}
@Command('next')
async onNextCommand(@Ctx() ctx: Context) {
if(ctx.from.id === 11178819) {
this.gameService.emit({ cmd: 'CompleteQueue'}, { user: ctx.from.id })
}
}
@On('callback_query')
async onInlineQuery(@Ctx() ctx: Context) {
console.log('callback query');
console.log(ctx.callbackQuery);
}
@Hears(Messages.CHANGE_PHOTO)
onChangePhoto(@Ctx() ctx: Context) {
ctx.scene.enter(REGISTER_PHOTO_SCENE);
}
@On('text')
async onMsg(@Message('text') msg: string, @Ctx() ctx: Context) {
const chars = [...msg];
if (['1', '2', '3', '4'].includes(chars[0])) {
ctx.scene.enter(QUIZ_SCENE, { answering: true }, false);
}
if (msg.includes(Messages.EMOJI_CARD)) {
ctx.scene.enter(QUIZ_SCENE, { answering: true}, false);
}
}
}
/*
@Help()
async helpCommand(@Ctx() ctx: Context) {
await ctx.reply('ты пидор');
}
@On('sticker')
async onSticker(@Ctx() ctx: Context) {
console.log(ctx.message.from);
await ctx.reply('👍');
}
*/

View file

@ -0,0 +1,51 @@
/* import { CommandBus, CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { RemoveCardFromUserCommand } from '../../game/commands/remove-card-from-user.command';
import { GuestsService } from '../../guests/guests.service';
import { SharedService } from '../../shared/shared.service';
import { PostCardsToUserCommand } from '../../game/commands/post-cards-to-user.command';
import { InjectBot } from 'nestjs-telegraf';
import { Telegraf } from 'telegraf';
import { Messages } from '../tg.text';
@CommandHandler(RemoveCardFromUserCommand)
export class RemoveCardFromUserCommandHandler implements ICommandHandler<RemoveCardFromUserCommand> {
constructor(
@InjectBot() private bot: Telegraf,
private guestService: GuestsService,
private sharedService: SharedService,
private cmdBus: CommandBus,
) {
}
async execute(command: RemoveCardFromUserCommand): Promise<any> {
const guest = await this.guestService.findById(command.telegramId);
const data = await this.sharedService.getConfig(`buttons_${command.telegramId}`);
const extra = {
reply_markup: {
remove_keyboard: false,
keyboard: [],
},
};
const buttons = JSON.parse(data.value);
let found = false;
buttons.reply_markup.keyboard.forEach((item) => {
if (item[0].text.includes(command.card.description) && !found) {
found = true;
} else {
extra.reply_markup.keyboard.push(
[ { ...item[0] } ]
)
}
});
if (extra.reply_markup.keyboard.length === 0) {
extra.reply_markup.remove_keyboard = true;
}
await this.sharedService.setConfig(`buttons_${command.telegramId}`, JSON.stringify(extra));
await this.bot.telegram.sendMessage(
guest.chatId,
Messages.SELECT_CARD,
extra,
);
}
}
*/

View file

@ -0,0 +1,22 @@
/* import { CommandBus, CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CardSelectionTimeExceedCommand } from '../../game/commands/card-selection-time-exceed.command';
import { Timeout } from '@nestjs/schedule';
import { TGD_Config } from '../../../app.config';
import { Logger } from '@nestjs/common';
import { HideKeyboardCommand } from '../../game/commands/hide-keyboard.command';
@CommandHandler(CardSelectionTimeExceedCommand)
export class TgCardSelectionSceneCommandHandler implements ICommandHandler<CardSelectionTimeExceedCommand> {
private logger = new Logger(TgCardSelectionSceneCommandHandler.name);
constructor(private cmdBus: CommandBus) {
}
execute(command: CardSelectionTimeExceedCommand): Promise<any> {
this.logger.verbose(`Timeout of selecting cards`);
return this.cmdBus.execute(
new HideKeyboardCommand('Время выбора карты истекло'),
);
return Promise.resolve(undefined);
}
}
*/

View file

@ -0,0 +1,39 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { InjectBot } from 'nestjs-telegraf';
import { Telegraf } from 'telegraf';
import { Messages } from '../tg.text';
/*
@CommandHandler(PostCardsToUserCommand)
export class TgPostCardsToUserCommandHandler implements ICommandHandler<PostCardsToUserCommand> {
constructor(
@InjectBot() private bot: Telegraf,
private sharedService: SharedService,
) {}
async execute(command: PostCardsToUserCommand): Promise<any> {
const extra = {
reply_markup: {
keyboard: [],
},
};
if (command.cards.length === 0) {
return;
}
command.cards.forEach((card) => {
extra.reply_markup.keyboard.push([
{ text: Messages.EMOJI_CARD + ' ' + card },
]);
});
await this.sharedService.setConfig(`buttons_${command.chatId}`,
JSON.stringify(extra),
);
return this.bot.telegram.sendMessage(
command.chatId,
Messages.SELECT_CARD,
extra,
);
}
}
*/

View file

@ -0,0 +1,4 @@
import { Scenes } from 'telegraf';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Context extends Scenes.SceneContext {}

View file

@ -0,0 +1,23 @@
import { Context } from './context.interface';
import { Messages } from './tg.text';
import { Injectable } from '@nestjs/common';
@Injectable()
export class GlobalCommands {
public printCommands(ctx: Context) {
ctx.replyWithMarkdown('ты хочешь:', {
reply_markup: {
keyboard: [
[{ text: Messages.CHANGE_PHOTO }],
[{ text: Messages.NOTHING_THANKS }],
],
},
});
}
public hideKeyboard(ctx: Context) {
ctx.replyWithMarkdown('-', {
reply_markup: {
remove_keyboard: true,
},
});
}
}

View file

@ -0,0 +1,98 @@
import { Command, Ctx, Message, On, Scene, SceneEnter } from 'nestjs-telegraf';
import { QUIZ_SCENE } from './scenes.const';
import { GlobalCommands } from '../global-commands';
import {Inject, Logger} from '@nestjs/common';
import { Messages } from '../tg.text';
import {Context} from "../context.interface";
import AppConsts from "../../constants";
import {ClientProxy} from "@nestjs/microservices";
@Scene(QUIZ_SCENE)
export class QuizScene {
private readonly logger = new Logger(QuizScene.name);
constructor(
private globalCmd: GlobalCommands,
@Inject(AppConsts.GameServiceName) private gameService: ClientProxy,
) {}
@SceneEnter()
async onSceneEnter(@Ctx() ctx: Context, @Message('text') text: string) {
if (ctx.session.__scenes.state.hasOwnProperty('answering')) {
return this.onText(text, ctx);
}
await ctx.reply(
'Ответы на вопросы будут появляться туть, кто первый тот победил',
);
}
@Command('leave')
async onLeaveScene(@Ctx() ctx: Context) {
await ctx.scene.leave();
}
@Command('cards')
async onCardCommand(@Ctx() ctx: Context) {
this.logger.verbose(`cards command (quiz)`);
this.gameService.emit({ cmd: 'GetCards'}, { user: ctx.from.id, inline: false});
}
@Command('next')
async onNextCommand(@Ctx() ctx: Context) {
if(ctx.from.id === 11178819) {
this.gameService.emit({ cmd: 'CompleteQueue'}, { user: ctx.from.id })
}
}
@Command('start')
async onCommandStart(@Ctx() ctx: Context) {
await ctx.scene.leave();
this.globalCmd.printCommands(ctx);
}
@On('callback_query')
async onInlineQuery(@Ctx() ctx: Context) {
this.gameService.emit({ cmd: 'ApplyDebuff'}, { ...ctx.callbackQuery, from: ctx.from.id });
this.logger.verbose(`emit callback for ${ctx.callbackQuery}`);
}
@On('text')
async onText(@Message('text') text: string, @Ctx() ctx: Context) {
console.log(text);
//console.log(JSON.stringify(ctx));
if(text.startsWith('/')) {
await ctx.scene.leave();
return;
}
if (text.includes(Messages.EMOJI_CARD)) {
this.gameService.emit({ cmd: 'CardPlayed'}, { text, user: ctx.message.from.id })
return;
}
if (text.includes(Messages.EMOJI_PLAYER)) {
this.gameService.emit({
cmd: 'PLayerSelected'
}, {
text, user: ctx.message.from.id
});
return;
}
this.logger.verbose(`Answer from: ${ctx.message.from.first_name}, validating`);
this.gameService.send(
{ cmd: 'ValidateAnswer'},
{ answer: text, user: ctx.message.from.id, name: ctx.message.from.first_name })
.subscribe(
(res) => {
if(res.valid) {
ctx.replyWithMarkdownV2('Верно', {
reply_markup: {
remove_keyboard: true,
},
});
return;
}
ctx.replyWithMarkdownV2('Ответ неверный', {
reply_markup: {
remove_keyboard: true,
},
});
}
)
}
}

View file

@ -0,0 +1,40 @@
import { REGISTER_NAME_PROMPT_SCENE, REGISTER_PHOTO_SCENE } from "./scenes.const";
import { Command, Ctx, Hears, On, Scene, SceneEnter, SceneLeave } from "nestjs-telegraf";
import {Context} from "../context.interface";
import {Inject, Logger} from "@nestjs/common";
import AppConsts from "../../constants";
import {ClientProxy} from "@nestjs/microservices";
@Scene(REGISTER_NAME_PROMPT_SCENE)
export class RegisterNamePrompt {
private logger = new Logger(RegisterNamePrompt.name);
constructor(@Inject(AppConsts.GameServiceName) private gameService: ClientProxy) {
}
@On('message')
async onMessage(@Ctx() ctx: Context) {
const name = (<any>ctx.message).text;
this.logger.verbose(`Message from ${ctx.message.from.first_name} [Scene register]`);
this.gameService.send(
{ cmd: 'RegisterUser'},
{
name: name,
telegramId: ctx.message.from.id,
chatId: ctx.chat.id
}).subscribe(async (result) => {
this.logger.verbose(`${ctx.message.from.first_name} User registered`);
await ctx.replyWithMarkdownV2('Охуенчик, добро пожаловать\\!', {
reply_markup: {
remove_keyboard: true,
}
});
await ctx.scene.enter(REGISTER_PHOTO_SCENE);
await ctx.reply(`Приятно познакомиться, ${name}!`);
});
}
@SceneLeave()
async sceneLeave(@Ctx() ctx: Context)
{
await ctx.scene.leave();
}
}

View file

@ -0,0 +1,93 @@
import {
Command,
Ctx,
InjectBot,
Message,
On,
Scene,
SceneEnter,
SceneLeave,
} from 'nestjs-telegraf';
import { REGISTER_PHOTO_SCENE } from './scenes.const';
import { Telegraf } from 'telegraf';
import { GlobalCommands } from '../global-commands';
import { Inject, Logger } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { catchError } from 'rxjs';
import {Context} from "../context.interface";
import AppConsts from "../../constants";
@Scene(REGISTER_PHOTO_SCENE)
export class RegisterPhotoScene {
readonly logger = new Logger(RegisterPhotoScene.name);
constructor(
@InjectBot() private bot: Telegraf,
// private guestService: GuestsService,
// private sharedService: SharedService,
private globalCmd: GlobalCommands,
@Inject(AppConsts.PhotoServiceName) private photoQueue: ClientProxy,
@Inject(AppConsts.GameServiceName) private gameService: ClientProxy,
) {}
@SceneEnter()
async onSceneEnter(@Ctx() ctx: Context) {
await ctx.reply('давай, шли свою фотографию сучка');
this.globalCmd.hideKeyboard(ctx);
}
@On('document')
async onDocument(@Ctx() ctx: Context) {
this.logger.warn(
`${ctx.message.from.first_name} tried to use invalid photo`,
);
ctx.replyWithMarkdown(
'[](https://i.ytimg.com/vi/pd342TR6PCM/hqdefault.jpg) Неправильно, попробуй еще раз',
);
}
@On('photo')
async onPhoto(@Message('photo') photo, @Ctx() ctx: Context) {
const link = await this.bot.telegram.getFileLink(
photo[photo.length - 1].file_id,
);
const regexp = new RegExp(/\.(gif|jpe?g|tiff?|png|webp|bmp)$/i);
if (!regexp.test(link.toString())) {
await ctx.reply('не тот формат, давай жипег');
return;
}
await ctx.reply(`Ожидай, наша нейросеточка находит твою мородчку 😼`);
this.photoQueue
.send('parse-photo', {
user: ctx.message.from.id,
url: link.toString(),
})
.pipe(
catchError((val) => {
console.log(val);
return 'Error';
}),
)
.subscribe(async (r) => {
//throw new Error('not implemented');
// TODO: Send to GameService for updating image
// this.sharedService.sendSocketNotificationToAllClients(
// SocketEvents.PHOTOS_UPDATED_EVENT,
// {
// id: ctx.message.from.id,
// },
// );
this.gameService.emit({ cmd: 'PhotoUpdated' }, { id: ctx.message.from.id })
await ctx.reply('Кажись все получилось, чекни на экране что все ок');
await this.onLeaveCommand(ctx);
});
}
@Command('leave')
async onLeaveCommand(@Ctx() ctx: Context) {
await ctx.scene.leave();
}
}

View file

@ -0,0 +1,65 @@
import { REGISTER_NAME_PROMPT_SCENE, REGISTER_PHOTO_SCENE, REGISTER_SCENE_ID } from "./scenes.const";
import {
Command,
Ctx,
Hears,
Scene,
SceneEnter,
SceneLeave,
} from 'nestjs-telegraf';
import { Context } from '../context.interface';
import { Markup } from 'telegraf';
import { Messages } from '../tg.text';
import AppConsts from "../../constants";
import {Inject} from "@nestjs/common";
import {ClientProxy} from "@nestjs/microservices";
@Scene(REGISTER_SCENE_ID)
export class RegisterScene {
constructor(@Inject(AppConsts.GameServiceName) private gameService: ClientProxy) {}
@SceneEnter()
onSceneEnter(@Ctx() ctx: Context) {
const reply = `Шалом-шалом ✋\r\n
Я могу тебя звать ${ctx.message.from.first_name}?
`;
ctx.reply(
reply,
Markup.keyboard([
Markup.button.text(Messages.THATS_ME),
Markup.button.text(Messages.NOT_ME),
]),
);
}
@Hears(Messages.THATS_ME)
async onAgree(@Ctx() ctx: Context) {
this.gameService.send(
{ cmd: 'RegisterUser'},
{
name: ctx.message.from.first_name,
telegramId: ctx.message.from.id,
chatId: ctx.chat.id
}).subscribe(async (result) => {
await ctx.replyWithMarkdownV2('Охуенчик, добро пожаловать\\!', {
reply_markup: {
remove_keyboard: true,
},
});
await ctx.scene.enter(REGISTER_PHOTO_SCENE);
})
}
@Hears(Messages.NOT_ME)
async onDisagree(@Ctx() ctx: Context) {
await ctx.replyWithMarkdownV2('Тогда назови себя', {
reply_markup: {
remove_keyboard: true,
},
});
await ctx.scene.enter(REGISTER_NAME_PROMPT_SCENE);
}
@Command('leave')
async onLeaveCommand(ctx: Context): Promise<void> {
await ctx.scene.leave();
}
}

View file

@ -0,0 +1,4 @@
export const REGISTER_SCENE_ID = 'REGISTER_SCENE';
export const REGISTER_NAME_PROMPT_SCENE = 'REGISTER_NAME_PROMPT_SCENE';
export const QUIZ_SCENE = 'QUIZ_SCENE';
export const REGISTER_PHOTO_SCENE = 'REGISTER_PHOTO_SCENE';

View file

@ -0,0 +1,7 @@
import {ExtraReplyMessage} from "telegraf/typings/telegram-types";
export interface SendMessageInterface {
chatId: number;
message: string;
extra: ExtraReplyMessage | undefined;
}

13
src/bot/tg.text.ts Normal file
View file

@ -0,0 +1,13 @@
export class Messages {
static IM_IN = 'Я в деле 😎';
static THATS_ME = '🥸 Да, енто я';
static NOT_ME = '🙅 ‍Нет, зови меня иначе';
static GO = 'Поехали';
static CHANGE_PHOTO = 'Залить фоточку новую';
static NOTHING_THANKS = 'Не, спасибо, ничего не надо';
static answerNumbers: string[] = ['1. ', '2. ', '3. ', '4. '];
static SELECT_CARD = 'Сыграть карту?';
static EMOJI_CARD = '🃏';
static EMOJI_PLAYER = '👤';
}

4
src/constants.ts Normal file
View file

@ -0,0 +1,4 @@
export default class AppConsts {
static GameServiceName = 'GameService';
static PhotoServiceName = 'tgd_photos';
}

31
src/main.ts Normal file
View file

@ -0,0 +1,31 @@
import {NestFactory} from '@nestjs/core';
import {AppModule} from './app.module';
import {MicroserviceOptions, Transport} from "@nestjs/microservices";
import * as process from "process";
import {Logger} from "@nestjs/common";
import {ConfigService} from "@nestjs/config";
async function bootstrap() {
//const nestApp = await NestFactory.create(AppModule, {
// logger: new Logger(),
//});
//const configService = nestApp.get<ConfigService>(ConfigService);
//const rmq_url = configService.get<string>('RMQ_URL');
//const rmq_outbox_q = configService.get<string>('RMQ_OUTBOX_Q');
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.RMQ,
options: {
urls: [process.env.RMQ_URL],
queue: process.env.RMQ_COMMANDS_OUTPUT_Q,
queueOptions: {
durable: false,
},
}
},
)
await app.listen();
}
bootstrap();

37
src/message.controller.ts Normal file
View file

@ -0,0 +1,37 @@
import {Controller, Inject, Logger} from "@nestjs/common";
import {MessagePattern, Payload} from "@nestjs/microservices";
import {SendMessageInterface} from "./bot/send-message.interface";
import {InjectBot} from "nestjs-telegraf";
import {Telegraf} from "telegraf";
import { BotCommand } from "telegraf/typings/core/types/typegram";
@Controller()
export class MessageController {
private readonly logger = new Logger(MessageController.name);
constructor(@InjectBot() private bot: Telegraf) {
}
@MessagePattern({ cmd: 'SendMessage' } )
async sendMessage(@Payload() data: SendMessageInterface) {
this.logger.verbose(`SendMessage action ${JSON.stringify(data)}`);
await this.bot.telegram.sendMessage(data.chatId, data.message, data.extra);
}
@MessagePattern({ cmd: 'SetCommands'})
async setCommands(@Payload() data: BotCommand[]) {
this.logger.log('update commands enter')
await this.bot.telegram.setMyCommands(data);
console.log(await this.bot.telegram.getMyCommands());
}
@MessagePattern({ cmd: 'ResetCommands'})
async resetCommands(@Payload() data: null) {
this.logger.log('reset commands enter')
await this.bot.telegram.deleteMyCommands();
await this.bot.telegram.setMyCommands([{
command: 'start',
description: 'главное меню'
}]);
console.log(await this.bot.telegram.getMyCommands());
}
}

View file

@ -0,0 +1,3 @@
import { session } from 'telegraf';
export const sessionMiddleware = session();

24
test/app.e2e-spec.ts Normal file
View file

@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

9
test/jest-e2e.json Normal file
View file

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

4
tsconfig.build.json Normal file
View file

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

21
tsconfig.json Normal file
View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}