This commit is contained in:
Kirill Ivlev 2024-10-29 22:38:26 +04:00
commit 5f1b44d3d5
218 changed files with 14595 additions and 0 deletions

6
.dockerignore Normal file
View file

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

24
.eslintrc.js Normal file
View file

@ -0,0 +1,24 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
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*
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
/.env

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/src/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_text.svg" width="320" 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).

13
app.config.ts Normal file
View file

@ -0,0 +1,13 @@
import * as process from "process";
export const TGD_Config = {
//voice: 'oksana',
// telegramApiKey: '5017274145:AAE5435LIoqp4cIgmJRm_j9JWNBazMk59ek',
//telegramApiKey: '5724214514:AAGUCLw5NjWYwpDBM5qjJUxefczGESUvKwY',
telegramApiKey: process.env.TELEGRAM_API_KEY,
enableWorkerService: process.env.ENABLE_WORKER_SERVICE,
workerServiceQueueUrl: process.env.WORKER_SERVICE_QUEUE_URL,
ampqUrl: process.env.AMPQ_URL,
ConfigCoefficientKeyName: 'PrizeCoefficient',
};

52
azure-pipelines.yml Normal file
View file

@ -0,0 +1,52 @@
# Node.js
# Build a general Node.js project with npm.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
trigger:
- master
pool: #Default
vmImage: ubuntu-latest
steps:
- task: NodeTool@0
inputs:
versionSpec: '16.x'
displayName: 'Install Node.js'
- script: |
npm install
npm run build
rm -rf node_modules
displayName: 'npm install and build'
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: './'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
replaceExistingArchive: true
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
ArtifactName: '$(Build.BuildId).zip'
publishLocation: 'Container'
- task: CopyFilesOverSSH@0
inputs:
sshEndpoint: 'NGWEB1'
sourceFolder: '$(Build.ArtifactStagingDirectory)'
contents: '$(Build.BuildId).zip'
targetFolder: '/apps/tgd/service'
cleanTargetFolder: true
readyTimeout: '20000'
#- task: SSH@0
# inputs:
# sshEndpoint: 'NGWEB1'
# runOptions: 'inline'
# inline: |
# unzip -qo /apps/tgd/service/$(Build.BuildId).zip -d /apps/tgd/service/
# rm -f /apps/tgd/service/$(Build.BuildId).zip
# cd /apps/tgd/service && npm install >/dev/null 2>&1
# pm2 restart tgd-service
# readyTimeout: '20000'

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -0,0 +1 @@
[{"weights":[{"name":"dense0/conv0/filters","shape":[3,3,3,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008194216092427571,"min":-0.9423348506291708}},{"name":"dense0/conv0/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006839508168837603,"min":-0.8412595047670252}},{"name":"dense0/conv1/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009194007106855804,"min":-1.2779669878529567}},{"name":"dense0/conv1/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0036026100317637128,"min":-0.3170296827952067}},{"name":"dense0/conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.000740380117706224,"min":-0.06367269012273527}},{"name":"dense0/conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":1,"min":0}},{"name":"dense0/conv2/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":1,"min":0}},{"name":"dense0/conv2/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0037702228508743585,"min":-0.6220867703942692}},{"name":"dense1/conv0/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0033707996209462483,"min":-0.421349952618281}},{"name":"dense1/conv0/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.014611541991140328,"min":-1.8556658328748217}},{"name":"dense1/conv0/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002832523046755323,"min":-0.30307996600281956}},{"name":"dense1/conv1/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006593170586754294,"min":-0.6329443763284123}},{"name":"dense1/conv1/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.012215249211180444,"min":-1.6001976466646382}},{"name":"dense1/conv1/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002384825547536214,"min":-0.3028728445370992}},{"name":"dense1/conv2/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005859645441466687,"min":-0.7617539073906693}},{"name":"dense1/conv2/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013121426806730382,"min":-1.7845140457153321}},{"name":"dense1/conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0032247188044529336,"min":-0.46435950784122243}},{"name":"dense2/conv0/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002659512618008782,"min":-0.32977956463308894}},{"name":"dense2/conv0/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015499923743453681,"min":-1.9839902391620712}},{"name":"dense2/conv0/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0032450980999890497,"min":-0.522460794098237}},{"name":"dense2/conv1/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005911862382701799,"min":-0.792189559282041}},{"name":"dense2/conv1/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021025861478319356,"min":-2.2077154552235325}},{"name":"dense2/conv1/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.00349616945958605,"min":-0.46149436866535865}},{"name":"dense2/conv2/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008104994250278847,"min":-1.013124281284856}},{"name":"dense2/conv2/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.029337059282789044,"min":-3.5791212325002633}},{"name":"dense2/conv2/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0038808938334969913,"min":-0.4230174278511721}},{"name":"fc/weights","shape":[128,136],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.014016061670639936,"min":-1.8921683255363912}},{"name":"fc/bias","shape":[136],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0029505149698724935,"min":0.088760145008564}}],"paths":["face_landmark_68_tiny_model-shard1"]}]

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
models/mtcnn_model-shard1 Normal file

Binary file not shown.

View file

@ -0,0 +1 @@
[{"paths":["mtcnn_model-shard1"],"weights":[{"dtype":"float32","name":"pnet/conv1/weights","shape":[3,3,3,10]},{"dtype":"float32","name":"pnet/conv1/bias","shape":[10]},{"dtype":"float32","name":"pnet/prelu1_alpha","shape":[10]},{"dtype":"float32","name":"pnet/conv2/weights","shape":[3,3,10,16]},{"dtype":"float32","name":"pnet/conv2/bias","shape":[16]},{"dtype":"float32","name":"pnet/prelu2_alpha","shape":[16]},{"dtype":"float32","name":"pnet/conv3/weights","shape":[3,3,16,32]},{"dtype":"float32","name":"pnet/conv3/bias","shape":[32]},{"dtype":"float32","name":"pnet/prelu3_alpha","shape":[32]},{"dtype":"float32","name":"pnet/conv4_1/weights","shape":[1,1,32,2]},{"dtype":"float32","name":"pnet/conv4_1/bias","shape":[2]},{"dtype":"float32","name":"pnet/conv4_2/weights","shape":[1,1,32,4]},{"dtype":"float32","name":"pnet/conv4_2/bias","shape":[4]},{"dtype":"float32","name":"rnet/conv1/weights","shape":[3,3,3,28]},{"dtype":"float32","name":"rnet/conv1/bias","shape":[28]},{"dtype":"float32","name":"rnet/prelu1_alpha","shape":[28]},{"dtype":"float32","name":"rnet/conv2/weights","shape":[3,3,28,48]},{"dtype":"float32","name":"rnet/conv2/bias","shape":[48]},{"dtype":"float32","name":"rnet/prelu2_alpha","shape":[48]},{"dtype":"float32","name":"rnet/conv3/weights","shape":[2,2,48,64]},{"dtype":"float32","name":"rnet/conv3/bias","shape":[64]},{"dtype":"float32","name":"rnet/prelu3_alpha","shape":[64]},{"dtype":"float32","name":"rnet/fc1/weights","shape":[576,128]},{"dtype":"float32","name":"rnet/fc1/bias","shape":[128]},{"dtype":"float32","name":"rnet/prelu4_alpha","shape":[128]},{"dtype":"float32","name":"rnet/fc2_1/weights","shape":[128,2]},{"dtype":"float32","name":"rnet/fc2_1/bias","shape":[2]},{"dtype":"float32","name":"rnet/fc2_2/weights","shape":[128,4]},{"dtype":"float32","name":"rnet/fc2_2/bias","shape":[4]},{"dtype":"float32","name":"onet/conv1/weights","shape":[3,3,3,32]},{"dtype":"float32","name":"onet/conv1/bias","shape":[32]},{"dtype":"float32","name":"onet/prelu1_alpha","shape":[32]},{"dtype":"float32","name":"onet/conv2/weights","shape":[3,3,32,64]},{"dtype":"float32","name":"onet/conv2/bias","shape":[64]},{"dtype":"float32","name":"onet/prelu2_alpha","shape":[64]},{"dtype":"float32","name":"onet/conv3/weights","shape":[3,3,64,64]},{"dtype":"float32","name":"onet/conv3/bias","shape":[64]},{"dtype":"float32","name":"onet/prelu3_alpha","shape":[64]},{"dtype":"float32","name":"onet/conv4/weights","shape":[2,2,64,128]},{"dtype":"float32","name":"onet/conv4/bias","shape":[128]},{"dtype":"float32","name":"onet/prelu4_alpha","shape":[128]},{"dtype":"float32","name":"onet/fc1/weights","shape":[1152,256]},{"dtype":"float32","name":"onet/fc1/bias","shape":[256]},{"dtype":"float32","name":"onet/prelu5_alpha","shape":[256]},{"dtype":"float32","name":"onet/fc2_1/weights","shape":[256,2]},{"dtype":"float32","name":"onet/fc2_1/bias","shape":[2]},{"dtype":"float32","name":"onet/fc2_2/weights","shape":[256,4]},{"dtype":"float32","name":"onet/fc2_2/bias","shape":[4]},{"dtype":"float32","name":"onet/fc2_3/weights","shape":[256,10]},{"dtype":"float32","name":"onet/fc2_3/bias","shape":[10]}]}]

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -0,0 +1 @@
[{"weights":[{"name":"conv0/filters","shape":[3,3,3,16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009007044399485869,"min":-1.2069439495311063}},{"name":"conv0/bias","shape":[16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005263455241334205,"min":-0.9211046672334858}},{"name":"conv1/depthwise_filter","shape":[3,3,16,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004001977630690033,"min":-0.5042491814669441}},{"name":"conv1/pointwise_filter","shape":[1,1,16,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013836609615999109,"min":-1.411334180831909}},{"name":"conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0015159862590771096,"min":-0.30926119685173037}},{"name":"conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002666276225856706,"min":-0.317286870876948}},{"name":"conv2/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015265831292844286,"min":-1.6792414422128714}},{"name":"conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0020280554598453,"min":-0.37113414915168985}},{"name":"conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006100742489683862,"min":-0.8907084034938438}},{"name":"conv3/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016276211832083907,"min":-2.0508026908425725}},{"name":"conv3/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003394414279975143,"min":-0.7637432129944072}},{"name":"conv4/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006716050119961009,"min":-0.8059260143953211}},{"name":"conv4/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021875603993733724,"min":-2.8875797271728514}},{"name":"conv4/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0041141652009066415,"min":-0.8187188749804216}},{"name":"conv5/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008423839597141042,"min":-0.9013508368940915}},{"name":"conv5/pointwise_filter","shape":[1,1,256,512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.030007277283014035,"min":-3.8709387695088107}},{"name":"conv5/bias","shape":[512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008402082966823203,"min":-1.4871686851277068}},{"name":"conv8/filters","shape":[1,1,512,25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.028336129469030042,"min":-4.675461362389957}},{"name":"conv8/bias","shape":[25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002268134028303857,"min":-0.41053225912299807}}],"paths":["tiny_face_detector_model-shard1"]}]

4
nest-cli.json Normal file
View file

@ -0,0 +1,4 @@
{
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

9646
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

93
package.json Normal file
View file

@ -0,0 +1,93 @@
{
"name": "tgd-service",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"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/src/main.js",
"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/axios": "3.0.1",
"@nestjs/cli": "^10.2.1",
"@nestjs/common": "^10.2.8",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.8",
"@nestjs/cqrs": "^10.2.6",
"@nestjs/microservices": "^10.2.8",
"@nestjs/mongoose": "^10.0.2",
"@nestjs/platform-express": "^10.2.8",
"@nestjs/platform-socket.io": "^10.2.8",
"@nestjs/schedule": "^4.0.0",
"@nestjs/websockets": "^10.2.8",
"@socket.io/mongo-adapter": "^0.3.0",
"@socket.io/mongo-emitter": "^0.2.0",
"amqp-connection-manager": "^4.1.14",
"amqplib": "^0.10.3",
"axios": "^1.6.1",
"class-transformer": "^0.5.1",
"cyrillic-to-translit-js": "^3.2.1",
"dotenv": "^16.3.1",
"latin-to-cyrillic": "^1.0.1",
"mongodb": "^6.2.0",
"mongoose": "^8.0.0",
"nestjs-telegraf": "^2.7.0",
"openai": "^4.19.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^5.0.5",
"rxjs": "^7.8.1",
"socket.io": "^4.7.2"
},
"devDependencies": {
"@nestjs/schematics": "^10.0.3",
"@nestjs/testing": "^10.2.8",
"@types/cron": "^2.0.1",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.8",
"@types/node": "^20.9.0",
"@types/supertest": "^2.0.16",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"supertest": "^6.3.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.5.0",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.2.2"
},
"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,12 @@
export class CommandsConsts {
static SendMessage = 'SendMessage';
static ValidateAnswer = 'ValidateAnswer';
static CardPlayed = 'CardPlayed';
static PhotoUpdated = 'PhotoUpdated';
static RegisterUser = 'RegisterUser';
static SetCommands = 'SetCommands';
static ResetCommands = 'ResetCommands';
static GetCards = 'GetCards';
static ApplyDebuff = 'ApplyDebuff';
static CompleteQueue = 'CompleteQueue';
}

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

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

@ -0,0 +1,48 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';
import { GuestsModule } from './guests/guests.module';
import { QuizModule } from './quiz/quiz.module';
import { SocketGateway } from './socket/socket.gateway';
import { SharedModule } from './shared/shared.module';
import { StateModule } from './state/state.module';
import { ScheduleModule } from '@nestjs/schedule';
import { SchedulerService } from './scheduler/scheduler.service';
import { GameModule } from './game/game.module';
import { CqrsModule } from '@nestjs/cqrs';
import { SocketHandlersModule } from './socket/socket-handlers/socket-handlers.module';
import { CardsModule } from './cards/cards.module';
import { PenaltyModule } from './penalty/penalty.module';
import { VoiceModule } from './voice/voice.module';
import { GiftsModule } from './gifts/gifts.module';
import {ConfigModule} from "@nestjs/config";
import {MessagingModule} from "./messaging/messaging.module";
import * as process from "process";
import {OpenaiModule} from "./openai/openai.module";
@Module({
imports: [
ConfigModule.forRoot(),
MessagingModule,
MongooseModule.forRoot(process.env.MONGO_URL),
MessagingModule,
GuestsModule,
QuizModule,
SharedModule,
ScheduleModule.forRoot(),
StateModule,
GameModule,
CqrsModule,
SocketHandlersModule,
CardsModule,
PenaltyModule,
VoiceModule,
GiftsModule,
OpenaiModule
],
controllers: [AppController],
providers: [AppService, SocketGateway, SchedulerService],
exports: [AppService, SocketGateway],
})
export class AppModule {}

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

@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
constructor() {
}
getHello(): string {
return 'Hello World!';
}
}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CardsController } from './cards.controller';
describe('CardsController', () => {
let controller: CardsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CardsController],
}).compile();
controller = module.get<CardsController>(CardsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View file

@ -0,0 +1,13 @@
import { Controller, Get, Param } from '@nestjs/common';
import { CardsService } from './cards.service';
@Controller('cards')
export class CardsController {
constructor(private cardsService: CardsService) {
}
@Get(':id')
async getUserCards(@Param('id') id: number) {
return this.cardsService.getCards(id);
}
}

29
src/cards/cards.module.ts Normal file
View file

@ -0,0 +1,29 @@
import { Global, Module } from '@nestjs/common';
import { CardsService } from './cards.service';
import { CardsController } from './cards.controller';
import { DealCardsCmdHandler } from './command-handlers/deal-cards.cmd.handler';
import { MongooseModule } from '@nestjs/mongoose';
import { Card, CardSchema } from '../schemas/cards.schema';
import { CqrsModule } from '@nestjs/cqrs';
import { StateModule } from '../state/state.module';
import { CardsUserRegisteredEventHandler } from './event-handlers/cards-user-registered.event.handler';
import {ConfigService} from "@nestjs/config";
import {NextQuestionEventHandler} from "./event-handlers/next-question-event.handler";
@Global()
@Module({
imports: [
MongooseModule.forFeature([{ name: Card.name, schema: CardSchema }]),
CqrsModule,
StateModule,
],
providers: [
CardsService,
DealCardsCmdHandler,
CardsUserRegisteredEventHandler,
NextQuestionEventHandler,
ConfigService
],
controllers: [CardsController],
exports: [CardsService]
})
export class CardsModule {}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CardsService } from './cards.service';
describe('CardsService', () => {
let service: CardsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CardsService],
}).compile();
service = module.get<CardsService>(CardsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

129
src/cards/cards.service.ts Normal file
View file

@ -0,0 +1,129 @@
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Card, CardDocument } from '../schemas/cards.schema';
import { Model } from 'mongoose';
import { gameCards, GameCard } from '../game/entities/cards.entities';
import { EventBus } from '@nestjs/cqrs';
import { CardsDealedEvent } from '../game/events/cards-dealed.event';
import { QuizAnswerStateEnum } from '../game/entities/quiz-answer-state.enum';
import { CardsSetChangedEvent } from '../game/events/cards-events/cards-set-changed.event';
import {ConfigService} from "@nestjs/config";
@Injectable()
export class CardsService {
private readonly logger = new Logger(CardsService.name);
private playerCards = [];
constructor(
@InjectModel(Card.name) private cardModel: Model<CardDocument>,
private configService: ConfigService,
private eventBus: EventBus,
) {
for (const card of gameCards) {
const nCard = new card();
this.playerCards.push(nCard);
}
}
getCards(telegramId: number) {
return this.cardModel.find({ telegramId, used: false }).exec();
}
calculateTotal(playerCards: GameCard[]): number {
let total = 0;
for (let i = 0; i < playerCards.length; ++i) {
total += playerCards[i].chance;
}
return total;
}
getRandomCard(totalWeigh: number, playerCards: GameCard[]) {
const threshold = Math.random() * totalWeigh;
let total = 0;
for (let i = 0; i < playerCards.length; i++) {
total += playerCards[i].chance;
if (total >= threshold) {
return playerCards[i];
}
}
}
async markCardAsUsed(telegramId: number, card: GameCard) {
this.logger.log(`checking that card is still applicable `);
const cardItem = await this.cardModel
.findOne({ telegramId, cardType: card.name, used: false })
.exec();
if (!cardItem) {
this.logger.log(`card already being used`);
throw new InternalServerErrorException('Card already used');
}
cardItem.used = true;
await cardItem.save();
this.logger.log(`Card ${card.name} is marked as used for ${telegramId}`);
return cardItem;
}
async withdrawNewCard(telegramId: number) {
this.logger.verbose(`generating card for player ${telegramId}`);
const card = this.getRandomCard(
this.calculateTotal(this.playerCards),
this.playerCards,
);
this.logger.verbose(card);
const cardItem = new this.cardModel({
telegramId,
cardName: card.description,
cardType: card.name,
emoji: card.getEmoji(),
mightBePlayed: card.mightBePlayed,
});
await cardItem.save();
this.eventBus.publish(new CardsSetChangedEvent(telegramId));
return card;
}
async dealStartCards(telegramId: number) {
const cardsOnHand = this.configService.get<number>('CARDS_ON_HAND');
const cards = await this.cardModel.find({ telegramId }).exec();
for(const card of cards) {
this.logger.verbose(
`Deleting card ${card.cardType} for player ${telegramId}`,
);
await card.deleteOne();
}
const starterCard = this.playerCards.filter((x) => x.dealOnStart === true);
const rndCards: GameCard[] = [...starterCard];
const weight = this.calculateTotal(this.playerCards);
for (let i = rndCards.length; i < cardsOnHand; i++) {
this.logger.verbose(`random card for ${telegramId}`);
rndCards.push(this.getRandomCard(weight, this.playerCards));
}
this.logger.verbose(`Dealed for ${telegramId}, ${rndCards.map((x) => x.name).join(',')}`);
for (const rcard of rndCards) {
const dbCard = new this.cardModel({
telegramId,
cardName: rcard.description,
cardType: rcard.name,
emoji: rcard.getEmoji(),
mightBePlayed: rcard.mightBePlayed,
});
await dbCard.save();
}
this.eventBus.publish(
new CardsDealedEvent(
telegramId,
rndCards.map((c) => c.description),
rndCards,
),
);
}
async getCardsForState(
telegramId: number,
quizState: QuizAnswerStateEnum,
) {
this.logger.verbose(`Finding cards for ${telegramId}, ${quizState}`);
return await this.cardModel.find({ telegramId, mightBePlayed: quizState, used: false }).exec();
}
}

View file

@ -0,0 +1,14 @@
import {CommandBus, CommandHandler, EventBus, ICommandHandler} from "@nestjs/cqrs";
import {BanPlayerCommand} from "../commands/ban-player.command";
import {Promise} from "mongoose";
import {SetGuestPropertyCommand} from "../../guests/command/set-guest-property.command";
@CommandHandler(BanPlayerCommand)
export class BanPlayerCommandHandler implements ICommandHandler<BanPlayerCommand> {
constructor(private eventBus: EventBus, private commandBus: CommandBus) {
}
async execute(command: BanPlayerCommand): Promise<any> {
await this.commandBus.execute(new SetGuestPropertyCommand(command.user, 'bannedFor', '2'));
return Promise.resolve(true);
}
}

View file

@ -0,0 +1,15 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { DealCardsCommand } from '../../game/commands/deal-cards.command';
import { CardsService } from '../cards.service';
import { Logger } from '@nestjs/common';
@CommandHandler(DealCardsCommand)
export class DealCardsCmdHandler implements ICommandHandler<DealCardsCommand> {
private readonly logger = new Logger(DealCardsCmdHandler.name);
constructor(private cardsService: CardsService) {
}
execute(command: DealCardsCommand): Promise<any> {
this.logger.verbose(`Dealing cards for user ${command.telegramId}`);
return this.cardsService.dealStartCards(command.telegramId);
}
}

View file

@ -0,0 +1,4 @@
export class BanPlayerCommand {
constructor(public user: number, public delay: number) {
}
}

View file

@ -0,0 +1 @@
export class UnbanPlayerCommand {}

View file

@ -0,0 +1,45 @@
import {CommandBus, EventsHandler, IEventHandler, QueryBus} from '@nestjs/cqrs';
import {UserRegisteredEvent} from '../../game/events/user-registered.event';
import {StateService} from '../../state/state.service';
import {GameStateEnum} from '../../state/game-state.enum';
import {CardsService} from '../cards.service';
import {Logger} from '@nestjs/common';
import {ChatgptRequestQuery} from "../../openai/commands/chatgpt-request.command";
import {Case, NameCasesHelper} from "../../openai/helpers/name-cases.helper";
import {SetGuestPropertyCommand} from "../../guests/command/set-guest-property.command";
import * as translit from 'latin-to-cyrillic';
import {GuestPropertiesConsts} from "../../schemas/properties.consts";
import {GetGuestPropertyQuery} from "../../guests/command/get-guest-property.handler";
@EventsHandler(UserRegisteredEvent)
export class CardsUserRegisteredEventHandler implements IEventHandler<UserRegisteredEvent> {
private readonly logger = new Logger(CardsUserRegisteredEventHandler.name);
constructor(
private stateService: StateService,
private cardsService: CardsService,
private queryBus: QueryBus,
private commandBus: CommandBus,
) {}
async handle(event: UserRegisteredEvent): Promise<any> {
const state = await this.stateService.getState('main');
if(event.name !=='Elene') {
const asyncGenitive = this.queryBus.execute(new ChatgptRequestQuery(NameCasesHelper.getRequestForNameInCase(translit(event.name), Case.GenitiveCase)));
const asyncAccusative = this.queryBus.execute(new ChatgptRequestQuery(NameCasesHelper.getRequestForNameInCase(translit(event.name), Case.AccusativeCase)));
const asyncSubjective = this.queryBus.execute(new ChatgptRequestQuery(NameCasesHelper.getRequestForNameInCase(translit(event.name), Case.SubjectiveCase)));
await this.commandBus.execute(new SetGuestPropertyCommand(event.telegramId, GuestPropertiesConsts.NameGenitiveCase, (await asyncGenitive).message.content));
await this.commandBus.execute(new SetGuestPropertyCommand(event.telegramId, GuestPropertiesConsts.NameAccusativeCase,(await asyncAccusative).message.content));
await this.commandBus.execute(new SetGuestPropertyCommand(event.telegramId, GuestPropertiesConsts.NameSubjectiveCase,(await asyncSubjective).message.content));
} else {
await this.commandBus.execute(new SetGuestPropertyCommand(event.telegramId, GuestPropertiesConsts.NameGenitiveCase, 'Элене'));
await this.commandBus.execute(new SetGuestPropertyCommand(event.telegramId, GuestPropertiesConsts.NameAccusativeCase,'Элене'));
await this.commandBus.execute(new SetGuestPropertyCommand(event.telegramId, GuestPropertiesConsts.NameSubjectiveCase,'Элене'));
}
this.logger.verbose(`Handle user reg event, game state is: ${state.value}`);
if (state.value === GameStateEnum.onboarding) {
this.logger.verbose(`Calling cardService to deal startup cards for ${event.telegramId}`);
return await this.cardsService.dealStartCards(event.telegramId);
}
}
}

View file

@ -0,0 +1,15 @@
import { EventBus, EventsHandler, IEventHandler} from "@nestjs/cqrs";
import {Logger} from "@nestjs/common";
import {NextQuestionEvent} from "../../game/events/next-question.event";
@EventsHandler(NextQuestionEvent)
export class NextQuestionEventHandler implements IEventHandler<NextQuestionEvent> {
private readonly logger = new Logger(NextQuestionEventHandler.name);
constructor(private eventbus: EventBus) {
}
handle(event: NextQuestionEvent): any {
this.logger.verbose(`handle next question from cards`);
}
}

View file

@ -0,0 +1,3 @@
export class BanUserEvent {
}

4
src/context.interface.ts Normal file
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,17 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreateNewQueueItemCommand } from '../commands/create-new-queue-item.command';
import { GameService } from '../game.service';
@CommandHandler(CreateNewQueueItemCommand)
export class CreateNewQueueItemCommandHandler implements ICommandHandler<CreateNewQueueItemCommand> {
constructor(
private gameService: GameService,
) {
}
async execute(command: CreateNewQueueItemCommand): Promise<any> {
await this.gameService.addTaskToGameQueue(command.target, command.type, command.text);
return Promise.resolve(undefined);
}
}

View file

@ -0,0 +1,12 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { GiveCardsCommand } from '../commands/give-cards.command';
import { GuestsService } from '../../guests/guests.service';
@CommandHandler(GiveCardsCommand)
export class GiveCardsCommandHandler
implements ICommandHandler<GiveCardsCommand>
{
constructor(private guestService: GuestsService) {}
async execute(command: GiveCardsCommand) {
await this.guestService.giveCards();
}
}

View file

@ -0,0 +1,23 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { GiveOutAPrizeCommand } from '../commands/give-out-a-prize.command';
import { GameService } from '../game.service';
import { Logger } from '@nestjs/common';
import { GameQueueTypes } from '../../schemas/game-queue.schema';
@CommandHandler(GiveOutAPrizeCommand)
export class GameGiveOutAPrizeCommandHandler
implements ICommandHandler<GiveOutAPrizeCommand> {
private readonly logger = new Logger(GameGiveOutAPrizeCommandHandler.name);
constructor(private gameService: GameService) {
}
async execute(command: GiveOutAPrizeCommand): Promise<any> {
this.logger.verbose(`Player winning a prize ${command.telegramId}`);
return this.gameService.addTaskToGameQueue(
command.telegramId,
GameQueueTypes.giveOutAPrize,
);
}
}

View file

@ -0,0 +1,63 @@
import { CommandBus, CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
import { ProceedGameQueueCommand } from '../commands/proceed-game-queue.command';
import { GameService } from '../game.service';
import { NextQuestionCommand } from '../commands/next-question.command';
import { SharedService } from '../../shared/shared.service';
import { SocketEvents } from '../../shared/events.consts';
import { Logger } from '@nestjs/common';
import { GameQueueTypes } from '../../schemas/game-queue.schema';
import { QuizAnswerStateChangedEvent } from '../events/quiz-answer-state-changed.event';
import { QuizAnswerStateEnum } from '../entities/quiz-answer-state.enum';
@CommandHandler(ProceedGameQueueCommand)
export class GameProceedGameQueueCommandHandler
implements ICommandHandler<ProceedGameQueueCommand> {
private readonly logger = new Logger(GameProceedGameQueueCommandHandler.name);
constructor(
private gameService: GameService,
private cmdBus: CommandBus,
private sharedService: SharedService,
private eventBus: EventBus,
) {}
async execute(command: ProceedGameQueueCommand) {
this.logger.verbose(`Proceed with game queue...`);
const item = await this.gameService.getGameQueueItem();
if (!item) {
return this.cmdBus.execute(new NextQuestionCommand());
}
this.sharedService.sendSocketNotificationToAllClients(
SocketEvents.GameQueueItem,
{
_id: item.id,
completed: item.completed,
target: item.target,
type: item.type,
text: item.text
},
);
switch (item.type) {
case GameQueueTypes.giveOutAPrize:
this.eventBus.publish(
new QuizAnswerStateChangedEvent(
QuizAnswerStateEnum.playerGettingReward,
item.target,
),
);
break;
case GameQueueTypes.penalty:
this.eventBus.publish(
new QuizAnswerStateChangedEvent(
QuizAnswerStateEnum.playerGotPenalty,
item.target,
),
);
break;
case GameQueueTypes.playExtraCard:
this.eventBus.publish(
new QuizAnswerStateChangedEvent(
QuizAnswerStateEnum.betweenRounds, item.target
)
)
}
}
}

View file

@ -0,0 +1,51 @@
import {CommandHandler, ICommandHandler} from "@nestjs/cqrs";
import {SelectTargetPlayerCommand} from "../commands/select-target-player.command";
import {Inject, Logger} from "@nestjs/common";
import {ClientProxy} from "@nestjs/microservices";
import {MqtMessageModel} from "../../messaging/models/mqt-message.model";
import {ChatMessageRequestModel} from "../../messaging/models/chat-message-request.model";
import {CommandsConsts} from "../../Consts/commands.consts";
import {GuestsService} from "../../guests/guests.service";
import {Messages} from "../../messaging/tg.text";
//import {Promise} from "mongoose";
@CommandHandler(SelectTargetPlayerCommand)
export class SelectTargetPlayerHandler implements ICommandHandler<SelectTargetPlayerCommand> {
private readonly logger = new Logger(SelectTargetPlayerHandler.name);
constructor(@Inject('Telegram') private telegramService: ClientProxy,
private guestService: GuestsService,
) {
}
async execute(command: SelectTargetPlayerCommand): Promise<any> {
this.logger.verbose('enter');
//const user = await this.guestService.findById(command.player);
const allUsers = await this.guestService.findAll();
const user = allUsers.find(x => x.telegramId === command.player);
if(!user) {
throw new Error(`Cant find current user ${command.player}`);
}
const buttons = allUsers.map((x) => {
return [{
text: `${Messages.EMOJI_PLAYER} ${x.name}`,
callback_data: `{ "card": "${command.debuffName}", "value": "${command.value}", "user": "${x.telegramId}" }`
}]
});
this.telegramService.send<MqtMessageModel,ChatMessageRequestModel>(
{ cmd: CommandsConsts.SendMessage},
{
chatId: user.chatId,
message: 'Выберите игрока',
extra: {
reply_markup: {
inline_keyboard: [
...buttons
]
}
}
}
).subscribe((r) => {
this.logger.verbose(`Reply from tg service: ${r}`);
})
}
}

View file

@ -0,0 +1,4 @@
export class CardSelectionTimeExceedCommand {
constructor() {
}
}

View file

@ -0,0 +1,6 @@
import { GameQueueTypes } from '../../schemas/game-queue.schema';
export class CreateNewQueueItemCommand {
constructor(public target: number, public type: GameQueueTypes, public text: string| null = null) {
}
}

View file

@ -0,0 +1,4 @@
export class DealCardsCommand {
constructor(public telegramId: number) {
}
}

View file

@ -0,0 +1,4 @@
export class GiveCardsCommand {
constructor() {
}
}

View file

@ -0,0 +1,4 @@
export class GiveOutAPrizeCommand {
constructor(public telegramId: number) {
}
}

View file

@ -0,0 +1,4 @@
export class HideKeyboardCommand {
constructor(public readonly message: string) {
}
}

View file

@ -0,0 +1,4 @@
export class IncreasePlayerWinningRateCommand {
constructor(public tId: number, public rate: number) {
}
}

View file

@ -0,0 +1 @@
export class MarkQuestionsAsUnansweredCommand {}

View file

@ -0,0 +1 @@
export class NextQuestionCommand {}

View file

@ -0,0 +1,6 @@
import { GameCard } from '../entities/cards.entities';
export class NotifyCardOnScreenCommand {
constructor(public telegramId: number, public card: GameCard, public timeToShow: number = 5000) {
}
}

View file

@ -0,0 +1,8 @@
export class PostCardsToUserCommand {
constructor(
public telegramId: number,
public chatId: number,
public cards: string[],
public inline: boolean = false,
) {}
}

View file

@ -0,0 +1 @@
export class ProceedGameQueueCommand {}

View file

@ -0,0 +1,6 @@
import { GameCard } from '../entities/cards.entities';
export class RemoveCardFromUserCommand {
constructor(public telegramId: number, public card: GameCard) {
}
}

View file

@ -0,0 +1,4 @@
export class ResetPlayerPrizeChanceCommand {
constructor(public telegramId: number) {
}
}

View file

@ -0,0 +1,4 @@
export class SelectTargetPlayerCommand {
constructor(public player,public debuffName: string, public value: string|number) {
}
}

View file

@ -0,0 +1,4 @@
export class SendToastCommand {
constructor(public text: string, public timeout: number) {
}
}

View file

@ -0,0 +1,232 @@
import { QuizAnswerStateEnum } from './quiz-answer-state.enum';
import {CommandBus, EventBus, ofType, QueryBus} from '@nestjs/cqrs';
import {Injectable, Logger} from '@nestjs/common';
import { IncreasePlayerWinningRateCommand } from '../commands/increase-player-winning-rate.command';
import { CreateNewQueueItemCommand } from '../commands/create-new-queue-item.command';
import { GameQueueTypes } from '../../schemas/game-queue.schema';
import { NotifyCardOnScreenCommand } from '../commands/notify-card-on-screen-command';
import { SendToastCommand } from '../commands/send-toast.command';
import { getRandomInt } from '../../helpers/rand-number';
import { GiveOutAPrizeCommand } from '../commands/give-out-a-prize.command';
import {SelectTargetPlayerCommand} from "../commands/select-target-player.command";
import {DebuffsConsts} from "./debuffs.consts";
import {NextQuestionEvent} from "../events/next-question.event";
import {FilterGuestsWithPropertyQuery} from "../../guests/queries/filterguestswithproperty.query";
import {DebuffCardPlayedEvent} from "../events/debuff-card-played.event";
import {filter} from "rxjs";
import {SetGuestPropertyCommand} from "../../guests/command/set-guest-property.command";
import {StringHelper} from "../../helpers/stringhelper";
import {GetGuestQuery} from "../../guests/queries/getguest.query";
import {CardsSetChangedEvent} from "../events/cards-events/cards-set-changed.event";
import {GetGuestPropertyQuery} from "../../guests/command/get-guest-property.handler";
import {GuestPropertiesConsts} from "../../schemas/properties.consts";
export interface IGameCard {
setupHandlers(eventBus: EventBus, commandBus: CommandBus, queryBus: QueryBus): void;
}
export class GameCard implements IGameCard{
protected readonly logger = new Logger(GameCard.name);
protected commandBus: CommandBus;
protected eventBus: EventBus;
protected queryBus: QueryBus;
protected telegramId: number;
protected destinationTelegramId: number;
public handlers: { event: any, commands: any[]}[] = [];
name: string;
description: string;
protected emoji: string;
public dealOnStart = false;
chance = 0;
getEmoji() {
return this.emoji;
}
setup(eb: EventBus, cb: CommandBus, qb: QueryBus, tId: number, destTId: number) {
this.eventBus = eb;
this.commandBus = cb;
this.telegramId = tId;
this.destinationTelegramId = destTId;
this.queryBus = qb;
}
async handle() {
this.logger.verbose(`Card has no handler`);
}
mightBePlayed: QuizAnswerStateEnum;
setupHandlers(eventBus: EventBus, commandBus: CommandBus, queryBus: QueryBus) {
}
}
export class DoubleTreasureCard extends GameCard {
name = DoubleTreasureCard.name;
description = 'Удвоить приз';
chance = 4;
emoji = '💰';
mightBePlayed = QuizAnswerStateEnum.playerGettingReward;
async handle(): Promise<void> {
await this.commandBus.execute(
new NotifyCardOnScreenCommand(this.telegramId, this),
);
await this.commandBus.execute(
new GiveOutAPrizeCommand(this.telegramId),
);
const userSrc = await this.queryBus.execute(new GetGuestQuery(this.telegramId));;
const subjcaseFrom = userSrc.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameSubjectiveCase));
const message = `${subjcaseFrom} решает удвоить приз!`;
await this.commandBus.execute(new SendToastCommand(message, 8000));
}
}
export class StolePrizeCard extends GameCard {
name = StolePrizeCard.name;
description = 'Украсть приз';
chance = 15;
emoji = '🥷🏽';
mightBePlayed = QuizAnswerStateEnum.someoneGettingReward;
async handle(): Promise<void> {
this.logger.verbose(`StolePrize from player ${this.telegramId} to player ${this.destinationTelegramId}`);
await this.commandBus.execute(
new NotifyCardOnScreenCommand(this.telegramId, this),
);
const userSrc = await this.queryBus.execute(new GetGuestQuery(this.telegramId));
const userDst = await this.queryBus.execute(new GetGuestQuery(this.destinationTelegramId));
const subjcaseFrom = userSrc.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameSubjectiveCase));
const genitiveTo = userDst.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameGenitiveCase));
const message = `${subjcaseFrom} крадет приз у ${genitiveTo}`;
await this.commandBus.execute(new SendToastCommand(message, 8000));
}
}
export class ShitCard extends GameCard {
name = ShitCard.name;
description = 'Говнокарта';
chance = 50;
emoji = '💩';
mightBePlayed = QuizAnswerStateEnum.someoneAnsweredCorrectly;
async handle(): Promise<void> {
this.logger.verbose(`Shit card from player ${this.telegramId} to player ${this.destinationTelegramId}`);
await this.commandBus.execute(
new NotifyCardOnScreenCommand(this.telegramId, this),
);
await this.commandBus.execute(
new CreateNewQueueItemCommand(
this.destinationTelegramId,
GameQueueTypes.additionalQuestion,
),
);
const userSrc = await this.queryBus.execute(new GetGuestQuery(this.telegramId));
const userDst = await this.queryBus.execute(new GetGuestQuery(this.destinationTelegramId));
const subjcaseFrom = userSrc.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameSubjectiveCase));
const accusativeTo = userDst.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameAccusativeCase));
const message = `${subjcaseFrom} кидает говно-карту в ${accusativeTo}`;
await this.commandBus.execute(new SendToastCommand(message, 8000));
}
}
export class LuckyCard extends GameCard {
name = LuckyCard.name;
description = 'Лаки-карта';
chance = 32;
mightBePlayed = QuizAnswerStateEnum.playerAnsweredCorrectly;
emoji = '🍀';
async handle() {
this.logger.verbose(`Handling card`);
const user = await this.queryBus.execute(new GetGuestQuery(this.telegramId));
const subjcase = user.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameSubjectiveCase));
const message = `${subjcase} использует лаки карту`;
this.logger.verbose(`message: ${message}`);
await this.commandBus.execute(new SendToastCommand(message, 8000));
await this.commandBus.execute(
new NotifyCardOnScreenCommand(this.telegramId, this),
);
await this.commandBus.execute(
new IncreasePlayerWinningRateCommand(
this.telegramId,
getRandomInt(8, 20),
),
);
}
}
export class AvoidPenaltyCard extends GameCard {
dealOnStart = true;
name = AvoidPenaltyCard.name;
description = 'Избежать наказание';
chance = 25;
emoji = '🏃‍';
mightBePlayed = QuizAnswerStateEnum.playerGotPenalty;
async handle() {
const user = await this.queryBus.execute(new GetGuestQuery(this.telegramId));
const subjcase = user.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameSubjectiveCase));
const message = `${subjcase} избегает наказания`;
this.logger.verbose(`message: ${message}`);
await this.commandBus.execute(new SendToastCommand(message, 8000));
await this.commandBus.execute(
new NotifyCardOnScreenCommand(this.telegramId, this),
);
}
}
@Injectable()
export class BanPlayer extends GameCard {
dealOnStart = true;
name = BanPlayer.name;
description = 'Забанить игрока';
chance = 10;
emoji = '🚷';
mightBePlayed = QuizAnswerStateEnum.betweenRounds;
async setupHandlers(eventBus: EventBus, commandBus: CommandBus, queryBus: QueryBus) {
super.setupHandlers(eventBus, commandBus, queryBus);
eventBus.pipe(ofType(DebuffCardPlayedEvent),filter(x => x.debufName == DebuffsConsts.bannedFor)).subscribe(async (r) => {
const destUser = await queryBus.execute(new GetGuestQuery(r.dest))
const sourceUser = await queryBus.execute(new GetGuestQuery(r.from));
const subjectiveSource = sourceUser.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameSubjectiveCase));
const accusativeDest = destUser.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameAccusativeCase));
const message = `Игрок ${subjectiveSource} забанил ${accusativeDest} `;
await commandBus.execute(new SetGuestPropertyCommand(r.dest, DebuffsConsts.bannedFor, r.data, true));
await commandBus.execute(new SendToastCommand(message, 8000));
await commandBus.execute(new NotifyCardOnScreenCommand(sourceUser.telegramId, this,0 ));
eventBus.publish(new CardsSetChangedEvent(sourceUser.telegramId));
})
eventBus.pipe(ofType(NextQuestionEvent)).subscribe(async (r)=> {
this.logger.verbose(`next event`);
const players = await queryBus.execute(new FilterGuestsWithPropertyQuery(DebuffsConsts.bannedFor, '$gt', 0))
this.logger.verbose(`enter: ban card handler, banned players count ${players.length}`);
players.map(async (player) => {
const newBannedFor = parseInt(player.get(StringHelper.getPropertyName(DebuffsConsts.bannedFor))) - 1;
await commandBus.execute(
new SetGuestPropertyCommand(
player.telegramId, DebuffsConsts.bannedFor,
newBannedFor.toString()
));
});
})
}
async handle() {
await this.commandBus.execute(
new SelectTargetPlayerCommand(this.telegramId, DebuffsConsts.bannedFor, 2)
)
await this.queryBus.execute(new FilterGuestsWithPropertyQuery(null,null,null));
this.eventBus.subscribe((data) =>{
this.logger.verbose(`Response from cmdBus: ${data}`);
});
}
}
export const gameCards: typeof GameCard[] = [
DoubleTreasureCard,
StolePrizeCard,
ShitCard,
LuckyCard,
AvoidPenaltyCard,
BanPlayer
];

View file

@ -0,0 +1,3 @@
export class DebuffsConsts {
static bannedFor = 'bannedFor';
}

View file

@ -0,0 +1,10 @@
export enum QuizAnswerStateEnum {
playerAnsweredCorrectly = 'player_answered_correctly',
playerGettingReward = 'player_getting_reward',
playerAnsweredWrong = 'player_answered_wrong',
someoneAnsweredCorrectly = 'someone_answered_correctly',
someoneGettingReward = 'someone_getting_reward',
someoneGettingPenalty = 'someone_getting_penalty',
playerGotPenalty = 'player_got_penalty',
betweenRounds = 'play_extra_card',
}

View file

@ -0,0 +1,4 @@
export enum QuizStateEnum {
waitingForAnswer = 'waiting_for_answer',
awaitingPlayerActions = 'awaiting_for_player_actions',
}

View file

@ -0,0 +1,13 @@
import { CommandBus, EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { CardSelectionPendingEvent } from '../events/card-selection-pending.event';
import { GameService } from '../game.service';
@EventsHandler(CardSelectionPendingEvent)
export class CardSelectionPendingEventHandler implements IEventHandler<CardSelectionPendingEvent> {
constructor(private gameService: GameService) {}
handle(event: CardSelectionPendingEvent): any {
this.gameService.beginCardSelectionScene();
}
}

View file

@ -0,0 +1,51 @@
import {
CommandBus,
EventBus,
EventsHandler,
IEventHandler, QueryBus,
} from '@nestjs/cqrs';
import { PlayerCardSelectedEvent } from '../events/player-card-selected.event';
import { Logger } from '@nestjs/common';
import { GameService } from '../game.service';
import { QuizService } from '../../quiz/quiz.service';
import { CardsService } from '../../cards/cards.service';
import { RemoveCardFromUserCommand } from '../commands/remove-card-from-user.command';
import { CardsSetChangedEvent } from '../events/cards-events/cards-set-changed.event';
@EventsHandler(PlayerCardSelectedEvent)
export class PlayerCardSelectedEventHandler implements IEventHandler<PlayerCardSelectedEvent>{
private readonly logger = new Logger(PlayerCardSelectedEventHandler.name);
constructor(
private cmdBus: CommandBus,
private eventBus: EventBus,
private gameService: GameService,
private quizService: QuizService,
private cardService: CardsService,
private queryBus: QueryBus,
) {}
async handle(event: PlayerCardSelectedEvent): Promise<any> {
this.logger.verbose(
`Handling card selected by ${event.telegramId} `,
JSON.stringify(event.card),
);
const question = await this.quizService.get();
event.card.setup(
this.eventBus,
this.cmdBus,
this.queryBus,
event.telegramId,
question.answeredBy,
);
try {
await this.cmdBus.execute(
new RemoveCardFromUserCommand(event.telegramId, event.card),
);
await this.cardService.markCardAsUsed(event.telegramId, event.card);
await this.eventBus.publish(new CardsSetChangedEvent(event.telegramId));
await event.card.handle();
return true;
} catch (e) {
return false;
}
}
}

View file

@ -0,0 +1,16 @@
import { CommandBus, EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { PrepareGameEvent } from '../events/prepare-game.event';
import { GameService } from '../game.service';
import { MarkQuestionsAsUnansweredCommand } from '../commands/mark-questions-as-unanswered.command';
@EventsHandler(PrepareGameEvent)
export class GamePrepareGameEventHandler
implements IEventHandler<PrepareGameEvent> {
constructor(private gameService: GameService, private cmdBus: CommandBus) {
}
async handle(event: PrepareGameEvent) {
await this.gameService.cleanGameQueue();
// await this.cmdBus.execute(new MarkQuestionsAsUnansweredCommand());
}
}

View file

@ -0,0 +1,27 @@
import { CommandBus, EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { PrizeChangeIncreasedEvent } from '../events/prize-change-increased.event';
import { ResetPlayerPrizeChanceCommand } from '../commands/reset-player-prize-chance.command';
import { Logger } from '@nestjs/common';
import { GiveOutAPrizeCommand } from '../commands/give-out-a-prize.command';
@EventsHandler(PrizeChangeIncreasedEvent)
export class GamePrizeChanceIncreasedEventHandler
implements IEventHandler<PrizeChangeIncreasedEvent> {
private readonly logger = new Logger(
GamePrizeChanceIncreasedEventHandler.name,
);
constructor(private cmdBus: CommandBus) {
}
async handle(event: PrizeChangeIncreasedEvent) {
this.logger.verbose(`Chance for player ${event.telegramId} changed and now its: ${event.newChance}`);
if (event.newChance > 100) {
await this.cmdBus.execute(
new ResetPlayerPrizeChanceCommand(event.telegramId),
);
await this.cmdBus.execute(new GiveOutAPrizeCommand(event.telegramId));
}
}
}

View file

@ -0,0 +1,15 @@
import { CommandBus, EventBus, EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { HideKeyboardCommand } from '../commands/hide-keyboard.command';
import { QuizAnsweredEvent } from '../events/quiz.answered';
@EventsHandler(QuizAnsweredEvent)
export class QuizAnsweredEventHandler
implements IEventHandler<QuizAnsweredEvent>
{
constructor(private eventBus: EventBus, private commandBus: CommandBus) {
}
async handle(event: QuizAnsweredEvent) {
await this.commandBus.execute(new HideKeyboardCommand(`На вопрос ответил: ${event.name}`));
}
}

View file

@ -0,0 +1,18 @@
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { WrongAnswerReceivedEvent } from '../events/wrong-answer-received.event';
import { GameService } from '../game.service';
import { GameQueueTypes } from '../../schemas/game-queue.schema';
@EventsHandler(WrongAnswerReceivedEvent)
export class GameWrongAnswerReceivedEventHandler
implements IEventHandler<WrongAnswerReceivedEvent> {
constructor(private gameService: GameService) {
}
async handle(event: WrongAnswerReceivedEvent) {
await this.gameService.addTaskToGameQueue(
event.tId,
GameQueueTypes.penalty,
);
}
}

View file

@ -0,0 +1,7 @@
import { Logger } from '@nestjs/common';
export class CardSelectionPendingEvent {
private readonly logger = new Logger(CardSelectionPendingEvent.name);
constructor() {}
}

View file

@ -0,0 +1,9 @@
import { GameCard } from '../entities/cards.entities';
export class CardsDealedEvent {
constructor(
public telegramId: number,
public cards: string[],
public cardsTypes: GameCard[],
) {}
}

View file

@ -0,0 +1,4 @@
export class CardsSetChangedEvent {
constructor(public telegramId: number) {
}
}

View file

@ -0,0 +1,4 @@
export class LuckyCardSelectedEvent {
constructor(public telegramId: number) {
}
}

View file

@ -0,0 +1,4 @@
export class ShitCardSelectedEvent {
constructor(public sourceId: number, public destId ) {
}
}

View file

@ -0,0 +1,4 @@
export class DebuffCardPlayedEvent {
constructor(public debufName: string, public data: string, public from: number, public dest: number) {
}
}

View file

@ -0,0 +1,3 @@
export class GameStartedEvent {
}

View file

@ -0,0 +1 @@
export class NextQuestionEvent {}

View file

@ -0,0 +1,6 @@
import { GameCard } from '../entities/cards.entities';
export class PlayerCardSelectedEvent {
constructor(public telegramId: number, public card: GameCard) {
}
}

View file

@ -0,0 +1 @@
export class PrepareGameEvent {}

View file

@ -0,0 +1,4 @@
export class PrizeChangeIncreasedEvent {
constructor(public telegramId: number, public newChance: number) {
}
}

View file

@ -0,0 +1,9 @@
import { QuizAnswerStateEnum } from '../entities/quiz-answer-state.enum';
import { Logger } from '@nestjs/common';
export class QuizAnswerStateChangedEvent {
private readonly logger = new Logger(QuizAnswerStateChangedEvent.name);
constructor(public newState: QuizAnswerStateEnum, public telegramId: number) {
this.logger.verbose(`New state is ${newState} by ${telegramId}`);
}
}

View file

@ -0,0 +1,9 @@
export class QuizAnsweredEvent {
constructor(
public name: string
) {
}
}

View file

@ -0,0 +1,4 @@
export class ScoreChangedEvent {
constructor(public telegramId: number, public newScore: number) {
}
}

View file

@ -0,0 +1,9 @@
import { Logger } from '@nestjs/common';
export class UserRegisteredEvent {
private logger = new Logger(UserRegisteredEvent.name);
constructor(public telegramId: number, public name: string) {
this.logger.verbose(`${telegramId} ${name}`);
}
}

View file

@ -0,0 +1,8 @@
import { Logger } from '@nestjs/common';
export class ValidAnswerReceivedEvent {
private readonly logger = new Logger(ValidAnswerReceivedEvent.name);
constructor(public tId: number, public validAnswer: string, public extraDetails: string) {
this.logger.verbose(`raised by: ${this.tId}`);
}
}

View file

@ -0,0 +1,8 @@
import { Logger } from '@nestjs/common';
export class WrongAnswerReceivedEvent {
private readonly logger = new Logger(WrongAnswerReceivedEvent.name);
constructor(public tId: number, public validAnswer: string) {
this.logger.verbose(`raised by ${tId}`);
}
}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GameController } from './game.controller';
describe('GameController', () => {
let controller: GameController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GameController],
}).compile();
controller = module.get<GameController>(GameController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View file

@ -0,0 +1,33 @@
import { Controller, Get, Param, Post } from '@nestjs/common';
import { GameService } from './game.service';
@Controller('game')
export class GameController {
constructor(private gameService: GameService) {
}
@Post(':id/complete')
async markQueueAsCompleted(@Param('id') id: string) {
return this.gameService.markQueueAsCompleted(id);
}
@Post('pause')
async pauseGame() {
return this.gameService.pauseGame();
}
@Post('resume')
async resumeGame() {
return this.gameService.resumeGame();
}
@Get('state')
async getState() {
return this.gameService.getState();
}
@Get('playextracards')
async playExtraCards() {
return this.gameService.playExtraCards();
}
}

52
src/game/game.module.ts Normal file
View file

@ -0,0 +1,52 @@
import { forwardRef, Global, Module } from '@nestjs/common';
import { GameService } from './game.service';
import { CqrsModule } from '@nestjs/cqrs';
import { QuizAnsweredEventHandler } from './event-handlers/quiz-answered-event.handler';
import { CardSelectionPendingEventHandler } from './event-handlers/card-selection-pending-event.handler';
import { PlayerCardSelectedEventHandler } from './event-handlers/player-card-selected-event.handler';
import { MongooseModule } from '@nestjs/mongoose';
import { GameQueue, GameQueueSchema } from '../schemas/game-queue.schema';
import { CreateNewQueueItemCommandHandler } from './comand-handlers/create-new-queue-item-command.handler';
import { GamePrepareGameEventHandler } from './event-handlers/prepare-game-event.handler';
import { GamePrizeChanceIncreasedEventHandler } from './event-handlers/prize-chance-increased-event.handler';
import { GameGiveOutAPrizeCommandHandler } from './comand-handlers/give-out-a-prize-command.handler';
import { GameProceedGameQueueCommandHandler } from './comand-handlers/proceed-game-queue-command.handler';
import { GameWrongAnswerReceivedEventHandler } from './event-handlers/wrong-answer-received-event.handler';
import { GameController } from './game.controller';
import {ConfigService} from "@nestjs/config";
import {SelectTargetPlayerHandler} from "./comand-handlers/select-target-player.handler";
import {SendBetweenRoundsActionsHandler} from "../guests/command/send-between-rounds-actions.command";
import {GuestsModule} from "../guests/guests.module";
const eventHandlers = [
QuizAnsweredEventHandler,
CardSelectionPendingEventHandler,
PlayerCardSelectedEventHandler,
GamePrepareGameEventHandler,
GamePrizeChanceIncreasedEventHandler,
GameGiveOutAPrizeCommandHandler,
GameWrongAnswerReceivedEventHandler
];
const commandHandlers = [
CreateNewQueueItemCommandHandler,
GamePrizeChanceIncreasedEventHandler,
GameProceedGameQueueCommandHandler,
SelectTargetPlayerHandler,
SendBetweenRoundsActionsHandler
];
@Global()
@Module({
imports: [
CqrsModule,
MongooseModule.forFeature([
{ name: GameQueue.name, schema: GameQueueSchema },
]),
forwardRef(() => GuestsModule)
],
providers: [GameService, ConfigService, ...eventHandlers, ...commandHandlers],
exports: [GameService],
controllers: [GameController],
})
export class GameModule {}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GameService } from './game.service';
describe('GameService', () => {
let service: GameService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GameService],
}).compile();
service = module.get<GameService>(GameService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

Some files were not shown because too many files have changed in this diff Show more