From c057e285469e2cb3bcf999e572ec2e8495e42516 Mon Sep 17 00:00:00 2001 From: "Havrylenko Ivan (WSL)" Date: Wed, 8 Nov 2023 10:07:47 +0200 Subject: [PATCH] feat(caching): add cache-manager, feat(ads): add module and service --- .eslintrc.js | 4 +- .prettierrc | 3 +- package-lock.json | 302 ++++++++++++++++-- package.json | 14 +- src/app.module.ts | 39 ++- src/controllers/updates/app.update.ts | 195 +++++++---- .../updates/tests/app.update.spec.ts | 47 ++- src/controllers/wizards/change-lang.wizard.ts | 37 ++- src/controllers/wizards/clear-last.wizard.ts | 51 +++ src/controllers/wizards/next.wizard.ts | 8 +- src/controllers/wizards/profiles.wizard.ts | 123 +++++-- src/controllers/wizards/register.wizard.ts | 170 +++++----- .../wizards/tests/change-lang.wizard.spec.ts | 31 +- .../wizards/tests/next.wizard.spec.ts | 1 + .../wizards/tests/profiles.wizard.spec.ts | 19 +- .../wizards/tests/register.wizard.spec.ts | 54 ++-- src/core/abstracts/ad.abstract.service.ts | 7 + src/core/abstracts/game.abstract.service.ts | 4 +- src/core/abstracts/index.ts | 2 + .../abstracts/profile.abstract.service.ts | 13 +- src/core/abstracts/reply.abstract.service.ts | 31 +- .../abstracts/reports.abstract.service.ts | 17 + src/core/abstracts/user.abstract.service.ts | 4 +- src/core/constants/callbacks.ts | 15 + src/core/constants/index.ts | 3 +- src/core/constants/keyboards.ts | 57 ++++ src/core/constants/markups/games.ts | 7 - src/core/constants/markups/index.ts | 5 - src/core/constants/markups/main-menu.ts | 21 -- src/core/constants/markups/profiles.ts | 11 - src/core/constants/markups/remove.ts | 5 - src/core/constants/markups/select-lang.ts | 13 - src/core/constants/tokens.ts | 5 + src/core/constants/wizards.ts | 3 + src/core/decorators/index.ts | 1 + src/core/decorators/roles.decorator.ts | 3 + src/core/dtos/ad.dto.ts | 7 + src/core/dtos/index.ts | 1 + src/core/dtos/profile.dto.ts | 10 +- src/core/dtos/report.dto.ts | 22 ++ src/core/entities/ad.entity.ts | 15 + src/core/entities/base.entity.ts | 6 +- src/core/entities/file.entity.ts | 5 +- src/core/entities/game.entity.ts | 7 +- src/core/entities/index.ts | 3 + src/core/entities/profile.entity.ts | 20 +- src/core/entities/report.entity.ts | 16 + src/core/entities/reports-channel.entity.ts | 9 + src/core/entities/user.entity.ts | 20 +- src/core/enums/index.ts | 1 - src/core/enums/languages.enum.ts | 7 - src/core/filters/global.filter.ts | 5 +- src/core/filters/tests/global.filter.spec.ts | 1 + src/core/guards/admin.guard.ts | 29 ++ src/core/guards/index.ts | 1 + src/core/interceptors/cache.interceptor.ts | 51 +++ src/core/interceptors/context.interceptor.ts | 33 -- src/core/interceptors/i18n.interceptor.ts | 5 +- src/core/interceptors/index.ts | 2 +- .../tests/context.interceptor.spec.ts | 87 ----- .../tests/i18n.interceptor.spec.ts | 13 +- src/core/pipes/about.pipe.ts | 15 + src/core/pipes/age.pipe.ts | 20 ++ src/core/pipes/game.pipe.ts | 22 ++ src/core/pipes/index.ts | 3 + src/core/types/index.ts | 1 - src/core/types/telegraf.ts | 43 --- src/core/utils/file-from-msg.ts | 1 + src/core/utils/get-cache-key.ts | 9 + src/core/utils/get-caption.ts | 9 +- src/core/utils/get-markup.ts | 43 ++- src/core/utils/index.ts | 1 + src/core/utils/tests/fetch-image.spec.ts | 1 + src/core/utils/tests/file-from-msg.spec.ts | 1 + src/core/utils/tests/get-caption.spec.ts | 5 +- src/frameworks/ad/typeorm/index.ts | 2 + .../ad/typeorm/typeorm-ad.module.ts | 18 ++ .../ad/typeorm/typeorm-ad.service.ts | 23 ++ src/frameworks/file/aws/aws-file.module.ts | 7 +- src/frameworks/file/aws/aws-file.service.ts | 4 +- .../file/aws/tests/aws-file.service.spec.ts | 1 + .../game/typeorm/tests/game.service.spec.ts | 3 +- .../game/typeorm/typeorm-game.module.ts | 3 +- .../game/typeorm/typeorm-game.service.ts | 18 +- .../tests/typeorm-profile.service.spec.ts | 3 +- .../profile/typeorm/typeorm-profile.module.ts | 5 +- .../typeorm/typeorm-profile.service.ts | 82 +++-- .../reply/telegraf/telegraf-reply.module.ts | 3 +- .../reply/telegraf/telegraf-reply.service.ts | 33 +- .../tests/telegraf-reply.service.spec.ts | 7 +- src/frameworks/report/typeorm/index.ts | 2 + .../report/typeorm/typeorm-report.module.ts | 18 ++ .../report/typeorm/typeorm-report.service.ts | 64 ++++ .../tests/typeorm-user.service.spec.ts | 3 +- .../user/typeorm/typeorm-user.module.ts | 3 +- .../user/typeorm/typeorm-user.service.ts | 25 +- src/generated/i18n.generated.ts | 25 +- src/i18n/en/errors.json | 5 +- src/i18n/en/messages.json | 20 +- src/i18n/ru/buttons.json | 6 - src/i18n/ru/commands.json | 6 - src/i18n/ru/errors.json | 5 - src/i18n/ru/messages.json | 35 -- src/i18n/ua/errors.json | 8 +- src/i18n/ua/messages.json | 20 +- src/main.ts | 1 + src/migrations/1698782983824-CreateGame.ts | 8 +- src/migrations/1698861675283-CreateAdmin.ts | 13 + src/services/ad/ad.module.ts | 8 + src/services/database/database.module.ts | 12 +- src/services/file/file.module.ts | 2 +- src/services/game/game.module.ts | 2 +- src/services/i18n/i18n.module.ts | 6 +- .../mock-database/mock-database.module.ts | 17 - src/services/profile/profile.module.ts | 2 +- src/services/reply/reply.module.ts | 2 +- src/services/report/report.module.ts | 8 + src/services/telegram/telegram.module.ts | 14 +- src/services/user/user.module.ts | 2 +- src/subscribers/profile.subscriber.ts | 43 +++ src/subscribers/subscribers.module.ts | 10 + src/types/telegraf.ts | 121 +++++-- src/use-cases/ad/ad.factory.service.ts | 12 + src/use-cases/ad/ad.use-case.module.ts | 11 + src/use-cases/ad/ad.use-case.ts | 24 ++ src/use-cases/ad/index.ts | 3 + src/use-cases/file/file.use-case.module.ts | 3 +- .../file/tests/file.use-case.spec.ts | 1 + src/use-cases/game/game.use-case.module.ts | 3 +- src/use-cases/game/game.use-case.ts | 8 +- .../game/tests/game.use-case.spec.ts | 1 + .../profile/profile-factory.service.ts | 12 +- .../profile/profile.use-case.module.ts | 6 +- src/use-cases/profile/profile.use-case.ts | 28 +- .../tests/profile-factory.service.spec.ts | 21 +- .../profile/tests/profile.use-case.spec.ts | 7 +- src/use-cases/reply/reply.use-case.module.ts | 6 +- src/use-cases/reply/reply.use-case.ts | 45 ++- .../reply/tests/reply.use-case.spec.ts | 1 + src/use-cases/reports/index.ts | 2 + .../reports/report.factory.service.ts | 24 ++ .../reports/report.use-case.module.ts | 12 + src/use-cases/reports/report.use-case.ts | 37 +++ .../user/tests/user-factory.service.spec.ts | 1 + .../user/tests/user.use-case.spec.ts | 19 +- src/use-cases/user/user.use-case.module.ts | 3 +- src/use-cases/user/user.use-case.ts | 11 +- 147 files changed, 2031 insertions(+), 873 deletions(-) create mode 100644 src/controllers/wizards/clear-last.wizard.ts create mode 100644 src/core/abstracts/ad.abstract.service.ts create mode 100644 src/core/abstracts/reports.abstract.service.ts create mode 100644 src/core/constants/keyboards.ts delete mode 100644 src/core/constants/markups/games.ts delete mode 100644 src/core/constants/markups/index.ts delete mode 100644 src/core/constants/markups/main-menu.ts delete mode 100644 src/core/constants/markups/profiles.ts delete mode 100644 src/core/constants/markups/remove.ts delete mode 100644 src/core/constants/markups/select-lang.ts create mode 100644 src/core/constants/tokens.ts create mode 100644 src/core/decorators/index.ts create mode 100644 src/core/decorators/roles.decorator.ts create mode 100644 src/core/dtos/ad.dto.ts create mode 100644 src/core/dtos/report.dto.ts create mode 100644 src/core/entities/ad.entity.ts create mode 100644 src/core/entities/report.entity.ts create mode 100644 src/core/entities/reports-channel.entity.ts delete mode 100644 src/core/enums/index.ts delete mode 100644 src/core/enums/languages.enum.ts create mode 100644 src/core/guards/admin.guard.ts create mode 100644 src/core/guards/index.ts create mode 100644 src/core/interceptors/cache.interceptor.ts delete mode 100644 src/core/interceptors/context.interceptor.ts delete mode 100644 src/core/interceptors/tests/context.interceptor.spec.ts create mode 100644 src/core/pipes/about.pipe.ts create mode 100644 src/core/pipes/age.pipe.ts create mode 100644 src/core/pipes/game.pipe.ts create mode 100644 src/core/pipes/index.ts delete mode 100644 src/core/types/index.ts delete mode 100644 src/core/types/telegraf.ts create mode 100644 src/core/utils/get-cache-key.ts create mode 100644 src/frameworks/ad/typeorm/index.ts create mode 100644 src/frameworks/ad/typeorm/typeorm-ad.module.ts create mode 100644 src/frameworks/ad/typeorm/typeorm-ad.service.ts create mode 100644 src/frameworks/report/typeorm/index.ts create mode 100644 src/frameworks/report/typeorm/typeorm-report.module.ts create mode 100644 src/frameworks/report/typeorm/typeorm-report.service.ts delete mode 100644 src/i18n/ru/buttons.json delete mode 100644 src/i18n/ru/commands.json delete mode 100644 src/i18n/ru/errors.json delete mode 100644 src/i18n/ru/messages.json create mode 100644 src/migrations/1698861675283-CreateAdmin.ts create mode 100644 src/services/ad/ad.module.ts delete mode 100644 src/services/mock-database/mock-database.module.ts create mode 100644 src/services/report/report.module.ts create mode 100644 src/subscribers/profile.subscriber.ts create mode 100644 src/subscribers/subscribers.module.ts create mode 100644 src/use-cases/ad/ad.factory.service.ts create mode 100644 src/use-cases/ad/ad.use-case.module.ts create mode 100644 src/use-cases/ad/ad.use-case.ts create mode 100644 src/use-cases/ad/index.ts create mode 100644 src/use-cases/reports/index.ts create mode 100644 src/use-cases/reports/report.factory.service.ts create mode 100644 src/use-cases/reports/report.use-case.module.ts create mode 100644 src/use-cases/reports/report.use-case.ts diff --git a/.eslintrc.js b/.eslintrc.js index ee845ed..03459cb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,10 +5,11 @@ module.exports = { tsconfigRootDir: __dirname, sourceType: 'module', }, - plugins: ['@typescript-eslint/eslint-plugin'], + plugins: ['@typescript-eslint/eslint-plugin', 'perfectionist'], extends: [ 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', + 'plugin:perfectionist/recommended-alphabetical', ], root: true, env: { @@ -21,6 +22,7 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', + 'perfectionist/sort-interfaces': 'error', 'prettier/prettier': [ 'error', { diff --git a/.prettierrc b/.prettierrc index 19dad0e..a20502b 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,4 @@ { "singleQuote": true, - "trailingComma": "all", - "plugins": ["prettier-plugin-organize-imports"] + "trailingComma": "all" } diff --git a/package-lock.json b/package-lock.json index 6189d8d..b3fa38e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,22 +11,24 @@ "dependencies": { "@aws-sdk/client-s3": "^3.436.0", "@aws-sdk/lib-storage": "^3.436.0", + "@nestjs/cache-manager": "^2.1.1", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/typeorm": "^10.0.0", "@telegraf/session": "^2.0.0-beta.6", - "better-sqlite3": "^8.7.0", + "cache-manager": "^5.2.4", + "cache-manager-redis-yet": "^4.1.2", "dotenv": "^16.3.1", "kysely": "^0.23.5", "nestjs-i18n": "^10.3.6", "nestjs-telegraf": "^2.7.0", "node-fetch": "^2.7.0", "pg": "^8.11.3", + "redis": "^4.6.10", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "telegraf": "^4.14.0", - "telegraf-ratelimit": "^2.0.0", "typeorm": "^0.3.17" }, "devDependencies": { @@ -46,6 +48,7 @@ "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", + "eslint-plugin-perfectionist": "^2.2.0", "eslint-plugin-prettier": "^5.0.0", "husky": "^8.0.3", "jest": "^29.5.0", @@ -4495,6 +4498,18 @@ "node": ">=8" } }, + "node_modules/@nestjs/cache-manager": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.1.1.tgz", + "integrity": "sha512-oYfRys4Ng0zp2HTUPNjH7gizf4vvG3PQZZ+3yGemb3xrF+p3JxDSK0cDq9NTjHzD5UmhjiyAftB9GkuL+t3r9g==", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "cache-manager": "<=5", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "10.1.18", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.1.18.tgz", @@ -4841,6 +4856,64 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.11.tgz", + "integrity": "sha512-cV7yHcOAtNQ5x/yQl7Yw1xf53kO0FNDTdDU6bFIMbW6ljB7U7ns0YRM+QIkpoqTAt6zK5k9Fq0QWlUbLcq9AvA==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz", + "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.5.tgz", + "integrity": "sha512-hPP8w7GfGsbtYEJdn4n7nXa6xt6hVZnnDktKW4ArMaFQ/m/aR7eFvsLQmG/mn1Upq99btPJk+F27IQ2dYpCoUg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", + "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -7139,6 +7212,8 @@ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.7.0.tgz", "integrity": "sha512-99jZU4le+f3G6aIl6PmmV0cxUIWqKieHxsiF7G34CVFiE+/UabpYqkU0NJIkY/96mQKikHeBjtR27vFfs5JpEw==", "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -7165,6 +7240,8 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "peer": true, "dependencies": { "file-uri-to-path": "1.0.0" } @@ -7173,6 +7250,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "devOptional": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -7183,6 +7261,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "devOptional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -7385,6 +7464,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "devOptional": true, "funding": [ { "type": "github", @@ -7484,6 +7564,41 @@ "node": ">= 0.8" } }, + "node_modules/cache-manager": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.2.4.tgz", + "integrity": "sha512-gkuCjug16NdGvKm/sydxGVx17uffrSWcEe2xraBtwRCgdYcFxwJAla4OYpASAZT2yhSoxgDiWL9XH6IAChcZJA==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^10.0.1" + } + }, + "node_modules/cache-manager-redis-yet": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/cache-manager-redis-yet/-/cache-manager-redis-yet-4.1.2.tgz", + "integrity": "sha512-pM2K1ZlOv8gQpE1Z5mcDrfLj5CsNKVRiYua/SZ12j7LEDgfDeFVntI6JSgIw0siFSR/9P/FpG30scI3frHwibA==", + "dependencies": { + "@redis/bloom": "^1.2.0", + "@redis/client": "^1.5.8", + "@redis/graph": "^1.1.0", + "@redis/json": "^1.0.4", + "@redis/search": "^1.1.3", + "@redis/time-series": "^1.0.4", + "cache-manager": "^5.2.2", + "redis": "^4.6.7" + }, + "engines": { + "node": ">= 16.17.0" + } + }, + "node_modules/cache-manager/node_modules/lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/call-bind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", @@ -7667,7 +7782,9 @@ "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "optional": true, + "peer": true }, "node_modules/chrome-trace-event": { "version": "1.0.3", @@ -7867,6 +7984,14 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -8300,6 +8425,8 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "optional": true, + "peer": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -8345,6 +8472,8 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "optional": true, + "peer": true, "engines": { "node": ">=4.0.0" } @@ -8595,6 +8724,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -8809,6 +8940,7 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "devOptional": true, "dependencies": { "once": "^1.4.0" } @@ -9120,6 +9252,62 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-perfectionist": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-2.2.0.tgz", + "integrity": "sha512-/nG2Uurd6AY7CI6zlgjHPOoiPY8B7EYMUWdNb5w+EzyauYiQjjD5lQwAI1FlkBbCCFFZw/CdZIPQhXumYoiyaw==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^6.7.5", + "minimatch": "^9.0.3", + "natural-compare-lite": "^1.4.0" + }, + "peerDependencies": { + "astro-eslint-parser": "^0.16.0", + "eslint": ">=8.0.0", + "svelte": ">=3.0.0", + "svelte-eslint-parser": "^0.33.0", + "vue-eslint-parser": ">=9.0.0" + }, + "peerDependenciesMeta": { + "astro-eslint-parser": { + "optional": true + }, + "svelte": { + "optional": true + }, + "svelte-eslint-parser": { + "optional": true + }, + "vue-eslint-parser": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-perfectionist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-plugin-perfectionist/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", @@ -9381,6 +9569,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -9685,7 +9875,9 @@ "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true, + "peer": true }, "node_modules/fill-range": { "version": "7.0.1", @@ -9938,7 +10130,9 @@ "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "optional": true, + "peer": true }, "node_modules/fs-extra": { "version": "10.1.0", @@ -9996,6 +10190,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -10058,7 +10260,9 @@ "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "optional": true, + "peer": true }, "node_modules/glob": { "version": "7.2.3", @@ -10524,7 +10728,9 @@ "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "optional": true, + "peer": true }, "node_modules/inquirer": { "version": "8.2.6", @@ -11710,6 +11916,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -11986,6 +12197,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "optional": true, + "peer": true, "engines": { "node": ">=10" }, @@ -12009,6 +12222,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "devOptional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -12044,7 +12258,9 @@ "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "optional": true, + "peer": true }, "node_modules/morgan": { "version": "1.10.0", @@ -12158,7 +12374,9 @@ "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "optional": true, + "peer": true }, "node_modules/natural-compare": { "version": "1.4.0", @@ -12166,6 +12384,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -12231,6 +12455,8 @@ "version": "3.49.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.49.0.tgz", "integrity": "sha512-ji8IK8VT2zAQv9BeOqwnpuvJnCivxPCe2HNiPe8P1z1SDhqEFpm7GqctqTWkujb8mLfZ1PWDrjMeiq6l9TN7fA==", + "optional": true, + "peer": true, "dependencies": { "semver": "^7.3.5" }, @@ -13011,6 +13237,8 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -13140,6 +13368,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "devOptional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -13267,6 +13496,8 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "optional": true, + "peer": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -13281,6 +13512,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13335,6 +13568,19 @@ "node": ">= 0.10" } }, + "node_modules/redis": { + "version": "4.6.10", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.10.tgz", + "integrity": "sha512-mmbyhuKgDiJ5TWUhiKhBssz+mjsuSI/lSZNPI9QvZOYzWvYGejtb+W3RlDDf8LD6Bdl5/mZeG8O1feUGhXTxEg==", + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.11", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.6", + "@redis/search": "1.1.5", + "@redis/time-series": "1.0.5" + } + }, "node_modules/reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -13785,6 +14031,7 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "devOptional": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -13799,6 +14046,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -13809,7 +14057,8 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true }, "node_modules/send": { "version": "0.18.0", @@ -14099,7 +14348,9 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "optional": true, + "peer": true }, "node_modules/simple-get": { "version": "4.0.1", @@ -14119,6 +14370,8 @@ "url": "https://feross.org/support" } ], + "optional": true, + "peer": true, "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -14743,6 +14996,8 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "optional": true, + "peer": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -14754,6 +15009,8 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "optional": true, + "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -14769,6 +15026,8 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -14799,25 +15058,6 @@ "node": "^12.20.0 || >=14.13.1" } }, - "node_modules/telegraf-ratelimit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/telegraf-ratelimit/-/telegraf-ratelimit-2.0.0.tgz", - "integrity": "sha512-OUVJRz+pVDpkOfwvFuv4x5YxcPIl6puPN3HRdicN3NDnT+jQeMzKDwVAkvloUtyH1Ruo1wH4nfB5tZbtJAJ4pQ==", - "dependencies": { - "debug": "^3.1.0" - }, - "engines": { - "node": ">=6.2.1" - } - }, - "node_modules/telegraf-ratelimit/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/terser": { "version": "5.21.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.21.0.tgz", @@ -15222,6 +15462,8 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "optional": true, + "peer": true, "dependencies": { "safe-buffer": "^5.0.1" }, diff --git a/package.json b/package.json index 5dc2660..bbbb280 100644 --- a/package.json +++ b/package.json @@ -39,31 +39,38 @@ "test:cov": "jest --coverage --runInBand --detectOpenHandles --forceExit", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "compodoc:build": "compodoc -p tsconfig.doc.json -n 'Ua teammates documentation' -s -r 3000", + "pg:up": "docker run --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=password -d postgres", + "redis:up": "docker run -d -p 6379:6379 --name redis --network redisnet redis", + "redisnet:up": "docker network create -d bridge redisnet", "docs:dev": "vitepress dev", "docs:build": "vitepress build", "docs:preview": "vitepress preview", "sessions:drop": "sqlite3 ./dev.db 'DROP TABLE \"telegraf-sessions\"'", - "typeorm": "typeorm-ts-node-commonjs" + "typeorm": "typeorm-ts-node-commonjs", + "migration:run": "npm run typeorm migration:run -- -d src/datasource", + "services:up": "npm run redis:up && npm run pg:up" }, "dependencies": { "@aws-sdk/client-s3": "^3.436.0", "@aws-sdk/lib-storage": "^3.436.0", + "@nestjs/cache-manager": "^2.1.1", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/typeorm": "^10.0.0", "@telegraf/session": "^2.0.0-beta.6", - "better-sqlite3": "^8.7.0", + "cache-manager": "^5.2.4", + "cache-manager-redis-yet": "^4.1.2", "dotenv": "^16.3.1", "kysely": "^0.23.5", "nestjs-i18n": "^10.3.6", "nestjs-telegraf": "^2.7.0", "node-fetch": "^2.7.0", "pg": "^8.11.3", + "redis": "^4.6.10", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "telegraf": "^4.14.0", - "telegraf-ratelimit": "^2.0.0", "typeorm": "^0.3.17" }, "devDependencies": { @@ -83,6 +90,7 @@ "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", + "eslint-plugin-perfectionist": "^2.2.0", "eslint-plugin-prettier": "^5.0.0", "husky": "^8.0.3", "jest": "^29.5.0", diff --git a/src/app.module.ts b/src/app.module.ts index d2a5278..60e75a7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,6 +1,10 @@ +import { CacheModule, CacheStore } from '@nestjs/cache-manager'; import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; +import { redisStore } from 'cache-manager-redis-yet'; +import { RedisClientOptions } from 'redis'; + import { AppUpdate } from './controllers/updates'; import { ChangeLangWizard, @@ -8,31 +12,49 @@ import { ProfilesWizard, RegisterWizard, } from './controllers/wizards'; +import { ClearLastProfilesWizard } from './controllers/wizards/clear-last.wizard'; import { GlobalFilter } from './core/filters'; -import { ContextInterceptor, I18nInterceptor } from './core/interceptors'; +import { RoleGuard } from './core/guards'; +import { CacheInterceptor, I18nInterceptor } from './core/interceptors'; import { DatabaseModule, ReplyModule, TelegramModule, UserModule, } from './services'; +import { AdModule } from './services/ad/ad.module'; import { FileModule } from './services/file/file.module'; import { GameModule } from './services/game/game.module'; import { I18nModule } from './services/i18n/i18n.module'; import { ProfileModule } from './services/profile/profile.module'; +import { ReportModule } from './services/report/report.module'; +import { SubscribersModule } from './subscribers/subscribers.module'; +import { AdUseCasesModule } from './use-cases/ad'; import { FileUseCasesModule } from './use-cases/file'; import { GameUseCasesModule } from './use-cases/game'; import { ProfileUseCasesModule } from './use-cases/profile'; import { ReplyUseCasesModule } from './use-cases/reply'; +import { ReportUseCasesModule } from './use-cases/reports/report.use-case.module'; import { UserUseCasesModule } from './use-cases/user'; @Module({ imports: [ + CacheModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + isGlobal: true, + useFactory: async (configService: ConfigService) => ({ + store: redisStore as unknown as CacheStore, + ttl: 24 * 60 * 60, // 1 day + url: configService.get('REDIS_URL'), + }), + }), ConfigModule.forRoot({ isGlobal: true, }), TelegramModule, DatabaseModule, + SubscribersModule, UserModule, UserUseCasesModule, ReplyModule, @@ -43,6 +65,10 @@ import { UserUseCasesModule } from './use-cases/user'; FileUseCasesModule, ProfileModule, ProfileUseCasesModule, + ReportModule, + ReportUseCasesModule, + AdModule, + AdUseCasesModule, I18nModule, ], providers: [ @@ -51,18 +77,23 @@ import { UserUseCasesModule } from './use-cases/user'; ChangeLangWizard, NextActionWizard, ProfilesWizard, + ClearLastProfilesWizard, { provide: APP_INTERCEPTOR, useClass: I18nInterceptor, }, { provide: APP_INTERCEPTOR, - useClass: ContextInterceptor, + useClass: CacheInterceptor, }, { provide: APP_FILTER, useClass: GlobalFilter, }, + { + provide: APP_GUARD, + useClass: RoleGuard, + }, ], }) export class AppModule {} diff --git a/src/controllers/updates/app.update.ts b/src/controllers/updates/app.update.ts index 55139b8..fd98d5a 100644 --- a/src/controllers/updates/app.update.ts +++ b/src/controllers/updates/app.update.ts @@ -1,21 +1,37 @@ -import { Command, Ctx, Hears, Help, On, Start, Update } from 'nestjs-telegraf'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Inject } from '@nestjs/common'; +import { Cache } from 'cache-manager'; +import { + Action, + Command, + Ctx, + Hears, + Help, + On, + Start, + Update, +} from 'nestjs-telegraf'; import { CHANGE_LANG_WIZARD_ID, COOP_CALLBACK, HELP_CALLBACK, + Keyboards, LANG_CALLBACK, - LEAVE_PROFILES_CALLBACK, LOOK_CALLBACK, - NEXT_PROFILE_CALLBACK, - PROFILES_WIZARD_ID, PROFILE_CALLBACK, + PROFILES_WIZARD_ID, + REGISTER_WIZARD_ID, + UPDATE_PROFILE_CALLBACK, } from 'src/core/constants'; -import { Game } from 'src/core/entities'; -import { getCaption } from 'src/core/utils'; -import { MessageContext, MsgKey, MsgWithExtra } from 'src/types'; +import { Roles } from 'src/core/decorators'; +import { Game, Profile } from 'src/core/entities'; +import { getCaption, getMeMarkup, getProfileCacheKey } from 'src/core/utils'; +import { HandlerResponse, Language, MessageContext } from 'src/types'; import { GameUseCases } from 'src/use-cases/game'; +import { ProfileUseCases } from 'src/use-cases/profile'; import { ReplyUseCases } from 'src/use-cases/reply'; -import { Markup } from 'telegraf'; +import { ReportUseCases } from 'src/use-cases/reports'; +import { deunionize, Markup } from 'telegraf'; import { InlineQueryResult } from 'telegraf/typings/core/types/typegram'; @Update() @@ -23,96 +39,145 @@ export class AppUpdate { constructor( private readonly replyUseCases: ReplyUseCases, private readonly gameUseCases: GameUseCases, + private readonly reportUseCases: ReportUseCases, + private readonly profileUseCases: ProfileUseCases, + @Inject(CACHE_MANAGER) private readonly cache: Cache, ) {} - @Start() - async onStart(@Ctx() ctx: MessageContext): Promise { - if (!ctx.session.user.profile) { - await this.replyUseCases.replyI18n(ctx, 'commands.start'); + @Command('coop') + @Hears(COOP_CALLBACK) + async onCoop(): Promise { + return [ + 'commands.coop', + { + parse_mode: 'HTML', + }, + ]; + } - await ctx.scene.enter(CHANGE_LANG_WIZARD_ID); + @Help() + @Hears(HELP_CALLBACK) + async onHelp(): Promise { + return 'commands.help'; + } - return; - } + @On('inline_query') + async onInlineQuery(@Ctx() ctx: MessageContext): Promise { + const games: Game[] = await this.gameUseCases.findStartsWith( + ctx.inlineQuery.query, + ); - return 'commands.start'; + await ctx.answerInlineQuery( + games.map( + (game): InlineQueryResult => ({ + description: game.description, + id: game.id.toString(), + input_message_content: { + message_text: game.title, + }, + thumbnail_url: game.image, + title: game.title, + type: 'article', + }), + ), + ); } @Command('language') @Hears(LANG_CALLBACK) - async onLanguage(@Ctx() ctx: MessageContext): Promise { + async onLanguage(@Ctx() ctx: MessageContext): Promise { await ctx.scene.enter(CHANGE_LANG_WIZARD_ID); } @Command('me') @Hears(PROFILE_CALLBACK) - async onMe(@Ctx() ctx: MessageContext) { - await ctx.replyWithPhoto( - { url: ctx.session.user.profile.file.url }, - { - caption: getCaption(ctx.session.user.profile), - }, + async onMe(@Ctx() ctx: MessageContext): Promise { + const cached = await this.cache.get( + getProfileCacheKey(ctx.from.id), ); - } + if (!cached) { + await ctx.scene.enter(CHANGE_LANG_WIZARD_ID); - @Command('coop') - @Hears(COOP_CALLBACK) - async onCoop(): Promise { - return [ - 'commands.coop', - { - parse_mode: 'HTML', - }, - ]; + return; + } + + await ctx.replyWithPhoto(cached.fileId, { + caption: getCaption(cached), + reply_markup: getMeMarkup( + this.replyUseCases.translate( + 'messages.profile.update', + ctx.session.lang, + ), + ), + }); } @Command('profiles') @Hears(LOOK_CALLBACK) - async onProfiles(@Ctx() ctx: MessageContext): Promise { + async onProfiles(@Ctx() ctx: MessageContext): Promise { + const cached = await this.cache.get( + getProfileCacheKey(ctx.from.id), + ); + if (!cached) { + await ctx.scene.enter(REGISTER_WIZARD_ID); + } + await this.replyUseCases.replyI18n(ctx, 'messages.searching_teammates', { reply_markup: Markup.removeKeyboard().reply_markup, }); await this.replyUseCases.replyI18n(ctx, 'commands.profiles', { - reply_markup: Markup.keyboard([ - [ - Markup.button.callback(NEXT_PROFILE_CALLBACK, NEXT_PROFILE_CALLBACK), - Markup.button.callback( - LEAVE_PROFILES_CALLBACK, - LEAVE_PROFILES_CALLBACK, - ), - ], - ]).resize(true).reply_markup, + reply_markup: Keyboards.profiles, }); await ctx.scene.enter(PROFILES_WIZARD_ID); } - @Help() - @Hears(HELP_CALLBACK) - async onHelp(): Promise { - return 'commands.help'; - } + // TODO + @Action(/reporter-info-*/) + async onReporterInfo(@Ctx() ctx: MessageContext): Promise {} - @On('inline_query') - async onInlineQuery(@Ctx() ctx: MessageContext) { - const games: Game[] = await this.gameUseCases.findStartsWith( - ctx.inlineQuery.query, + @Action(/sen-*/) + async onSentence(@Ctx() ctx: MessageContext): Promise { + const userId = parseInt( + deunionize(ctx.callbackQuery).data.replace('sen-', ''), ); - await ctx.answerInlineQuery( - games.map( - (game): InlineQueryResult => ({ - type: 'article', - id: game.id.toString(), - title: game.title, - description: game.description, - thumbnail_url: game.image, - input_message_content: { - message_text: game.title, - }, - }), - ), + await this.profileUseCases.deleteByUser(userId); + + await this.replyUseCases.sendMsgToChatI18n( + userId, + Language.UA, + 'messages.profile.deleted', ); } + + @Roles(['admin']) + @Command('set_reports_channel') + async onSetReportsBranch( + @Ctx() ctx: MessageContext, + ): Promise { + await this.reportUseCases.createReportChannel({ + id: ctx.chat.id, + }); + + return 'messages.report.channel.ok'; + } + + @Start() + async onStart(@Ctx() ctx: MessageContext): Promise { + const cached = await this.cache.get( + getProfileCacheKey(ctx.from.id), + ); + if (!cached) { + await ctx.scene.enter(CHANGE_LANG_WIZARD_ID); + } + + return 'commands.start'; + } + + @Action(UPDATE_PROFILE_CALLBACK) + async onUpdateProfile(@Ctx() ctx: MessageContext): Promise { + await ctx.scene.enter(REGISTER_WIZARD_ID); + } } diff --git a/src/controllers/updates/tests/app.update.spec.ts b/src/controllers/updates/tests/app.update.spec.ts index 48d76d9..726001a 100644 --- a/src/controllers/updates/tests/app.update.spec.ts +++ b/src/controllers/updates/tests/app.update.spec.ts @@ -1,5 +1,7 @@ import { createMock } from '@golevelup/ts-jest'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Test, TestingModule } from '@nestjs/testing'; +import { Cache } from 'cache-manager'; import { CHANGE_LANG_WIZARD_ID, LEAVE_PROFILES_CALLBACK, @@ -10,10 +12,13 @@ import { Game, User } from 'src/core/entities'; import { getCaption } from 'src/core/utils'; import { MessageContext } from 'src/types/telegraf'; import { GameUseCases } from 'src/use-cases/game'; +import { ProfileUseCases } from 'src/use-cases/profile'; import { ReplyUseCases } from 'src/use-cases/reply'; +import { ReportUseCases } from 'src/use-cases/reports'; import { UserUseCases } from 'src/use-cases/user/user.use-case'; import { Markup } from 'telegraf'; import { InlineQueryResult } from 'telegraf/typings/core/types/typegram'; + import { AppUpdate } from '../app.update'; describe('AppUpdate', () => { @@ -37,6 +42,18 @@ describe('AppUpdate', () => { provide: GameUseCases, useValue: createMock(), }, + { + provide: ReportUseCases, + useValue: createMock(), + }, + { + provide: ProfileUseCases, + useValue: createMock(), + }, + { + provide: CACHE_MANAGER, + useValue: createMock(), + }, ], }).compile(); @@ -56,9 +73,6 @@ describe('AppUpdate', () => { from: { id: userId, }, - session: { - user: {}, - }, scene: { enter: jest.fn(), }, @@ -81,7 +95,6 @@ describe('AppUpdate', () => { const resp = await update.onStart(ctx); - expect(ctx.session.user).toEqual(ctx.session.user); expect(resp).toEqual('commands.start'); }); }); @@ -109,7 +122,6 @@ describe('AppUpdate', () => { file: createMock({ url: 'url', }), - name: 'name', games: [ createMock({ title: 'CS:GO', @@ -118,6 +130,7 @@ describe('AppUpdate', () => { title: 'CS 2', }), ], + name: 'name', }), }), }, @@ -191,28 +204,28 @@ describe('AppUpdate', () => { it('should reply with the inline query results', async () => { const games = [ createMock({ - id: 1, - title: 'Test1', description: "Test1's description", + id: 1, image: "Test1's image", + title: 'Test1', }), createMock({ - id: 2, - title: 'Test2', description: "Test2's description", + id: 2, image: "Test2's image", + title: 'Test2', }), createMock({ - id: 3, - title: 'Test3', description: "Test3's description", + id: 3, image: "Test3's image", + title: 'Test3', }), createMock({ - id: 4, - title: 'Test4', description: "Test4's description", + id: 4, image: "Test4's image", + title: 'Test4', }), ]; const ctx = createMock({ @@ -228,14 +241,14 @@ describe('AppUpdate', () => { expect(ctx.answerInlineQuery).toHaveBeenCalledWith( games.map( (game): InlineQueryResult => ({ - type: 'article', - id: game.id.toString(), - title: game.title, description: game.description, - thumbnail_url: game.image, + id: game.id.toString(), input_message_content: { message_text: game.title, }, + thumbnail_url: game.image, + title: game.title, + type: 'article', }), ), ); diff --git a/src/controllers/wizards/change-lang.wizard.ts b/src/controllers/wizards/change-lang.wizard.ts index d2e4f93..5a8ea22 100644 --- a/src/controllers/wizards/change-lang.wizard.ts +++ b/src/controllers/wizards/change-lang.wizard.ts @@ -1,29 +1,33 @@ +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Inject } from '@nestjs/common'; +import { Cache } from 'cache-manager'; import { Ctx, Message, On, Wizard, WizardStep } from 'nestjs-telegraf'; import { CHANGE_LANG_WIZARD_ID, + Keyboards, NEXT_WIZARD_ID, REGISTER_WIZARD_ID, - REMOVE_KEYBOARD_MARKUP, - SELECT_LANG_MARKUP, } from 'src/core/constants'; -import { Language } from 'src/core/enums'; -import { Extra } from 'src/core/types'; -import { MsgKey, MsgWithExtra, WizardContext } from 'src/types'; +import { getProfileCacheKey } from 'src/core/utils'; +import { HandlerResponse, Language, WizardContext } from 'src/types'; import { ReplyUseCases } from 'src/use-cases/reply'; @Wizard(CHANGE_LANG_WIZARD_ID) export class ChangeLangWizard { - constructor(private readonly replyUseCases: ReplyUseCases) {} + constructor( + @Inject(CACHE_MANAGER) private readonly cache: Cache, + private readonly replyUseCases: ReplyUseCases, + ) {} @WizardStep(1) - onEnter(@Ctx() ctx: WizardContext): [MsgKey, Extra] { + async onEnter(@Ctx() ctx: WizardContext): Promise { + const profile = await this.cache.get(getProfileCacheKey(ctx.from.id)); + ctx.wizard.next(); return [ - ctx.session.user.profile - ? 'messages.lang.update' - : 'messages.lang.select', - { reply_markup: SELECT_LANG_MARKUP }, + profile ? 'messages.lang.update' : 'messages.lang.select', + { reply_markup: Keyboards.selectLang }, ]; } @@ -32,7 +36,9 @@ export class ChangeLangWizard { async onLang( @Ctx() ctx: WizardContext, @Message() msg: { text: string }, - ): Promise { + ): Promise { + const profile = await this.cache.get(getProfileCacheKey(ctx.from.id)); + switch (msg.text) { case '🇺🇦': ctx.session.lang = Language.UA; @@ -40,16 +46,13 @@ export class ChangeLangWizard { case '🇬🇧': ctx.session.lang = Language.EN; break; - case '🇷🇺': - ctx.session.lang = Language.RU; - break; default: return 'messages.lang.invalid'; } await ctx.scene.leave(); - if (!ctx.session.user.profile) { + if (!profile) { await this.replyUseCases.replyI18n(ctx, 'messages.lang.changed'); await ctx.scene.enter(REGISTER_WIZARD_ID); @@ -57,7 +60,7 @@ export class ChangeLangWizard { } await this.replyUseCases.replyI18n(ctx, 'messages.lang.changed', { - reply_markup: REMOVE_KEYBOARD_MARKUP, + reply_markup: Keyboards.remove, }); await ctx.scene.enter(NEXT_WIZARD_ID); diff --git a/src/controllers/wizards/clear-last.wizard.ts b/src/controllers/wizards/clear-last.wizard.ts new file mode 100644 index 0000000..5032a3f --- /dev/null +++ b/src/controllers/wizards/clear-last.wizard.ts @@ -0,0 +1,51 @@ +import { Context, Ctx, Message, On, Wizard, WizardStep } from 'nestjs-telegraf'; +import { + CLEAR_LAST_WIZARD_ID, + CLEAR_LAST_YES_CALLBACK, + Keyboards, + NEXT_WIZARD_ID, +} from 'src/core/constants'; +import { HandlerResponse, MessageContext, WizardContext } from 'src/types'; +import { ReplyUseCases } from 'src/use-cases/reply'; + +@Wizard(CLEAR_LAST_WIZARD_ID) +export class ClearLastProfilesWizard { + constructor(private readonly replyUseCases: ReplyUseCases) {} + + @WizardStep(2) + @On('text') + async onAnswer( + @Context() ctx: MessageContext, + @Message() msg: { text: string }, + ): Promise { + if (msg.text === CLEAR_LAST_YES_CALLBACK) { + ctx.session.seenProfiles = []; + + this.replyUseCases.replyI18n(ctx, 'messages.profile.last.cleared', { + reply_markup: Keyboards.remove, + }); + } + + await ctx.scene.enter(NEXT_WIZARD_ID); + } + + @WizardStep(1) + onEnter(@Ctx() ctx: WizardContext): HandlerResponse { + ctx.wizard.next(); + + return [ + [ + 'messages.profile.last.no_more', + { + reply_markup: Keyboards.remove, + }, + ], + [ + 'messages.profile.last.clear', + { + reply_markup: Keyboards.clearLast, + }, + ], + ]; + } +} diff --git a/src/controllers/wizards/next.wizard.ts b/src/controllers/wizards/next.wizard.ts index 22fa2c1..4ea23a7 100644 --- a/src/controllers/wizards/next.wizard.ts +++ b/src/controllers/wizards/next.wizard.ts @@ -1,16 +1,16 @@ import { Ctx, Wizard, WizardStep } from 'nestjs-telegraf'; -import { MAIN_MENU_MARKUP, NEXT_WIZARD_ID } from 'src/core/constants'; -import { MessageContext, MsgWithExtra } from 'src/types'; +import { Keyboards, NEXT_WIZARD_ID } from 'src/core/constants'; +import { HandlerResponse, MessageContext } from 'src/types'; @Wizard(NEXT_WIZARD_ID) export class NextActionWizard { @WizardStep(1) - async onEnter(@Ctx() ctx: MessageContext): Promise { + async onEnter(@Ctx() ctx: MessageContext): Promise { await ctx.scene.leave(); return [ 'messages.next_action', { - reply_markup: MAIN_MENU_MARKUP, + reply_markup: Keyboards.mainMenu, }, ]; } diff --git a/src/controllers/wizards/profiles.wizard.ts b/src/controllers/wizards/profiles.wizard.ts index f3796d5..032e1d3 100644 --- a/src/controllers/wizards/profiles.wizard.ts +++ b/src/controllers/wizards/profiles.wizard.ts @@ -1,47 +1,44 @@ -import { Ctx, Hears, Message, Wizard, WizardStep } from 'nestjs-telegraf'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Inject } from '@nestjs/common'; +import { Cache } from 'cache-manager'; +import { Ctx, Hears, Message, On, Wizard, WizardStep } from 'nestjs-telegraf'; import { + CLEAR_LAST_WIZARD_ID, LEAVE_PROFILES_CALLBACK, NEXT_PROFILE_CALLBACK, NEXT_WIZARD_ID, PROFILES_WIZARD_ID, + REPORT_CALLBACK, } from 'src/core/constants'; -import { getCaption, getProfileMarkup } from 'src/core/utils'; -import { ProfilesMessageContext } from 'src/types'; +import { CreateReportDto } from 'src/core/dtos'; +import { Profile } from 'src/core/entities'; +import { + getCaption, + getProfileCacheKey, + getProfileMarkup, +} from 'src/core/utils'; +import { HandlerResponse, ProfilesWizardContext } from 'src/types'; import { ProfileUseCases } from 'src/use-cases/profile'; +import { ReplyUseCases } from 'src/use-cases/reply'; +import { ReportUseCases } from 'src/use-cases/reports'; import { deunionize } from 'telegraf'; +// TODO: implement ads functionality @Wizard(PROFILES_WIZARD_ID) export class ProfilesWizard { - constructor(private readonly profileUseCases: ProfileUseCases) {} - - @WizardStep(1) - async onEnter(@Ctx() ctx: ProfilesMessageContext) { - const profile = await this.profileUseCases.findRecommended( - ctx.session.user.profile, - ); - - ctx.wizard.state.current = profile; - ctx.wizard.next(); - - const chat = await ctx.telegram.getChat(profile.user.id); - - await ctx.replyWithPhoto( - { url: ctx.wizard.state.current.file.url }, - { - caption: getCaption(profile), - reply_markup: getProfileMarkup( - `https://t.me/${deunionize(chat).username}`, - ), - }, - ); - } + constructor( + private readonly profileUseCases: ProfileUseCases, + private readonly replyUseCases: ReplyUseCases, + private readonly reportUseCases: ReportUseCases, + @Inject(CACHE_MANAGER) private readonly cache: Cache, + ) {} @WizardStep(2) - @Hears([NEXT_PROFILE_CALLBACK, LEAVE_PROFILES_CALLBACK]) + @Hears([NEXT_PROFILE_CALLBACK, LEAVE_PROFILES_CALLBACK, REPORT_CALLBACK]) async onAction( - @Ctx() ctx: ProfilesMessageContext, + @Ctx() ctx: ProfilesWizardContext, @Message() msg: { text: string }, - ) { + ): Promise { switch (msg.text) { case NEXT_PROFILE_CALLBACK: { ctx.scene.leave(); @@ -58,6 +55,74 @@ export class ProfilesWizard { break; } + + case REPORT_CALLBACK: { + ctx.wizard.next(); + + return 'messages.report.send'; + } + } + } + + @WizardStep(1) + async onEnter(@Ctx() ctx: ProfilesWizardContext): Promise { + if (!ctx.session.seenProfiles) { + ctx.session.seenProfiles = []; + } + + if (!ctx.session.seenLength) { + ctx.session.seenLength = 0; } + + const cached = await this.cache.get( + getProfileCacheKey(ctx.from.id), + ); + const profile = await this.profileUseCases.findRecommended( + cached, + ctx.session.seenProfiles, + ctx.session.seenLength, + ); + + ctx.wizard.state.current = profile; + + if (!profile) { + await ctx.scene.enter(CLEAR_LAST_WIZARD_ID); + + return; + } + + ctx.session.seenProfiles.push(profile.id); + ctx.session.seenLength++; + ctx.wizard.next(); + + const chat = await ctx.telegram.getChat(profile.user.id); + + await ctx.replyWithPhoto(profile.fileId, { + caption: getCaption(profile), + reply_markup: getProfileMarkup( + `https://t.me/${deunionize(chat).username}`, + ), + }); + } + + @WizardStep(3) + @On('text') + async onText( + @Ctx() ctx: ProfilesWizardContext, + @Message() msg: { text: string }, + ): Promise { + const reportDto: CreateReportDto = { + description: msg.text, + reporterId: ctx.from.id, + userId: ctx.wizard.state.current.user.id, + }; + + const report = await this.reportUseCases.createReport(reportDto); + + await this.replyUseCases.sendToReportsChannel(report); + + await ctx.scene.leave(); + + return 'messages.report.ok'; } } diff --git a/src/controllers/wizards/register.wizard.ts b/src/controllers/wizards/register.wizard.ts index 02168b0..45af210 100644 --- a/src/controllers/wizards/register.wizard.ts +++ b/src/controllers/wizards/register.wizard.ts @@ -1,61 +1,54 @@ +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Inject } from '@nestjs/common'; +import { Cache } from 'cache-manager'; import { Ctx, Message, On, Wizard, WizardStep } from 'nestjs-telegraf'; import { - GAMES_MARKUP, + Keyboards, NEXT_WIZARD_ID, REGISTER_WIZARD_ID, - REMOVE_KEYBOARD_MARKUP, } from 'src/core/constants'; import { CreateProfileDto } from 'src/core/dtos'; +import { Profile } from 'src/core/entities'; import { BotException } from 'src/core/errors'; -import { Extra } from 'src/core/types'; -import { fileFromMsg, getNameMarkup } from 'src/core/utils'; +import { AboutPipe, AgePipe, GamePipe } from 'src/core/pipes'; +import { getNameMarkup, getProfileCacheKey } from 'src/core/utils'; import { + HandlerResponse, MsgKey, MsgWithExtra, PhotoMessage, - WizardMessageContext, + RegisterWizardContext, } from 'src/types/telegraf'; -import { FileUseCases } from 'src/use-cases/file/file.use-case.service'; -import { GameUseCases } from 'src/use-cases/game'; import { ProfileUseCases } from 'src/use-cases/profile'; import { ReplyUseCases } from 'src/use-cases/reply'; @Wizard(REGISTER_WIZARD_ID) export class RegisterWizard { constructor( + @Inject(CACHE_MANAGER) private readonly cache: Cache, private readonly replyUseCases: ReplyUseCases, - private readonly fileUseCases: FileUseCases, private readonly profileUseCases: ProfileUseCases, - private readonly gameUseCases: GameUseCases, ) {} - @WizardStep(1) - async onEnter(@Ctx() ctx: WizardMessageContext): Promise { - ctx.wizard.next(); - - return [ - ['messages.user.new', {}], - [ - 'messages.name.send', - { reply_markup: getNameMarkup(ctx.from.first_name) }, - ], - ]; - } - @On('text') - @WizardStep(2) - async onName( - @Ctx() ctx: WizardMessageContext, - @Message() msg: { text: string }, - ): Promise<[MsgKey, Extra]> { - ctx.wizard.state.name = msg.text; + @WizardStep(4) + async onAbout( + @Ctx() ctx: RegisterWizardContext, + @Message(AboutPipe) msg: { text: string }, + ): Promise { + const about = msg.text; + + ctx.wizard.state['about'] = about; ctx.wizard.next(); return [ - 'messages.age.send', + 'messages.game.send', { - reply_markup: REMOVE_KEYBOARD_MARKUP, + i18nArgs: { + username: ctx.me, + }, + reply_markup: Keyboards.games, }, ]; } @@ -63,105 +56,122 @@ export class RegisterWizard { @On('text') @WizardStep(3) async onAge( - @Ctx() ctx: WizardMessageContext, - @Message() msg: { text: string }, - ): Promise { - const age = parseInt(msg.text); - - if (isNaN(age)) { - return 'messages.age.invalid'; - } - - ctx.wizard.state['age'] = age; + @Ctx() ctx: RegisterWizardContext, + @Message(AgePipe) msg: { text: number }, + ): Promise { + ctx.wizard.state['age'] = msg.text; ctx.wizard.next(); return 'messages.about.send'; } - @On('text') - @WizardStep(4) - async onAbout( - @Ctx() ctx: WizardMessageContext, - @Message() msg: { text: string }, - ): Promise { - const about = msg.text; - - ctx.wizard.state['about'] = about; + @WizardStep(1) + async onEnter(@Ctx() ctx: RegisterWizardContext): Promise { + const profile = await this.cache.get(getProfileCacheKey(ctx.from.id)); ctx.wizard.next(); - return [ - 'messages.game.send', + ctx.wizard.state.games = []; + + const resp: MsgWithExtra[] = []; + + if (!profile) { + resp.push(['messages.user.new', {}]); + } + + resp.push([ + 'messages.name.send', { - reply_markup: GAMES_MARKUP, - i18nArgs: { - username: ctx.me, - }, + reply_markup: getNameMarkup(ctx.from.first_name), }, - ]; + ]); + + return resp; } @On('text') @WizardStep(5) async onGame( - @Ctx() ctx: WizardMessageContext, - @Message() msg: { text: string }, - ): Promise { + @Ctx() ctx: RegisterWizardContext, + @Message(GamePipe) msg: { gameId: number; text: string }, + ): Promise { if (msg.text === '✅') { + if (ctx.wizard.state.games.length === 0) { + return 'errors.unknown'; + } + ctx.wizard.next(); return [ 'messages.picture.send', { - reply_markup: REMOVE_KEYBOARD_MARKUP, + reply_markup: Keyboards.remove, }, ]; } - const game = await this.gameUseCases.findByTitle(msg.text); - if (!game) { - throw new BotException('messages.game.invalid'); - } - - if (!ctx.wizard.state.games) { - ctx.wizard.state.games = []; - } - - if (ctx.wizard.state.games.includes(game.id)) { + if (ctx.wizard.state.games.includes(msg.gameId)) { throw new BotException('messages.game.already_added'); } - ctx.wizard.state.games.push(game.id); + ctx.wizard.state.games.push(msg.gameId); return 'messages.game.ok'; } + @On('text') + @WizardStep(2) + async onName( + @Ctx() ctx: RegisterWizardContext, + @Message() msg: { text: string }, + ): Promise { + ctx.wizard.state.name = msg.text; + + ctx.wizard.next(); + + return [ + 'messages.age.send', + { + reply_markup: Keyboards.remove, + }, + ]; + } + @On('photo') @WizardStep(6) async onPhoto( @Ctx() - ctx: WizardMessageContext, + ctx: RegisterWizardContext, @Message() msg: PhotoMessage, ): Promise { - const file = await fileFromMsg(ctx, msg); + const profile = await this.cache.get( + getProfileCacheKey(ctx.from.id), + ); - const fileId = await this.fileUseCases.upload(file.content, file.name); + const fileId = msg.photo.pop().file_id; const profileDto: CreateProfileDto = { - userId: ctx.from.id, - name: ctx.wizard.state.name, - age: ctx.wizard.state.age, - games: ctx.wizard.state.games, about: ctx.wizard.state.about, + age: ctx.wizard.state.age, fileId, + games: ctx.wizard.state.games, + name: ctx.wizard.state.name, + userId: ctx.from.id, }; - const profile = await this.profileUseCases.create(profileDto); + if (profile) { + await this.profileUseCases.update(profile.id, profileDto); - ctx.session.user.profile = profile; + await ctx.scene.enter(NEXT_WIZARD_ID); + + return; + } + + await this.profileUseCases.create(profileDto); await this.replyUseCases.replyI18n(ctx, 'messages.register.completed'); + await ctx.scene.enter(NEXT_WIZARD_ID); return; diff --git a/src/controllers/wizards/tests/change-lang.wizard.spec.ts b/src/controllers/wizards/tests/change-lang.wizard.spec.ts index 9da0135..6c79cfd 100644 --- a/src/controllers/wizards/tests/change-lang.wizard.spec.ts +++ b/src/controllers/wizards/tests/change-lang.wizard.spec.ts @@ -4,6 +4,7 @@ import { REGISTER_WIZARD_ID, SELECT_LANG_MARKUP } from 'src/core/constants'; import { Language } from 'src/core/enums'; import { WizardContext } from 'src/types'; import { ReplyUseCases } from 'src/use-cases/reply'; + import { ChangeLangWizard } from '../change-lang.wizard'; describe('ChangeLangWizard', () => { @@ -26,12 +27,12 @@ describe('ChangeLangWizard', () => { describe('onEnter', () => { it('should return the select language message with the select language markup if profile is undefined', async () => { const ctx = createMock({ - wizard: { - next: jest.fn(), - }, session: { user: {}, }, + wizard: { + next: jest.fn(), + }, }); const result = wizard.onEnter(ctx); @@ -45,14 +46,14 @@ describe('ChangeLangWizard', () => { it('should return the update language message with the select language markup', async () => { const ctx = createMock({ - wizard: { - next: jest.fn(), - }, session: { user: { profile: {}, }, }, + wizard: { + next: jest.fn(), + }, }); const result = wizard.onEnter(ctx); @@ -82,12 +83,12 @@ describe('ChangeLangWizard', () => { }, ].forEach(async (testCase) => { const ctx = createMock({ - session: { - user: {}, - }, scene: { - leave: jest.fn(), enter: jest.fn(), + leave: jest.fn(), + }, + session: { + user: {}, }, }); @@ -101,15 +102,15 @@ describe('ChangeLangWizard', () => { it('should not enter register scene if profile is defined', async () => { const ctx = createMock({ + scene: { + enter: jest.fn(), + leave: jest.fn(), + }, session: { user: { profile: {}, }, }, - scene: { - leave: jest.fn(), - enter: jest.fn(), - }, }); const msg = { text: '🇺🇦' }; @@ -121,10 +122,10 @@ describe('ChangeLangWizard', () => { it('should return the invalid lang message for an invalid language', async () => { const ctx = createMock({ - session: {}, scene: { leave: jest.fn(), }, + session: {}, }); const msg = { text: 'invalid' }; diff --git a/src/controllers/wizards/tests/next.wizard.spec.ts b/src/controllers/wizards/tests/next.wizard.spec.ts index 43396f0..fac29c8 100644 --- a/src/controllers/wizards/tests/next.wizard.spec.ts +++ b/src/controllers/wizards/tests/next.wizard.spec.ts @@ -2,6 +2,7 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { MAIN_MENU_MARKUP } from 'src/core/constants'; import { MessageContext } from 'src/types'; + import { NextActionWizard } from '../next.wizard'; describe('NextActionWizard', () => { diff --git a/src/controllers/wizards/tests/profiles.wizard.spec.ts b/src/controllers/wizards/tests/profiles.wizard.spec.ts index 1a61f73..b1621f3 100644 --- a/src/controllers/wizards/tests/profiles.wizard.spec.ts +++ b/src/controllers/wizards/tests/profiles.wizard.spec.ts @@ -12,6 +12,7 @@ import { ProfilesMessageContext } from 'src/types'; import { ProfileUseCases } from 'src/use-cases/profile'; import { deunionize } from 'telegraf'; import { ChatFromGetChat } from 'telegraf/typings/core/types/typegram'; + import { ProfilesWizard } from '../profiles.wizard'; jest.mock('telegraf', () => { @@ -52,12 +53,8 @@ describe('ProfilesWizard', () => { const ctx = createMock({ scene: { - leave: jest.fn(), enter: jest.fn(), - }, - wizard: { - state: {}, - next: jest.fn(), + leave: jest.fn(), }, session: { user: createMock({ @@ -72,14 +69,18 @@ describe('ProfilesWizard', () => { .fn() .mockResolvedValueOnce(createMock()), }, + wizard: { + next: jest.fn(), + state: {}, + }, }); const recommended = createMock({ - id: 1, - name: 'test', file: createMock({ url: 'https://test.com', }), games: [], + id: 1, + name: 'test', }); const findSpy = jest @@ -105,8 +106,8 @@ describe('ProfilesWizard', () => { it('should leave the scene if message text equals LEAVE_PROFILES_CALLBACK', async () => { const ctx = createMock({ scene: { - leave: jest.fn(), enter: jest.fn(), + leave: jest.fn(), }, }); const msg = { @@ -122,8 +123,8 @@ describe('ProfilesWizard', () => { it('should re-enter the scene if message text equals NEXT_PROFILE_CALLBACK', async () => { const ctx = createMock({ scene: { - leave: jest.fn(), enter: jest.fn(), + leave: jest.fn(), }, }); const msg = { diff --git a/src/controllers/wizards/tests/register.wizard.spec.ts b/src/controllers/wizards/tests/register.wizard.spec.ts index 15627ce..2fa3ef4 100644 --- a/src/controllers/wizards/tests/register.wizard.spec.ts +++ b/src/controllers/wizards/tests/register.wizard.spec.ts @@ -77,12 +77,12 @@ describe('RegisterWizard', () => { describe('onEnter', () => { it('should call enterName on the reply use cases and go to the next step', async () => { const ctx = createMock({ - wizard: { - next: jest.fn(), - }, from: { first_name: 'John', }, + wizard: { + next: jest.fn(), + }, }); const resp = await wizard.onEnter(ctx); @@ -102,8 +102,8 @@ describe('RegisterWizard', () => { it('should set the name state and go to the next step', async () => { const ctx = createMock({ wizard: { - state: {}, next: jest.fn(), + state: {}, }, }); const msg = { text: 'name' }; @@ -123,8 +123,8 @@ describe('RegisterWizard', () => { it('should call enterAge on the reply use cases and go to the next step', async () => { const ctx = createMock({ wizard: { - state: {}, next: jest.fn(), + state: {}, }, }); @@ -138,8 +138,8 @@ describe('RegisterWizard', () => { it('should return error message if age is not a number', async () => { const ctx = createMock({ wizard: { - state: {}, next: jest.fn(), + state: {}, }, }); @@ -154,11 +154,11 @@ describe('RegisterWizard', () => { describe('onAbout', () => { it('should set the about state and go to the next step', async () => { const ctx = createMock({ + me: 'test', wizard: { next: jest.fn(), state: {}, }, - me: 'test', }); const msg = { text: 'Kharkiv' }; const resp = await wizard.onAbout(ctx, msg); @@ -168,10 +168,10 @@ describe('RegisterWizard', () => { expect(resp).toEqual([ 'messages.game.send', { - reply_markup: GAMES_MARKUP, i18nArgs: { username: ctx.me, }, + reply_markup: GAMES_MARKUP, }, ]); }); @@ -181,8 +181,8 @@ describe('RegisterWizard', () => { it('should set the games state', async () => { const ctx = createMock({ wizard: { - state: {}, next: jest.fn(), + state: {}, }, }); @@ -203,8 +203,8 @@ describe('RegisterWizard', () => { it('should throw an error if the game is not in the list', async () => { const ctx = createMock({ wizard: { - state: {}, next: jest.fn(), + state: {}, }, }); const msg = { text: 'game_that_does_not_exist' }; @@ -224,10 +224,10 @@ describe('RegisterWizard', () => { it('should send error message if game is already in state', async () => { const ctx = createMock({ wizard: { + next: jest.fn(), state: { games: [1], }, - next: jest.fn(), }, }); const msg = { text: 'game1' }; @@ -247,8 +247,8 @@ describe('RegisterWizard', () => { it('should go to the next step if the message is ✅', async () => { const ctx = createMock({ wizard: { - state: {}, next: jest.fn(), + state: {}, }, }); const msg = { text: '✅' }; @@ -269,37 +269,37 @@ describe('RegisterWizard', () => { describe('onPhoto', () => { it('should call enterPicture on the reply use cases and go to the next step', async () => { const ctx = createMock({ - wizard: { - state: { - name: 'test', - age: 17, - about: 'test', - games: [1], - }, - next: jest.fn(), - }, from: { id: 12345, }, + scene: { + enter: jest.fn(), + }, session: { user: createMock(), }, - scene: { - enter: jest.fn(), + wizard: { + next: jest.fn(), + state: { + about: 'test', + age: 17, + games: [1], + name: 'test', + }, }, }); const profileDto = { - name: ctx.wizard.state.name, - age: ctx.wizard.state.age, about: ctx.wizard.state.about, + age: ctx.wizard.state.age, + fileId: 1, games: ctx.wizard.state.games, + name: ctx.wizard.state.name, userId: ctx.from.id, - fileId: 1, }; const createdProfile = createMock({ + age: 17, id: 1, name: 'test', - age: 17, }); const uploadSpy = jest diff --git a/src/core/abstracts/ad.abstract.service.ts b/src/core/abstracts/ad.abstract.service.ts new file mode 100644 index 0000000..d17853d --- /dev/null +++ b/src/core/abstracts/ad.abstract.service.ts @@ -0,0 +1,7 @@ +import { Ad } from '../entities/ad.entity'; + +export abstract class IAdService { + abstract create(ad: Ad): Promise; + + abstract findOne(seenProfiles: number[]): Promise; +} diff --git a/src/core/abstracts/game.abstract.service.ts b/src/core/abstracts/game.abstract.service.ts index 54bb569..a06c02e 100644 --- a/src/core/abstracts/game.abstract.service.ts +++ b/src/core/abstracts/game.abstract.service.ts @@ -3,11 +3,11 @@ import { Game } from '../entities'; abstract class IGameService { abstract findAll(): Promise; - abstract findWithLimit(limit: number): Promise; + abstract findByTitle(title: string): Promise; abstract findStartsWith(title: string): Promise; - abstract findByTitle(title: string): Promise; + abstract findWithLimit(limit: number): Promise; } export { IGameService }; diff --git a/src/core/abstracts/index.ts b/src/core/abstracts/index.ts index dc6b2b8..5cf894f 100644 --- a/src/core/abstracts/index.ts +++ b/src/core/abstracts/index.ts @@ -2,4 +2,6 @@ export * from './file.abstract.service'; export * from './game.abstract.service'; export * from './profile.abstract.service'; export * from './reply.abstract.service'; +export * from './reports.abstract.service'; export * from './user.abstract.service'; +export * from './ad.abstract.service'; diff --git a/src/core/abstracts/profile.abstract.service.ts b/src/core/abstracts/profile.abstract.service.ts index a0ab374..2ed9777 100644 --- a/src/core/abstracts/profile.abstract.service.ts +++ b/src/core/abstracts/profile.abstract.service.ts @@ -3,13 +3,20 @@ import { Profile } from '../entities'; abstract class IProfileService { abstract createProfile(profile: Profile): Promise; - abstract updateProfile(profileId: number, profile: Profile): Promise; + abstract delete(profileId: number): Promise; + + abstract deleteByUser(userId: number): Promise; - abstract deleteProfile(profileId: number): Promise; + abstract findById(profileId: number): Promise; abstract findByUser(userId: number): Promise; - abstract findRecommended(user: Profile): Promise; + abstract findRecommended( + user: Profile, + seenProfiles: number[], + ): Promise; + + abstract updateProfile(profileId: number, profile: Profile): Promise; } export { IProfileService }; diff --git a/src/core/abstracts/reply.abstract.service.ts b/src/core/abstracts/reply.abstract.service.ts index 6e78ab1..840733d 100644 --- a/src/core/abstracts/reply.abstract.service.ts +++ b/src/core/abstracts/reply.abstract.service.ts @@ -1,29 +1,40 @@ import { PathImpl2 } from '@nestjs/config'; import { I18nService } from 'nestjs-i18n'; import { I18nTranslations } from 'src/generated/i18n.generated'; +import { Extra, I18nArgs, Language, PhotoExtra } from 'src/types'; import { Context } from 'telegraf'; -import { Language } from '../enums/languages.enum'; -import { Extra, I18nArgs } from '../types'; abstract class IReplyService { constructor(protected readonly i18n: I18nService) {} - abstract reply( - ctx: Context, - msgCode: PathImpl2, - args?: Extra, - ): Promise; - - protected translate( + public translate( key: PathImpl2, lang: Language, args?: I18nArgs, ): string { return this.i18n.t(key, { - lang, args, + lang, }); } + + abstract reply( + ctx: Context, + msgCode: PathImpl2, + args?: Extra, + ): Promise; + + abstract sendMsgToChat( + chatId: number, + msg: string, + args?: Extra, + ): Promise; + + abstract sendPhotoToChat( + chatId: number, + fileId: string, + args?: PhotoExtra, + ): Promise; } export { IReplyService }; diff --git a/src/core/abstracts/reports.abstract.service.ts b/src/core/abstracts/reports.abstract.service.ts new file mode 100644 index 0000000..f1f52dc --- /dev/null +++ b/src/core/abstracts/reports.abstract.service.ts @@ -0,0 +1,17 @@ +import { Report, ReportsChannel } from '../entities'; + +abstract class IReportService { + abstract createReport(report: Report): Promise; + + abstract createReportsChannel( + reportChannel: ReportsChannel, + ): Promise; + + abstract deleteReport(id: number): Promise; + + abstract findById(id: number): Promise; + + abstract findReportsChannel(): Promise; +} + +export { IReportService }; diff --git a/src/core/abstracts/user.abstract.service.ts b/src/core/abstracts/user.abstract.service.ts index be87a51..07f4b1e 100644 --- a/src/core/abstracts/user.abstract.service.ts +++ b/src/core/abstracts/user.abstract.service.ts @@ -3,9 +3,9 @@ import { User } from '../entities'; abstract class IUserService { abstract create(user: User): Promise; - abstract update(userId: number, user: User): Promise; + abstract findById(userId: number): Promise; - abstract findById(userId: number): Promise; + abstract update(userId: number, user: User): Promise; } export { IUserService }; diff --git a/src/core/constants/callbacks.ts b/src/core/constants/callbacks.ts index ac7ae9f..31f432e 100644 --- a/src/core/constants/callbacks.ts +++ b/src/core/constants/callbacks.ts @@ -14,7 +14,20 @@ const TEXT_CALLBACK = '✉️'; const LEAVE_PROFILES_CALLBACK = '❌'; +const AD_CALLBACK = '🔎'; + +const REPORT_CALLBACK = '❗️'; + +const CLEAR_LAST_YES_CALLBACK = '✅'; + +const CLEAR_LAST_NO_CALLBACK = '❌'; + +const UPDATE_PROFILE_CALLBACK = 'update-profile'; + export { + AD_CALLBACK, + CLEAR_LAST_NO_CALLBACK, + CLEAR_LAST_YES_CALLBACK, COOP_CALLBACK, HELP_CALLBACK, LANG_CALLBACK, @@ -22,5 +35,7 @@ export { LOOK_CALLBACK, NEXT_PROFILE_CALLBACK, PROFILE_CALLBACK, + REPORT_CALLBACK, TEXT_CALLBACK, + UPDATE_PROFILE_CALLBACK, }; diff --git a/src/core/constants/index.ts b/src/core/constants/index.ts index 615f682..6d5c7e3 100644 --- a/src/core/constants/index.ts +++ b/src/core/constants/index.ts @@ -1,3 +1,4 @@ export * from './callbacks'; -export * from './markups'; +export * from './keyboards'; +export * from './tokens'; export * from './wizards'; diff --git a/src/core/constants/keyboards.ts b/src/core/constants/keyboards.ts new file mode 100644 index 0000000..93c7ec3 --- /dev/null +++ b/src/core/constants/keyboards.ts @@ -0,0 +1,57 @@ +import { Markup } from 'telegraf'; + +import { + CLEAR_LAST_NO_CALLBACK, + CLEAR_LAST_YES_CALLBACK, + COOP_CALLBACK, + HELP_CALLBACK, + LANG_CALLBACK, + LEAVE_PROFILES_CALLBACK, + LOOK_CALLBACK, + NEXT_PROFILE_CALLBACK, + PROFILE_CALLBACK, + REPORT_CALLBACK, +} from './callbacks'; + +export class Keyboards { + static clearLast = Markup.keyboard([ + [ + Markup.button.callback(CLEAR_LAST_YES_CALLBACK, CLEAR_LAST_YES_CALLBACK), + Markup.button.callback(CLEAR_LAST_NO_CALLBACK, CLEAR_LAST_NO_CALLBACK), + ], + ]).resize(true).reply_markup; + + static games = Markup.keyboard([[Markup.button.callback('✅', '✅')]]).resize( + true, + ).reply_markup; + + static mainMenu = Markup.keyboard([ + [ + Markup.button.callback(PROFILE_CALLBACK, PROFILE_CALLBACK), + Markup.button.callback(LANG_CALLBACK, LANG_CALLBACK), + Markup.button.callback(LOOK_CALLBACK, LOOK_CALLBACK), + Markup.button.callback(COOP_CALLBACK, COOP_CALLBACK), + Markup.button.callback(HELP_CALLBACK, HELP_CALLBACK), + ], + ]).resize(true).reply_markup; + + static profiles = Markup.keyboard([ + [ + Markup.button.callback(NEXT_PROFILE_CALLBACK, NEXT_PROFILE_CALLBACK), + Markup.button.callback(LEAVE_PROFILES_CALLBACK, LEAVE_PROFILES_CALLBACK), + Markup.button.callback(REPORT_CALLBACK, REPORT_CALLBACK), + ], + ]).resize(true).reply_markup; + + static remove = Markup.removeKeyboard().reply_markup; + + static selectLang = Markup.keyboard([ + [ + Markup.button.callback('🇺🇦', 'lang_ua'), + Markup.button.callback('🇬🇧', 'lang_en'), + Markup.button.callback('🇷🇺', 'lang_ru'), + ], + ]) + .resize(true) + .oneTime(true).reply_markup; +} diff --git a/src/core/constants/markups/games.ts b/src/core/constants/markups/games.ts deleted file mode 100644 index c154f7e..0000000 --- a/src/core/constants/markups/games.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Markup } from 'telegraf'; - -const GAMES_MARKUP = Markup.keyboard([ - [Markup.button.callback('✅', '✅')], -]).resize(true).reply_markup; - -export { GAMES_MARKUP }; diff --git a/src/core/constants/markups/index.ts b/src/core/constants/markups/index.ts deleted file mode 100644 index e6bd924..0000000 --- a/src/core/constants/markups/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './games'; -export * from './main-menu'; -export * from './profiles'; -export * from './remove'; -export * from './select-lang'; diff --git a/src/core/constants/markups/main-menu.ts b/src/core/constants/markups/main-menu.ts deleted file mode 100644 index ea017da..0000000 --- a/src/core/constants/markups/main-menu.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Markup } from 'telegraf'; -import { - COOP_CALLBACK, - HELP_CALLBACK, - LANG_CALLBACK, - LOOK_CALLBACK, - PROFILE_CALLBACK, -} from '../callbacks'; - -// init main menu markup -const MAIN_MENU_MARKUP = Markup.keyboard([ - [ - Markup.button.callback(PROFILE_CALLBACK, PROFILE_CALLBACK), - Markup.button.callback(LANG_CALLBACK, LANG_CALLBACK), - Markup.button.callback(LOOK_CALLBACK, LOOK_CALLBACK), - Markup.button.callback(COOP_CALLBACK, COOP_CALLBACK), - Markup.button.callback(HELP_CALLBACK, HELP_CALLBACK), - ], -]).resize(true).reply_markup; - -export { MAIN_MENU_MARKUP }; diff --git a/src/core/constants/markups/profiles.ts b/src/core/constants/markups/profiles.ts deleted file mode 100644 index a09ecd4..0000000 --- a/src/core/constants/markups/profiles.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Markup } from 'telegraf'; -import { LEAVE_PROFILES_CALLBACK, NEXT_PROFILE_CALLBACK } from '../callbacks'; - -const PROFILES_MARKUP = Markup.keyboard([ - [ - Markup.button.callback(NEXT_PROFILE_CALLBACK, NEXT_PROFILE_CALLBACK), - Markup.button.callback(LEAVE_PROFILES_CALLBACK, LEAVE_PROFILES_CALLBACK), - ], -]).resize(true).reply_markup; - -export { PROFILES_MARKUP }; diff --git a/src/core/constants/markups/remove.ts b/src/core/constants/markups/remove.ts deleted file mode 100644 index 47ad539..0000000 --- a/src/core/constants/markups/remove.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Markup } from 'telegraf'; - -const REMOVE_KEYBOARD_MARKUP = Markup.removeKeyboard().reply_markup; - -export { REMOVE_KEYBOARD_MARKUP }; diff --git a/src/core/constants/markups/select-lang.ts b/src/core/constants/markups/select-lang.ts deleted file mode 100644 index 121a89d..0000000 --- a/src/core/constants/markups/select-lang.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Markup } from 'telegraf'; - -const SELECT_LANG_MARKUP = Markup.keyboard([ - [ - Markup.button.callback('🇺🇦', 'lang_ua'), - Markup.button.callback('🇬🇧', 'lang_en'), - Markup.button.callback('🇷🇺', 'lang_ru'), - ], -]) - .resize(true) - .oneTime(true).reply_markup; - -export { SELECT_LANG_MARKUP }; diff --git a/src/core/constants/tokens.ts b/src/core/constants/tokens.ts new file mode 100644 index 0000000..3316126 --- /dev/null +++ b/src/core/constants/tokens.ts @@ -0,0 +1,5 @@ +const SESSION_STORE = 'SESSION_STORE'; + +const REDIS_CLIENT = 'REDIS_CLIENT'; + +export { REDIS_CLIENT, SESSION_STORE }; diff --git a/src/core/constants/wizards.ts b/src/core/constants/wizards.ts index 03adf71..df6926c 100644 --- a/src/core/constants/wizards.ts +++ b/src/core/constants/wizards.ts @@ -6,8 +6,11 @@ const NEXT_WIZARD_ID = 'next_wizard'; const PROFILES_WIZARD_ID = 'profiles_scene'; +const CLEAR_LAST_WIZARD_ID = 'clear_last_profiles_wizard'; + export { CHANGE_LANG_WIZARD_ID, + CLEAR_LAST_WIZARD_ID, NEXT_WIZARD_ID, PROFILES_WIZARD_ID, REGISTER_WIZARD_ID, diff --git a/src/core/decorators/index.ts b/src/core/decorators/index.ts new file mode 100644 index 0000000..8974ca7 --- /dev/null +++ b/src/core/decorators/index.ts @@ -0,0 +1 @@ +export * from './roles.decorator'; diff --git a/src/core/decorators/roles.decorator.ts b/src/core/decorators/roles.decorator.ts new file mode 100644 index 0000000..b69b373 --- /dev/null +++ b/src/core/decorators/roles.decorator.ts @@ -0,0 +1,3 @@ +import { Reflector } from '@nestjs/core'; + +export const Roles = Reflector.createDecorator(); diff --git a/src/core/dtos/ad.dto.ts b/src/core/dtos/ad.dto.ts new file mode 100644 index 0000000..ac4d43a --- /dev/null +++ b/src/core/dtos/ad.dto.ts @@ -0,0 +1,7 @@ +class CreateAdDto { + description: string; + fileId: string; + url: string; +} + +export { CreateAdDto }; diff --git a/src/core/dtos/index.ts b/src/core/dtos/index.ts index f7b0a05..8ed343e 100644 --- a/src/core/dtos/index.ts +++ b/src/core/dtos/index.ts @@ -1,2 +1,3 @@ export * from './profile.dto'; +export * from './report.dto'; export * from './user.dto'; diff --git a/src/core/dtos/profile.dto.ts b/src/core/dtos/profile.dto.ts index a9d211c..8fe769c 100644 --- a/src/core/dtos/profile.dto.ts +++ b/src/core/dtos/profile.dto.ts @@ -1,17 +1,17 @@ import { partial } from '../utils'; class CreateProfileDto { - userId: number; - - name: string; + about: string; age: number; - about: string; + fileId: string; games: number[]; - fileId: number; + name: string; + + userId: number; } class UpdateProfileDto extends partial>() {} diff --git a/src/core/dtos/report.dto.ts b/src/core/dtos/report.dto.ts new file mode 100644 index 0000000..824faa1 --- /dev/null +++ b/src/core/dtos/report.dto.ts @@ -0,0 +1,22 @@ +import { partial } from '../utils'; + +class CreateReportDto { + description: string; + reporterId: number; + userId: number; +} + +class UpdateReportDto extends partial>() {} + +class CreateReportsChannelDto { + id: number; +} + +class UpdateReportsChannelDto extends CreateReportsChannelDto {} + +export { + CreateReportDto, + CreateReportsChannelDto, + UpdateReportDto, + UpdateReportsChannelDto, +}; diff --git a/src/core/entities/ad.entity.ts b/src/core/entities/ad.entity.ts new file mode 100644 index 0000000..cd8e063 --- /dev/null +++ b/src/core/entities/ad.entity.ts @@ -0,0 +1,15 @@ +import { Column, Entity } from 'typeorm'; + +import { IEntity } from './base.entity'; + +@Entity('ads') +export class Ad extends IEntity { + @Column() + description: string; + + @Column() + fileId: string; + + @Column() + url: string; +} diff --git a/src/core/entities/base.entity.ts b/src/core/entities/base.entity.ts index 1eea58c..c1bc82f 100644 --- a/src/core/entities/base.entity.ts +++ b/src/core/entities/base.entity.ts @@ -6,12 +6,12 @@ import { } from 'typeorm'; abstract class IEntity extends BaseEntity { - @PrimaryGeneratedColumn() - id: number; - @CreateDateColumn() created_at: Date; + @PrimaryGeneratedColumn() + id: number; + @UpdateDateColumn() updated_at: Date; } diff --git a/src/core/entities/file.entity.ts b/src/core/entities/file.entity.ts index 8ab17c3..9cc6b0b 100644 --- a/src/core/entities/file.entity.ts +++ b/src/core/entities/file.entity.ts @@ -1,13 +1,14 @@ import { Column, Entity } from 'typeorm'; + import { IEntity } from './base.entity'; @Entity('images') class File extends IEntity { @Column() - url: string; + key: string; @Column() - key: string; + url: string; } export { File }; diff --git a/src/core/entities/game.entity.ts b/src/core/entities/game.entity.ts index 9c06174..4d0daed 100644 --- a/src/core/entities/game.entity.ts +++ b/src/core/entities/game.entity.ts @@ -1,12 +1,10 @@ import { Column, Entity, ManyToMany } from 'typeorm'; + import { IEntity } from './base.entity'; import { Profile } from './profile.entity'; @Entity('games') class Game extends IEntity { - @Column() - title: string; - @Column() description: string; @@ -15,6 +13,9 @@ class Game extends IEntity { @ManyToMany(() => Profile, (profile) => profile.games) profiles: Profile[]; + + @Column() + title: string; } export { Game }; diff --git a/src/core/entities/index.ts b/src/core/entities/index.ts index bee6b8e..8bcfd95 100644 --- a/src/core/entities/index.ts +++ b/src/core/entities/index.ts @@ -2,4 +2,7 @@ export * from './base.entity'; export * from './file.entity'; export * from './game.entity'; export * from './profile.entity'; +export * from './report.entity'; +export * from './reports-channel.entity'; export * from './user.entity'; +export * from './ad.entity'; diff --git a/src/core/entities/profile.entity.ts b/src/core/entities/profile.entity.ts index 3b2e853..2344f9d 100644 --- a/src/core/entities/profile.entity.ts +++ b/src/core/entities/profile.entity.ts @@ -6,32 +6,34 @@ import { ManyToMany, OneToOne, } from 'typeorm'; + import { IEntity } from './base.entity'; -import { File } from './file.entity'; import { Game } from './game.entity'; import { User } from './user.entity'; @Entity('profiles') class Profile extends IEntity { @Column() - name: string; + about: string; @Column() age: number; - @Column() - about: string; - - @OneToOne(() => User, (user) => user.profile) - user: User; + @Column({ + name: 'file_id', + }) + fileId: string; @JoinTable() @ManyToMany(() => Game, (game) => game.profiles) games: Game[]; + @Column() + name: string; + @JoinColumn() - @OneToOne(() => File) - file: File; + @OneToOne(() => User, (user) => user.profile) + user: User; } export { Profile }; diff --git a/src/core/entities/report.entity.ts b/src/core/entities/report.entity.ts new file mode 100644 index 0000000..3212600 --- /dev/null +++ b/src/core/entities/report.entity.ts @@ -0,0 +1,16 @@ +import { Column, Entity, ManyToOne } from 'typeorm'; + +import { IEntity } from './base.entity'; +import { User } from './user.entity'; + +@Entity() +export class Report extends IEntity { + @Column() + description: string; + + @ManyToOne(() => User, (user) => user.reported) + reporter: User; + + @ManyToOne(() => User, (user) => user.reports) + user: User; +} diff --git a/src/core/entities/reports-channel.entity.ts b/src/core/entities/reports-channel.entity.ts new file mode 100644 index 0000000..d576f10 --- /dev/null +++ b/src/core/entities/reports-channel.entity.ts @@ -0,0 +1,9 @@ +import { BaseEntity, Entity, PrimaryColumn } from 'typeorm'; + +@Entity() +export class ReportsChannel extends BaseEntity { + @PrimaryColumn({ + type: 'bigint', + }) + id: number; +} diff --git a/src/core/entities/user.entity.ts b/src/core/entities/user.entity.ts index b5d0a6f..31f21d1 100644 --- a/src/core/entities/user.entity.ts +++ b/src/core/entities/user.entity.ts @@ -1,11 +1,14 @@ import { BaseEntity, + Column, Entity, - JoinColumn, + OneToMany, OneToOne, PrimaryColumn, } from 'typeorm'; + import { Profile } from './profile.entity'; +import { Report } from './report.entity'; @Entity('users') class User extends BaseEntity { @@ -14,12 +17,23 @@ class User extends BaseEntity { }) id: number; - @JoinColumn() @OneToOne(() => Profile, (profile) => profile.user, { - nullable: true, cascade: true, + nullable: true, }) profile: Profile; + + @OneToMany(() => Report, (report) => report.reporter) + reported: Report[]; + + @OneToMany(() => Report, (report) => report.user) + reports: Report[]; + + @Column({ + default: 'user', + enum: ['user', 'admin'], + }) + role: 'admin' | 'user'; } export { User }; diff --git a/src/core/enums/index.ts b/src/core/enums/index.ts deleted file mode 100644 index ba7bd62..0000000 --- a/src/core/enums/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './languages.enum'; diff --git a/src/core/enums/languages.enum.ts b/src/core/enums/languages.enum.ts deleted file mode 100644 index 9270b19..0000000 --- a/src/core/enums/languages.enum.ts +++ /dev/null @@ -1,7 +0,0 @@ -enum Language { - UA = 'ua', - RU = 'ru', - EN = 'en', -} - -export { Language }; diff --git a/src/core/filters/global.filter.ts b/src/core/filters/global.filter.ts index 53babcd..60ccf40 100644 --- a/src/core/filters/global.filter.ts +++ b/src/core/filters/global.filter.ts @@ -3,6 +3,7 @@ import { TelegrafArgumentsHost } from 'nestjs-telegraf'; import { MsgKey } from 'src/types'; import { ReplyUseCases } from 'src/use-cases/reply'; import { Context } from 'telegraf'; + import { BotException } from '../errors'; @Catch() @@ -17,10 +18,10 @@ export class GlobalFilter implements ExceptionFilter { if (exception instanceof BotException) { message = exception.message as MsgKey; + } else { + console.error(exception); } - console.error(exception.message); - await this.replyUseCases.replyI18n(ctx, message); } } diff --git a/src/core/filters/tests/global.filter.spec.ts b/src/core/filters/tests/global.filter.spec.ts index d0d5fa4..0126137 100644 --- a/src/core/filters/tests/global.filter.spec.ts +++ b/src/core/filters/tests/global.filter.spec.ts @@ -4,6 +4,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BotException } from 'src/core/errors'; import { MessageContext } from 'src/types'; import { ReplyUseCases } from 'src/use-cases/reply'; + import { GlobalFilter } from '../global.filter'; describe('GlobalFilter', () => { diff --git a/src/core/guards/admin.guard.ts b/src/core/guards/admin.guard.ts new file mode 100644 index 0000000..fd3a0e9 --- /dev/null +++ b/src/core/guards/admin.guard.ts @@ -0,0 +1,29 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { TelegrafExecutionContext } from 'nestjs-telegraf'; +import { MessageContext } from 'src/types'; +import { UserUseCases } from 'src/use-cases/user'; + +import { Roles } from '../decorators'; + +@Injectable() +export class RoleGuard implements CanActivate { + constructor( + private readonly userUseCases: UserUseCases, + private readonly reflector: Reflector, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const roles = this.reflector.get(Roles, context.getHandler()); + if (!roles || roles.length === 0) { + return true; + } + + const ctx = TelegrafExecutionContext.create(context); + const { from } = ctx.getContext(); + + const user = await this.userUseCases.findById(from.id); + + return roles.includes(user.role); + } +} diff --git a/src/core/guards/index.ts b/src/core/guards/index.ts new file mode 100644 index 0000000..ce1dc62 --- /dev/null +++ b/src/core/guards/index.ts @@ -0,0 +1 @@ +export * from './admin.guard'; diff --git a/src/core/interceptors/cache.interceptor.ts b/src/core/interceptors/cache.interceptor.ts new file mode 100644 index 0000000..c2e7dd4 --- /dev/null +++ b/src/core/interceptors/cache.interceptor.ts @@ -0,0 +1,51 @@ +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { + CallHandler, + ExecutionContext, + Inject, + Injectable, + Logger, + NestInterceptor, +} from '@nestjs/common'; +import { Cache } from 'cache-manager'; +import { TelegrafExecutionContext } from 'nestjs-telegraf'; +import { MessageContext } from 'src/types'; +import { UserUseCases } from 'src/use-cases/user'; + +import { Profile } from '../entities'; +import { getProfileCacheKey } from '../utils'; + +// TODO: store language in database +@Injectable() +export class CacheInterceptor implements NestInterceptor { + private readonly logger: Logger = new Logger(CacheInterceptor.name); + + constructor( + private readonly userUseCases: UserUseCases, + @Inject(CACHE_MANAGER) private readonly cache: Cache, + ) {} + + async intercept(ctx: ExecutionContext, next: CallHandler) { + const tgExecutionContext = TelegrafExecutionContext.create(ctx); + const tgCtx = tgExecutionContext.getContext(); + const profileKey = getProfileCacheKey(tgCtx.from.id); + + const profile = await this.cache.get(profileKey); + if (!profile) { + console.log('profile is null'); + const user = await this.userUseCases.findById(tgCtx.from.id); + if (!user) { + console.log('created user'); + await this.userUseCases.create({ + id: tgCtx.from.id, + }); + + return next.handle(); + } + + await this.cache.set(profileKey, user.profile); + } + + return next.handle(); + } +} diff --git a/src/core/interceptors/context.interceptor.ts b/src/core/interceptors/context.interceptor.ts deleted file mode 100644 index 9e004bc..0000000 --- a/src/core/interceptors/context.interceptor.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - CallHandler, - ExecutionContext, - Injectable, - NestInterceptor, -} from '@nestjs/common'; -import { TelegrafExecutionContext } from 'nestjs-telegraf'; -import { MessageContext } from 'src/types'; -import { UserUseCases } from 'src/use-cases/user'; - -@Injectable() -export class ContextInterceptor implements NestInterceptor { - constructor(private readonly userUseCases: UserUseCases) {} - - async intercept(ctx: ExecutionContext, next: CallHandler) { - const tgExecutionContext = TelegrafExecutionContext.create(ctx); - const tgCtx = tgExecutionContext.getContext(); - - if (!tgCtx.session.user) { - const user = await this.userUseCases.findById(tgCtx.from.id); - - if (user) { - tgCtx.session.user = user; - } else { - tgCtx.session.user = await this.userUseCases.create({ - id: tgCtx.from.id, - }); - } - } - - return next.handle(); - } -} diff --git a/src/core/interceptors/i18n.interceptor.ts b/src/core/interceptors/i18n.interceptor.ts index b4d6502..5cde42c 100644 --- a/src/core/interceptors/i18n.interceptor.ts +++ b/src/core/interceptors/i18n.interceptor.ts @@ -7,9 +7,8 @@ import { } from '@nestjs/common'; import { TelegrafExecutionContext } from 'nestjs-telegraf'; import { map } from 'rxjs'; -import { MessageContext, MsgKey } from 'src/types'; +import { Extra, MessageContext, MsgKey } from 'src/types'; import { ReplyUseCases } from 'src/use-cases/reply'; -import { Extra } from '../types'; @Injectable() export class I18nInterceptor implements NestInterceptor { @@ -17,7 +16,7 @@ export class I18nInterceptor implements NestInterceptor { constructor(private readonly replyUseCases: ReplyUseCases) {} - intercept(ctx: ExecutionContext, next: CallHandler) { + async intercept(ctx: ExecutionContext, next: CallHandler) { const tgExecutionContext = TelegrafExecutionContext.create(ctx); const tgCtx = tgExecutionContext.getContext(); diff --git a/src/core/interceptors/index.ts b/src/core/interceptors/index.ts index 3683aa1..364a796 100644 --- a/src/core/interceptors/index.ts +++ b/src/core/interceptors/index.ts @@ -1,2 +1,2 @@ -export * from './context.interceptor'; +export * from './cache.interceptor'; export * from './i18n.interceptor'; diff --git a/src/core/interceptors/tests/context.interceptor.spec.ts b/src/core/interceptors/tests/context.interceptor.spec.ts deleted file mode 100644 index 7e6442f..0000000 --- a/src/core/interceptors/tests/context.interceptor.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { ExecutionContext } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { TelegrafExecutionContext } from 'nestjs-telegraf'; -import { User } from 'src/core/entities'; -import { UserUseCases } from 'src/use-cases/user'; -import { ContextInterceptor } from '../context.interceptor'; - -describe('ContextInterceptor', () => { - let interceptor: ContextInterceptor; - let userUseCases: UserUseCases; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ContextInterceptor, - { - provide: UserUseCases, - useValue: createMock(), - }, - ], - }).compile(); - - interceptor = module.get(ContextInterceptor); - userUseCases = module.get(UserUseCases); - }); - - describe('intercept', () => { - it('should set the session user to an existing user and call next', async () => { - const next = { handle: jest.fn().mockReturnValue(Promise.resolve()) }; - const user = createMock({ - id: 1, - }); - - jest.spyOn(TelegrafExecutionContext, 'create').mockReturnValue( - createMock({ - getContext: jest.fn().mockReturnValue({ - from: { id: 12345 }, - session: { - user: undefined, - }, - }), - }), - ); - jest.spyOn(userUseCases, 'findById').mockResolvedValue(user); - - await interceptor.intercept(createMock(), next); - - expect(userUseCases.findById).toHaveBeenCalledWith(12345); - expect(next.handle).toHaveBeenCalled(); - }); - - it('should set the session user to a new user and call next for a private chat without an existing user', async () => { - const ctx = createMock({ - getArgByIndex: jest.fn().mockReturnValue({ - from: { id: 12345 }, - session: { user: undefined }, - }), - }); - const next = { handle: jest.fn().mockReturnValue(Promise.resolve()) }; - const user = createMock({ - id: 1, - }); - - jest.spyOn(TelegrafExecutionContext, 'create').mockReturnValue( - createMock({ - getContext: jest.fn().mockReturnValue({ - from: { id: 12345 }, - session: { - user: undefined, - }, - }), - }), - ); - jest.spyOn(userUseCases, 'findById').mockResolvedValue(undefined); - jest.spyOn(userUseCases, 'create').mockResolvedValue(user); - - await interceptor.intercept(ctx, next); - - expect(userUseCases.findById).toHaveBeenCalledWith(12345); - expect(userUseCases.create).toHaveBeenCalledWith({ - id: 12345, - }); - expect(next.handle).toHaveBeenCalled(); - }); - }); -}); diff --git a/src/core/interceptors/tests/i18n.interceptor.spec.ts b/src/core/interceptors/tests/i18n.interceptor.spec.ts index cf5e23a..d61ce0e 100644 --- a/src/core/interceptors/tests/i18n.interceptor.spec.ts +++ b/src/core/interceptors/tests/i18n.interceptor.spec.ts @@ -7,6 +7,7 @@ import { Extra } from 'src/core/types'; import { MessageContext, MsgKey, MsgWithExtra } from 'src/types'; import { ReplyUseCases } from 'src/use-cases/reply'; import { Markup } from 'telegraf'; + import { I18nInterceptor } from '../i18n.interceptor'; describe('I18nInterceptor', () => { @@ -53,15 +54,15 @@ describe('I18nInterceptor', () => { const response = interceptor.intercept(context, handler); response.subscribe({ + complete: () => { + expect(replySpy).toHaveBeenCalledTimes(1); + }, next: () => { expect(replyUseCases.replyI18n).toHaveBeenCalledWith( tgCtx, 'messages.test', ); }, - complete: () => { - expect(replySpy).toHaveBeenCalledTimes(1); - }, }); }); @@ -93,15 +94,15 @@ describe('I18nInterceptor', () => { const response = interceptor.intercept(context, handler); response.subscribe({ + complete: () => { + expect(replySpy).toHaveBeenCalledTimes(1); + }, next: () => { expect(replyUseCases.replyI18n).toHaveBeenCalledWith( {}, ...controllerResp, ); }, - complete: () => { - expect(replySpy).toHaveBeenCalledTimes(1); - }, }); }); diff --git a/src/core/pipes/about.pipe.ts b/src/core/pipes/about.pipe.ts new file mode 100644 index 0000000..d7fe9b1 --- /dev/null +++ b/src/core/pipes/about.pipe.ts @@ -0,0 +1,15 @@ +import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; + +import { BotException } from '../errors'; + +@Injectable() +export class AboutPipe implements PipeTransform { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + transform(value: any, metadata: ArgumentMetadata) { + if (value.length > 512) { + throw new BotException('errors.about.invalid'); + } + + return value; + } +} diff --git a/src/core/pipes/age.pipe.ts b/src/core/pipes/age.pipe.ts new file mode 100644 index 0000000..89b1e81 --- /dev/null +++ b/src/core/pipes/age.pipe.ts @@ -0,0 +1,20 @@ +import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; + +import { BotException } from '../errors'; + +@Injectable() +export class AgePipe implements PipeTransform { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + transform(value: any, metadata: ArgumentMetadata) { + const age = parseInt(value.text); + + if (isNaN(age) || age < 0) { + throw new BotException('messages.age.invalid'); + } + + return { + ...value, + text: age, + }; + } +} diff --git a/src/core/pipes/game.pipe.ts b/src/core/pipes/game.pipe.ts new file mode 100644 index 0000000..d72f7d8 --- /dev/null +++ b/src/core/pipes/game.pipe.ts @@ -0,0 +1,22 @@ +import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; +import { GameUseCases } from 'src/use-cases/game'; + +import { BotException } from '../errors'; + +@Injectable() +export class GamePipe implements PipeTransform { + constructor(private readonly gameUseCases: GameUseCases) {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async transform(value: any, metadata: ArgumentMetadata) { + const game = await this.gameUseCases.findByTitle(value.text); + if (!game) { + throw new BotException('messages.game.invalid'); + } + + return { + ...value, + gameId: game.id, + }; + } +} diff --git a/src/core/pipes/index.ts b/src/core/pipes/index.ts new file mode 100644 index 0000000..e7fc542 --- /dev/null +++ b/src/core/pipes/index.ts @@ -0,0 +1,3 @@ +export * from './about.pipe'; +export * from './age.pipe'; +export * from './game.pipe'; diff --git a/src/core/types/index.ts b/src/core/types/index.ts deleted file mode 100644 index 55d9899..0000000 --- a/src/core/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './telegraf'; diff --git a/src/core/types/telegraf.ts b/src/core/types/telegraf.ts deleted file mode 100644 index 7d49784..0000000 --- a/src/core/types/telegraf.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - ForceReply, - InlineKeyboardMarkup, - MessageEntity, - ParseMode, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, -} from 'telegraf/typings/core/types/typegram'; - -type I18nArgs = - | ( - | string - | { - [k: string]: any; - } - )[] - | { - [k: string]: any; - }; - -type Extra = Omit< - { - chat_id: string | number; - message_thread_id?: number; - text: string; - parse_mode?: ParseMode; - entities?: MessageEntity[]; - disable_web_page_preview?: boolean; - disable_notification?: boolean; - protect_content?: boolean; - reply_to_message_id?: number; - allow_sending_without_reply?: boolean; - reply_markup?: - | InlineKeyboardMarkup - | ReplyKeyboardMarkup - | ReplyKeyboardRemove - | ForceReply; - i18nArgs?: I18nArgs; - }, - 'chat_id' | 'text' ->; - -export type { Extra, I18nArgs }; diff --git a/src/core/utils/file-from-msg.ts b/src/core/utils/file-from-msg.ts index e83559d..5f40cc9 100644 --- a/src/core/utils/file-from-msg.ts +++ b/src/core/utils/file-from-msg.ts @@ -1,4 +1,5 @@ import { MessageContext, PhotoMessage } from 'src/types'; + import { fetchImage } from './fetch-image'; type File = { diff --git a/src/core/utils/get-cache-key.ts b/src/core/utils/get-cache-key.ts new file mode 100644 index 0000000..82fdafa --- /dev/null +++ b/src/core/utils/get-cache-key.ts @@ -0,0 +1,9 @@ +const getProfileCacheKey = (userId: number) => { + return `profile:${userId}`; +}; + +const getImageCacheKey = (userId: number) => { + return `image:${userId}`; +}; + +export { getImageCacheKey, getProfileCacheKey }; diff --git a/src/core/utils/get-caption.ts b/src/core/utils/get-caption.ts index 351b27f..17d9c9a 100644 --- a/src/core/utils/get-caption.ts +++ b/src/core/utils/get-caption.ts @@ -7,6 +7,13 @@ const getCaption = (profile: Profile) => { ); }; +const getReportCaption = (profile: Profile) => { + return ( + `New report:\n\n${profile.name}, ${profile.age}:\n\n${profile.about}\n\n` + + getGamesCaption(profile.games) + ); +}; + const getGamesCaption = (games: Game[]) => { return games.map((game) => getGameHashTag(game)).join(' '); }; @@ -14,4 +21,4 @@ const getGamesCaption = (games: Game[]) => { const getGameHashTag = (game: Game) => `#${game.title.replaceAll(/\s+|-|:/g, '_')}`; -export { getCaption }; +export { getCaption, getReportCaption }; diff --git a/src/core/utils/get-markup.ts b/src/core/utils/get-markup.ts index 58d1489..16408c5 100644 --- a/src/core/utils/get-markup.ts +++ b/src/core/utils/get-markup.ts @@ -3,7 +3,12 @@ import { InlineKeyboardMarkup, ReplyKeyboardMarkup, } from 'telegraf/typings/core/types/typegram'; -import { TEXT_CALLBACK } from '../constants'; + +import { + AD_CALLBACK, + TEXT_CALLBACK, + UPDATE_PROFILE_CALLBACK, +} from '../constants'; const getNameMarkup = (name: string): ReplyKeyboardMarkup => { const reply_markup = Markup.keyboard([ @@ -27,4 +32,38 @@ const getProfileMarkup = (url: string): InlineKeyboardMarkup => { return reply_markup; }; -export { getNameMarkup, getProfileMarkup }; +const getAdMarkup = (url?: string): InlineKeyboardMarkup => { + const markup = url ? [[Markup.button.url(AD_CALLBACK, url)]] : []; + + const reply_markup = Markup.inlineKeyboard(markup).reply_markup; + + return reply_markup; +}; + +const getReportMarkup = ( + userId: number, + reporterId: number, +): InlineKeyboardMarkup => { + const markup = Markup.inlineKeyboard([ + Markup.button.callback('Delete Profile', `sen-${userId}`), + Markup.button.callback('Reporter Info', `reporter-info-${reporterId}`), + ]).reply_markup; + + return markup; +}; + +const getMeMarkup = (text: string): InlineKeyboardMarkup => { + const markup = Markup.inlineKeyboard([ + Markup.button.callback(text, UPDATE_PROFILE_CALLBACK), + ]).reply_markup; + + return markup; +}; + +export { + getAdMarkup, + getMeMarkup, + getNameMarkup, + getProfileMarkup, + getReportMarkup, +}; diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 17b3b56..f25887e 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -1,5 +1,6 @@ export * from './fetch-image'; export * from './file-from-msg'; +export * from './get-cache-key'; export * from './get-caption'; export * from './get-markup'; export * from './partial'; diff --git a/src/core/utils/tests/fetch-image.spec.ts b/src/core/utils/tests/fetch-image.spec.ts index 054deb2..37a9688 100644 --- a/src/core/utils/tests/fetch-image.spec.ts +++ b/src/core/utils/tests/fetch-image.spec.ts @@ -1,5 +1,6 @@ import { createMock } from '@golevelup/ts-jest'; import fetch from 'node-fetch'; + import { fetchImage } from '../fetch-image'; jest.mock('node-fetch', () => { diff --git a/src/core/utils/tests/file-from-msg.spec.ts b/src/core/utils/tests/file-from-msg.spec.ts index 3c73299..0bc5b31 100644 --- a/src/core/utils/tests/file-from-msg.spec.ts +++ b/src/core/utils/tests/file-from-msg.spec.ts @@ -1,6 +1,7 @@ import { createMock } from '@golevelup/ts-jest'; import { MessageContext, PhotoMessage } from 'src/types'; import { PhotoSize } from 'telegraf/typings/core/types/typegram'; + import { fetchImage } from '../fetch-image'; import { fileFromMsg } from '../file-from-msg'; diff --git a/src/core/utils/tests/get-caption.spec.ts b/src/core/utils/tests/get-caption.spec.ts index dd72334..1348a9d 100644 --- a/src/core/utils/tests/get-caption.spec.ts +++ b/src/core/utils/tests/get-caption.spec.ts @@ -1,5 +1,6 @@ import { createMock } from '@golevelup/ts-jest'; import { Game, Profile } from 'src/core/entities'; + import { getCaption } from '../get-caption'; describe('getCaption', () => { @@ -9,10 +10,10 @@ describe('getCaption', () => { createMock({ title: 'Game 2' }), ]; const profile: Profile = createMock({ - name: 'Test', - age: 20, about: 'About Test', + age: 20, games, + name: 'Test', }); const result = getCaption(profile); diff --git a/src/frameworks/ad/typeorm/index.ts b/src/frameworks/ad/typeorm/index.ts new file mode 100644 index 0000000..6549cf1 --- /dev/null +++ b/src/frameworks/ad/typeorm/index.ts @@ -0,0 +1,2 @@ +export * from './typeorm-ad.module'; +export * from './typeorm-ad.service'; diff --git a/src/frameworks/ad/typeorm/typeorm-ad.module.ts b/src/frameworks/ad/typeorm/typeorm-ad.module.ts new file mode 100644 index 0000000..b53d5e1 --- /dev/null +++ b/src/frameworks/ad/typeorm/typeorm-ad.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { IAdService } from 'src/core/abstracts/ad.abstract.service'; +import { Ad } from 'src/core/entities'; + +import { TypeOrmAdService } from './typeorm-ad.service'; + +@Module({ + exports: [IAdService], + imports: [TypeOrmModule.forFeature([Ad])], + providers: [ + { + provide: IAdService, + useClass: TypeOrmAdService, + }, + ], +}) +export class TypeOrmAdModule {} diff --git a/src/frameworks/ad/typeorm/typeorm-ad.service.ts b/src/frameworks/ad/typeorm/typeorm-ad.service.ts new file mode 100644 index 0000000..05b23a5 --- /dev/null +++ b/src/frameworks/ad/typeorm/typeorm-ad.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { IAdService } from 'src/core/abstracts'; +import { Ad } from 'src/core/entities'; +import { In, Not, Repository } from 'typeorm'; + +// TODO: add expiration date for ads +@Injectable() +export class TypeOrmAdService implements IAdService { + constructor(@InjectRepository(Ad) private readonly adRepo: Repository) {} + + async create(ad: Ad): Promise { + return this.adRepo.save(ad); + } + + async findOne(seenProfiles: number[]): Promise { + return this.adRepo.findOne({ + where: { + id: Not(In(seenProfiles)), + }, + }); + } +} diff --git a/src/frameworks/file/aws/aws-file.module.ts b/src/frameworks/file/aws/aws-file.module.ts index 5c9ae36..446750f 100644 --- a/src/frameworks/file/aws/aws-file.module.ts +++ b/src/frameworks/file/aws/aws-file.module.ts @@ -4,9 +4,11 @@ import { ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { IFileService } from 'src/core/abstracts'; import { File } from 'src/core/entities'; + import { AwsFileService } from './aws-file.service'; @Module({ + exports: [IFileService], imports: [TypeOrmModule.forFeature([File])], providers: [ { @@ -14,21 +16,20 @@ import { AwsFileService } from './aws-file.service'; useClass: AwsFileService, }, { + inject: [ConfigService], provide: 'S3_CLIENT', useFactory: async ( configService: ConfigService, ): Promise => { return new S3({ - region: configService.get('AWS_REGION'), credentials: { accessKeyId: configService.get('AWS_ACCESS_KEY'), secretAccessKey: configService.get('AWS_SECRET_ACCESS_KEY'), }, + region: configService.get('AWS_REGION'), }); }, - inject: [ConfigService], }, ], - exports: [IFileService], }) export class AwsFileModule {} diff --git a/src/frameworks/file/aws/aws-file.service.ts b/src/frameworks/file/aws/aws-file.service.ts index bd4dc77..f8d3c88 100644 --- a/src/frameworks/file/aws/aws-file.service.ts +++ b/src/frameworks/file/aws/aws-file.service.ts @@ -22,9 +22,9 @@ export class AwsFileService implements IFileService { const uploadResult = (await new Upload({ client: this.s3Client, params: { - Bucket: this.configService.get('AWS_PUBLIC_BUCKET_NAME'), Body: blob, - Key: `${uuid()}-${filename}`, + Bucket: this.configService.get('AWS_PUBLIC_BUCKET_NAME'), + Key: `f-${uuid()}-${filename}.jpg`, }, }).done()) as CompleteMultipartUploadCommandOutput; diff --git a/src/frameworks/file/aws/tests/aws-file.service.spec.ts b/src/frameworks/file/aws/tests/aws-file.service.spec.ts index 7cab9c2..c4e2122 100644 --- a/src/frameworks/file/aws/tests/aws-file.service.spec.ts +++ b/src/frameworks/file/aws/tests/aws-file.service.spec.ts @@ -8,6 +8,7 @@ import { mocked } from 'jest-mock'; import { File } from 'src/core/entities'; import { BotException } from 'src/core/errors'; import { Repository } from 'typeorm'; + import { AwsFileService } from '../aws-file.service'; jest.mock('uuid', () => ({ diff --git a/src/frameworks/game/typeorm/tests/game.service.spec.ts b/src/frameworks/game/typeorm/tests/game.service.spec.ts index ad22cbc..3bc7343 100644 --- a/src/frameworks/game/typeorm/tests/game.service.spec.ts +++ b/src/frameworks/game/typeorm/tests/game.service.spec.ts @@ -1,8 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm'; +import { getRepositoryToken, TypeOrmModule } from '@nestjs/typeorm'; import { Game } from 'src/core/entities'; import { MockDatabaseModule } from 'src/services/mock-database/mock-database.module'; import { Repository } from 'typeorm'; + import { TypeOrmGameService } from '../typeorm-game.service'; describe('TypeOrmGameService', () => { diff --git a/src/frameworks/game/typeorm/typeorm-game.module.ts b/src/frameworks/game/typeorm/typeorm-game.module.ts index 5d152fc..d312896 100644 --- a/src/frameworks/game/typeorm/typeorm-game.module.ts +++ b/src/frameworks/game/typeorm/typeorm-game.module.ts @@ -2,9 +2,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { IGameService } from 'src/core/abstracts/game.abstract.service'; import { Game } from 'src/core/entities'; + import { TypeOrmGameService } from './typeorm-game.service'; @Module({ + exports: [IGameService], imports: [TypeOrmModule.forFeature([Game])], providers: [ { @@ -12,6 +14,5 @@ import { TypeOrmGameService } from './typeorm-game.service'; useClass: TypeOrmGameService, }, ], - exports: [IGameService], }) export class TypeOrmGameModule {} diff --git a/src/frameworks/game/typeorm/typeorm-game.service.ts b/src/frameworks/game/typeorm/typeorm-game.service.ts index 81ae40c..dfcfed0 100644 --- a/src/frameworks/game/typeorm/typeorm-game.service.ts +++ b/src/frameworks/game/typeorm/typeorm-game.service.ts @@ -18,12 +18,11 @@ export class TypeOrmGameService implements IGameService { }); } - async findWithLimit(limit: number): Promise { - return this.gameRepo.find({ - order: { - created_at: 'DESC', + async findByTitle(title: string): Promise { + return this.gameRepo.findOne({ + where: { + title, }, - take: limit, }); } @@ -41,11 +40,12 @@ export class TypeOrmGameService implements IGameService { .getMany(); } - async findByTitle(title: string): Promise { - return this.gameRepo.findOne({ - where: { - title, + async findWithLimit(limit: number): Promise { + return this.gameRepo.find({ + order: { + created_at: 'DESC', }, + take: limit, }); } } diff --git a/src/frameworks/profile/typeorm/tests/typeorm-profile.service.spec.ts b/src/frameworks/profile/typeorm/tests/typeorm-profile.service.spec.ts index 94627bf..c4a65f2 100644 --- a/src/frameworks/profile/typeorm/tests/typeorm-profile.service.spec.ts +++ b/src/frameworks/profile/typeorm/tests/typeorm-profile.service.spec.ts @@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Profile } from 'src/core/entities'; import { InsertResult, Repository } from 'typeorm'; + import { TypeOrmProfileService } from '../typeorm-profile.service'; describe('TypeOrmProfileService', () => { @@ -76,7 +77,7 @@ describe('TypeOrmProfileService', () => { const deleteSpy = jest.spyOn(repo, 'delete').mockResolvedValue(undefined); - await service.deleteProfile(profileId); + await service.delete(profileId); expect(deleteSpy).toHaveBeenCalledWith(profileId); }); diff --git a/src/frameworks/profile/typeorm/typeorm-profile.module.ts b/src/frameworks/profile/typeorm/typeorm-profile.module.ts index 5802073..83db3b8 100644 --- a/src/frameworks/profile/typeorm/typeorm-profile.module.ts +++ b/src/frameworks/profile/typeorm/typeorm-profile.module.ts @@ -1,12 +1,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { IProfileService } from 'src/core/abstracts'; -import { Profile } from 'src/core/entities'; +import { Profile, User } from 'src/core/entities'; + import { TypeOrmProfileService } from './typeorm-profile.service'; @Module({ - imports: [TypeOrmModule.forFeature([Profile])], exports: [IProfileService], + imports: [TypeOrmModule.forFeature([Profile, User])], providers: [ { provide: IProfileService, diff --git a/src/frameworks/profile/typeorm/typeorm-profile.service.ts b/src/frameworks/profile/typeorm/typeorm-profile.service.ts index 05b7826..50a48f4 100644 --- a/src/frameworks/profile/typeorm/typeorm-profile.service.ts +++ b/src/frameworks/profile/typeorm/typeorm-profile.service.ts @@ -11,56 +11,94 @@ export class TypeOrmProfileService implements IProfileService { private readonly profileRepo: Repository, ) {} - async findByUser(userId: number): Promise { + async createProfile(profile: Profile): Promise { + const res = await this.profileRepo.save(profile); + return this.profileRepo.findOne({ + relations: { + games: true, + }, where: { - user: { - id: userId, - }, + id: res.id, }, }); } - async createProfile(profile: Profile): Promise { - const res = await this.profileRepo.save(profile); + async delete(profileId: number): Promise { + const profile = await this.findById(profileId); + + await this.profileRepo.remove(profile); + } + + async deleteByUser(userId: number): Promise { + const profile = await this.findByUser(userId); + + await this.profileRepo.remove(profile); + } + + async findAd(): Promise { + const result = await this.profileRepo + .createQueryBuilder('profile') + .orderBy('RANDOM()') + .addOrderBy('profile.created_at', 'DESC') + .limit(1) + .getOne(); + + return result; + } + async findById(profileId: number): Promise { return this.profileRepo.findOne({ - where: { - id: res.id, - }, relations: { - file: true, games: true, + user: true, + }, + where: { + id: profileId, }, }); } - async updateProfile(profileId: number, profile: Profile): Promise { - return await this.profileRepo.save({ - id: profileId, - ...profile, + async findByUser(userId: number): Promise { + return this.profileRepo.findOne({ + relations: { + games: true, + user: true, + }, + where: { + user: { + id: userId, + }, + }, }); } - async deleteProfile(profileId: number): Promise { - await this.profileRepo.delete(profileId); - } - - async findRecommended(profile: Profile): Promise { + async findRecommended( + profile: Profile, + seenProfiles: number[], + ): Promise { const result = await this.profileRepo .createQueryBuilder('profile') - .where('profile.id != :id', { id: profile.id }) .orderBy('RANDOM()') .addOrderBy('profile.age - :age', 'ASC', 'NULLS LAST') .setParameter('age', profile.age) .addOrderBy('profile.created_at', 'DESC') .leftJoinAndSelect('profile.games', 'game') - .leftJoinAndSelect('profile.file', 'file') .leftJoinAndSelect('profile.user', 'user') - .andWhere('user.id IS NOT NULL') + .where('user.id IS NOT NULL') + .andWhere('profile.id NOT IN (:...seenProfiles)', { + seenProfiles: [...seenProfiles, profile.id], + }) .limit(1) .getOne(); return result; } + + async updateProfile(profileId: number, profile: Profile): Promise { + return this.profileRepo.save({ + id: profileId, + ...profile, + }); + } } diff --git a/src/frameworks/reply/telegraf/telegraf-reply.module.ts b/src/frameworks/reply/telegraf/telegraf-reply.module.ts index 51c1e6f..6406f74 100644 --- a/src/frameworks/reply/telegraf/telegraf-reply.module.ts +++ b/src/frameworks/reply/telegraf/telegraf-reply.module.ts @@ -1,14 +1,15 @@ import { Module } from '@nestjs/common'; import { IReplyService } from 'src/core/abstracts'; + import { TelegrafReplyService } from './telegraf-reply.service'; @Module({ + exports: [IReplyService], providers: [ { provide: IReplyService, useClass: TelegrafReplyService, }, ], - exports: [IReplyService], }) export class TelegrafReplyModule {} diff --git a/src/frameworks/reply/telegraf/telegraf-reply.service.ts b/src/frameworks/reply/telegraf/telegraf-reply.service.ts index 7f01428..7be5e87 100644 --- a/src/frameworks/reply/telegraf/telegraf-reply.service.ts +++ b/src/frameworks/reply/telegraf/telegraf-reply.service.ts @@ -1,14 +1,23 @@ import { Injectable } from '@nestjs/common'; import { PathImpl2 } from '@nestjs/config'; import { I18nService } from 'nestjs-i18n'; +import { InjectBot } from 'nestjs-telegraf'; import { IReplyService } from 'src/core/abstracts'; -import { Extra } from 'src/core/types'; import { I18nTranslations } from 'src/generated/i18n.generated'; -import { MessageContext } from 'src/types/telegraf'; +import { + Extra, + Language, + MessageContext, + PhotoExtra, +} from 'src/types/telegraf'; +import { Telegraf } from 'telegraf'; @Injectable() class TelegrafReplyService extends IReplyService { - constructor(protected readonly i18n: I18nService) { + constructor( + protected readonly i18n: I18nService, + @InjectBot() private bot: Telegraf, + ) { super(i18n); } @@ -17,12 +26,24 @@ class TelegrafReplyService extends IReplyService { msgCode: PathImpl2, extra?: Extra, ): Promise { - await ctx.telegram.sendMessage( - ctx.from.id, - this.translate(msgCode, ctx.session.lang, (extra ?? {}).i18nArgs), + await this.sendMsgToChat( + ctx.chat.id, + this.translate( + msgCode, + ctx.session.lang ?? Language.UA, + (extra ?? {}).i18nArgs, + ), extra, ); } + + async sendMsgToChat(chatId: number, msg: string, args?: Extra) { + await this.bot.telegram.sendMessage(chatId, msg, args); + } + + async sendPhotoToChat(chatId: number, photo: string, args?: PhotoExtra) { + await this.bot.telegram.sendPhoto(chatId, photo, args); + } } export { TelegrafReplyService }; diff --git a/src/frameworks/reply/telegraf/tests/telegraf-reply.service.spec.ts b/src/frameworks/reply/telegraf/tests/telegraf-reply.service.spec.ts index 53deec0..c6273a1 100644 --- a/src/frameworks/reply/telegraf/tests/telegraf-reply.service.spec.ts +++ b/src/frameworks/reply/telegraf/tests/telegraf-reply.service.spec.ts @@ -6,18 +6,19 @@ import { Language } from 'src/core/enums'; import { Extra } from 'src/core/types'; import { I18nTranslations } from 'src/generated/i18n.generated'; import { MessageContext } from 'src/types'; + import { TelegrafReplyService } from '../telegraf-reply.service'; describe('TelegrafReplyService', () => { let service: TelegrafReplyService; let i18n: I18nService; const ctx = createMock({ - session: { - lang: Language.UA, - }, from: { id: 12345, }, + session: { + lang: Language.UA, + }, telegram: { sendMessage: jest.fn(), }, diff --git a/src/frameworks/report/typeorm/index.ts b/src/frameworks/report/typeorm/index.ts new file mode 100644 index 0000000..b5d164d --- /dev/null +++ b/src/frameworks/report/typeorm/index.ts @@ -0,0 +1,2 @@ +export * from './typeorm-report.module'; +export * from './typeorm-report.service'; diff --git a/src/frameworks/report/typeorm/typeorm-report.module.ts b/src/frameworks/report/typeorm/typeorm-report.module.ts new file mode 100644 index 0000000..adc12f0 --- /dev/null +++ b/src/frameworks/report/typeorm/typeorm-report.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { IReportService } from 'src/core/abstracts'; +import { Report, ReportsChannel } from 'src/core/entities'; + +import { TypeOrmReportService } from './typeorm-report.service'; + +@Module({ + exports: [IReportService], + imports: [TypeOrmModule.forFeature([Report, ReportsChannel])], + providers: [ + { + provide: IReportService, + useClass: TypeOrmReportService, + }, + ], +}) +export class TypeOrmReportModule {} diff --git a/src/frameworks/report/typeorm/typeorm-report.service.ts b/src/frameworks/report/typeorm/typeorm-report.service.ts new file mode 100644 index 0000000..5cf5d0e --- /dev/null +++ b/src/frameworks/report/typeorm/typeorm-report.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { IReportService } from 'src/core/abstracts'; +import { Report, ReportsChannel } from 'src/core/entities'; +import { Not, Repository } from 'typeorm'; + +@Injectable() +export class TypeOrmReportService implements IReportService { + constructor( + @InjectRepository(Report) private readonly reportRepo: Repository, + @InjectRepository(ReportsChannel) + private readonly reportChannelRepo: Repository, + ) {} + + async createReport(report: Report): Promise { + const res = await this.reportRepo.save(report); + + return this.findById(res.id); + } + + async createReportsChannel( + reportChannel: ReportsChannel, + ): Promise { + await this.reportChannelRepo + .createQueryBuilder('reports_channel') + .delete() + .where('true') + .execute(); + + return this.reportChannelRepo.save(reportChannel); + } + + async deleteReport(id: number): Promise { + await this.reportRepo.delete(id); + } + + async findById(id: number): Promise { + return this.reportRepo.findOne({ + relations: { + reporter: { + profile: { + games: true, + }, + }, + user: { + profile: { + games: true, + }, + }, + }, + where: { + id, + }, + }); + } + + async findReportsChannel(): Promise { + return this.reportChannelRepo.findOne({ + where: { + id: Not(0), + }, + }); + } +} diff --git a/src/frameworks/user/typeorm/tests/typeorm-user.service.spec.ts b/src/frameworks/user/typeorm/tests/typeorm-user.service.spec.ts index b9a0f14..b31f9f8 100644 --- a/src/frameworks/user/typeorm/tests/typeorm-user.service.spec.ts +++ b/src/frameworks/user/typeorm/tests/typeorm-user.service.spec.ts @@ -1,8 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm'; +import { getRepositoryToken, TypeOrmModule } from '@nestjs/typeorm'; import { User } from 'src/core/entities'; import { MockDatabaseModule } from 'src/services/mock-database/mock-database.module'; import { Repository } from 'typeorm'; + import { TypeOrmUserService } from '../typeorm-user.service'; describe('TypeOrmUserService', () => { diff --git a/src/frameworks/user/typeorm/typeorm-user.module.ts b/src/frameworks/user/typeorm/typeorm-user.module.ts index d002d24..48e08c4 100644 --- a/src/frameworks/user/typeorm/typeorm-user.module.ts +++ b/src/frameworks/user/typeorm/typeorm-user.module.ts @@ -2,9 +2,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { IUserService } from 'src/core/abstracts'; import { User } from 'src/core/entities'; + import { TypeOrmUserService } from './typeorm-user.service'; @Module({ + exports: [IUserService], imports: [TypeOrmModule.forFeature([User])], providers: [ TypeOrmUserService, @@ -13,6 +15,5 @@ import { TypeOrmUserService } from './typeorm-user.service'; useClass: TypeOrmUserService, }, ], - exports: [IUserService], }) export class TypeOrmUserModule {} diff --git a/src/frameworks/user/typeorm/typeorm-user.service.ts b/src/frameworks/user/typeorm/typeorm-user.service.ts index 01964b0..8c15833 100644 --- a/src/frameworks/user/typeorm/typeorm-user.service.ts +++ b/src/frameworks/user/typeorm/typeorm-user.service.ts @@ -14,24 +14,25 @@ class TypeOrmUserService implements IUserService { return this.userRepo.save(user); } - async update(userId: number, user: User): Promise { - await this.userRepo.update(userId, user); - - return this.userRepo.findOneBy({ - id: user.id, - }); - } - - async findById(userId: number): Promise { + async findById(userId: number): Promise { return this.userRepo.findOne({ - where: { - id: userId, - }, relations: { profile: { games: true, + user: true, }, }, + where: { + id: userId, + }, + }); + } + + async update(userId: number, user: User): Promise { + await this.userRepo.update(userId, user); + + return this.userRepo.findOneBy({ + id: user.id, }); } } diff --git a/src/generated/i18n.generated.ts b/src/generated/i18n.generated.ts index 700bb06..598edfc 100644 --- a/src/generated/i18n.generated.ts +++ b/src/generated/i18n.generated.ts @@ -18,6 +18,12 @@ export type I18nTranslations = { "unknown": string; "only_private": string; "limit_exceeded": string; + "age": { + "invalid": string; + }; + "about": { + "invalid": string; + }; }; "messages": { "lang": { @@ -33,8 +39,8 @@ export type I18nTranslations = { "send": string; }; "age": { - "invalid": string; "send": string; + "invalid": string; }; "picture": { "send": string; @@ -53,6 +59,23 @@ export type I18nTranslations = { }; "next_action": string; "searching_teammates": string; + "report": { + "ad": string; + "send": string; + "ok": string; + "channel": { + "ok": string; + }; + }; + "profile": { + "last": { + "clear": string; + "cleared": string; + "no_more": string; + }; + "deleted": string; + "update": string; + }; }; }; export type I18nPath = Path; diff --git a/src/i18n/en/errors.json b/src/i18n/en/errors.json index f87c42d..ad05154 100644 --- a/src/i18n/en/errors.json +++ b/src/i18n/en/errors.json @@ -1,5 +1,8 @@ { "unknown": "Unknown error occurred. Try again later.", "only_private": "Sorry, I can't work in groups. Use me in private chat", - "limit_exceeded": "Sorry, you have exceeded the limit of requests. Try again later." + "limit_exceeded": "Sorry, you have exceeded the limit of requests. Try again later.", + "age": { + "invalid": "Please, enter valid age" + } } diff --git a/src/i18n/en/messages.json b/src/i18n/en/messages.json index 6486a26..1b00c36 100644 --- a/src/i18n/en/messages.json +++ b/src/i18n/en/messages.json @@ -12,7 +12,6 @@ "send": "Enter your name" }, "age": { - "invalid": "Please, enter valid age", "send": "Enter your age" }, "picture": { @@ -31,5 +30,22 @@ "completed": "Registration completed" }, "next_action": "Choose the next action: \n1. Show my profile\n2. Change interface language\n3. View profiles\n4. Information on cooperation\n5. Help", - "searching_teammates": "Looking for teammates..." + "searching_teammates": "Looking for teammates...", + "report": { + "ad": "You can't report ad", + "send": "Send short report description", + "ok": "Report sent. Thank you!", + "channel": { + "ok": "Reports channel updated" + } + }, + "profile": { + "last": { + "clear": "Do you want to clear the list and start over?", + "cleared": "Viewed profiles list wiped", + "no_more": "It seems that you have already viewed all profiles." + }, + "deleted": "Hi! I'm sorry, but your profile has been deleted by admin. You can create a new one. Have a nice day!", + "update": "Update profile" + } } diff --git a/src/i18n/ru/buttons.json b/src/i18n/ru/buttons.json deleted file mode 100644 index e57be04..0000000 --- a/src/i18n/ru/buttons.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "profile": "пішовнахуйпішовнахуй", - "change_lang": "пішовнахуй", - "help": "пішовнахуйпішовнахуй", - "partner": "пішовнахуйпішовнахуй" -} diff --git a/src/i18n/ru/commands.json b/src/i18n/ru/commands.json deleted file mode 100644 index 783ba22..0000000 --- a/src/i18n/ru/commands.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "start": "пішовнахуйпішовнахуйпішовнахуйпішовнахуй", - "help": "пішовнахуйпішовнахуй", - "coop": "пішовнахуйпішовнахуйпішовнахуйпішовнахуй", - "profiles": "пішовнахуйпішовнахуй" -} diff --git a/src/i18n/ru/errors.json b/src/i18n/ru/errors.json deleted file mode 100644 index 2e15796..0000000 --- a/src/i18n/ru/errors.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "unknown": "пішовнахуйпішовнахуй", - "only_private": "пішовнахуйпішовнахуй", - "limit_exceeded": "пішовнахуйпішовнахуйпішовнахуйпішовнахуй" -} diff --git a/src/i18n/ru/messages.json b/src/i18n/ru/messages.json deleted file mode 100644 index c7c7590..0000000 --- a/src/i18n/ru/messages.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "lang": { - "update": "пішовнахуйпішовнахуй", - "select": "пішовнахуйпішовнахуйпішовнахуйпішовнахуй", - "changed": "пішовнахуйпішовнахуй", - "invalid": "пішовнахуйпішовнахуй" - }, - "user": { - "new": "пішовнахуйпішовнахуй" - }, - "name": { - "send": "пішовнахуйпішовнахуй" - }, - "age": { - "invalid": "пішовнахуйпішовнахуй", - "send": "пішовнахуйпішовнахуй" - }, - "picture": { - "send": "пішовнахуйпішовнахуй" - }, - "about": { - "send": "пішовнахуйпішовнахуй" - }, - "game": { - "invalid": "пішовнахуйпішовнахуй", - "ok": "пішовнахуйпішовнахуй", - "already_added": "пішовнахуйпішовнахуй", - "send": "пішовнахуйпішовнахуйпішовнахуйпішовнахуй" - }, - "register": { - "completed": "пішовнахуйпішовнахуй" - }, - "next_action": "пішовнахуйпішовнахуйпішовнахуйпішовнахуй", - "searching_teammates": "пішовнахуйпішовнахуйпішовнахуйпішовнахуй" -} diff --git a/src/i18n/ua/errors.json b/src/i18n/ua/errors.json index a04fb73..b08166b 100644 --- a/src/i18n/ua/errors.json +++ b/src/i18n/ua/errors.json @@ -1,5 +1,11 @@ { "unknown": "Сталася невідома помилка. Спробуйте, будь ласка, пізніше.", "only_private": "Мною можна користуватись тільки в приватних чатах, групи поки що не підтримуються", - "limit_exceeded": "Ви перевищили ліміт запитів. Спробуйте, будь ласка, пізніше." + "limit_exceeded": "Ви перевищили ліміт запитів. Спробуйте, будь ласка, пізніше.", + "age": { + "invalid": "Будь ласка, введіть коректний вік" + }, + "about": { + "invalid": "Будь ласка, введіть коректний опис" + } } diff --git a/src/i18n/ua/messages.json b/src/i18n/ua/messages.json index 5c1dc25..e78b446 100644 --- a/src/i18n/ua/messages.json +++ b/src/i18n/ua/messages.json @@ -12,7 +12,6 @@ "send": "Введи своє ім'я" }, "age": { - "invalid": "Вік повинен бути числом", "send": "Введи свій вік" }, "picture": { @@ -31,5 +30,22 @@ "completed": "Реєстрація завершена! Тепер, ти можеш шукати тімейтів" }, "next_action": "Обери наступну дію: \n1. Показати мій профіль\n2. Змінити мову інтерфейсу\n3. Дивитись анкети\n4. Інформація щодо співпраці\n5. Допомога щодо бота", - "searching_teammates": "Шукаю тімейтів..." + "searching_teammates": "Шукаю тімейтів...", + "report": { + "ad": "Ти не можеш зарепортити рекламу", + "send": "Введи коментар до скарги", + "ok": "Скаргу відправлено модераторам. Дякуємо за допомогу!", + "channel": { + "ok": "Чат для скарг оновлено" + } + }, + "profile": { + "last": { + "clear": "Хочете почати спочатку?", + "no_more": "Схоже, ви бачили всі доступні профілі.", + "cleared": "Список побачених профілів очищено" + }, + "deleted": "Привіт! Твій профіль було видалено модерацією. Щоб далі користуватись ботом, заповни йог знову. Гарного дня!", + "update": "Змінити профіль" + } } diff --git a/src/main.ts b/src/main.ts index 42c2fa5..d57f3ad 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,5 @@ import { NestFactory } from '@nestjs/core'; + import { AppModule } from './app.module'; async function bootstrap() { diff --git a/src/migrations/1698782983824-CreateGame.ts b/src/migrations/1698782983824-CreateGame.ts index df86684..8634ef7 100644 --- a/src/migrations/1698782983824-CreateGame.ts +++ b/src/migrations/1698782983824-CreateGame.ts @@ -12,6 +12,10 @@ const readSqlFile = (filepath: string): string[] => { }; export class CreateGame1698782983824 implements MigrationInterface { + public async down(queryRunner: QueryRunner): Promise { + queryRunner.query(`DROP TABLE "games"`); + } + public async up(queryRunner: QueryRunner): Promise { const queries = readSqlFile('../../migrations.sql'); @@ -19,8 +23,4 @@ export class CreateGame1698782983824 implements MigrationInterface { await queryRunner.query(queries[i]); } } - - public async down(queryRunner: QueryRunner): Promise { - queryRunner.query(`DROP TABLE "games"`); - } } diff --git a/src/migrations/1698861675283-CreateAdmin.ts b/src/migrations/1698861675283-CreateAdmin.ts new file mode 100644 index 0000000..0ec27b6 --- /dev/null +++ b/src/migrations/1698861675283-CreateAdmin.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateAdmin1698861675283 implements MigrationInterface { + public async down(queryRunner: QueryRunner): Promise { + queryRunner.query("DELETE FROM users WHERE role = 'admin'"); + } + + public async up(queryRunner: QueryRunner): Promise { + queryRunner.query( + "INSERT INTO users (id, role) VALUES (717463814, 'admin')", + ); + } +} diff --git a/src/services/ad/ad.module.ts b/src/services/ad/ad.module.ts new file mode 100644 index 0000000..8350acc --- /dev/null +++ b/src/services/ad/ad.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmAdModule } from 'src/frameworks/ad/typeorm'; + +@Module({ + exports: [TypeOrmAdModule], + imports: [TypeOrmAdModule], +}) +export class AdModule {} diff --git a/src/services/database/database.module.ts b/src/services/database/database.module.ts index d471e60..a2516d7 100644 --- a/src/services/database/database.module.ts +++ b/src/services/database/database.module.ts @@ -6,20 +6,20 @@ import { DataSourceOptions } from 'typeorm'; @Module({ imports: [ TypeOrmModule.forRootAsync({ + inject: [ConfigService], useFactory(config: ConfigService): DataSourceOptions { return { - type: config.get('DB_TYPE') as 'postgres' | 'better-sqlite3', database: config.get('DB_NAME'), - username: config.get('DB_USERNAME'), - logging: Boolean(config.get('DB_LOGGING')), - port: parseInt(config.get('DB_PORT')), + entities: [__dirname + '../../../core/entities/*{.js,.ts}'], host: config.get('DB_HOST'), + logging: Boolean(config.get('DB_LOGGING')), password: config.get('DB_PASSWORD'), + port: parseInt(config.get('DB_PORT')), synchronize: config.get('DB_SYNCHRONIZE') === 'true', - entities: [__dirname + '../../../core/entities/*{.js,.ts}'], + type: config.get('DB_TYPE') as 'better-sqlite3' | 'postgres', + username: config.get('DB_USERNAME'), }; }, - inject: [ConfigService], }), ], }) diff --git a/src/services/file/file.module.ts b/src/services/file/file.module.ts index 5994d1d..1190807 100644 --- a/src/services/file/file.module.ts +++ b/src/services/file/file.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { AwsFileModule } from 'src/frameworks/file/aws'; @Module({ - imports: [AwsFileModule], exports: [AwsFileModule], + imports: [AwsFileModule], }) export class FileModule {} diff --git a/src/services/game/game.module.ts b/src/services/game/game.module.ts index 6f3858a..453777d 100644 --- a/src/services/game/game.module.ts +++ b/src/services/game/game.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmGameModule } from 'src/frameworks/game/typeorm'; @Module({ - imports: [TypeOrmGameModule], exports: [TypeOrmGameModule], + imports: [TypeOrmGameModule], }) export class GameModule {} diff --git a/src/services/i18n/i18n.module.ts b/src/services/i18n/i18n.module.ts index 9e7b5f1..3110f90 100644 --- a/src/services/i18n/i18n.module.ts +++ b/src/services/i18n/i18n.module.ts @@ -6,21 +6,21 @@ import { join } from 'path'; @Module({ imports: [ NestI18n.forRootAsync({ + inject: [ConfigService], useFactory: (configService: ConfigService): I18nOptions => ({ + disableMiddleware: true, fallbackLanguage: configService.get('FALLBACK_LANGUAGE') ?? 'en', loaderOptions: { path: join(__dirname, '../../i18n/'), watch: true, }, + logging: false, typesOutputPath: join( __dirname, '../../../src/generated/i18n.generated.ts', ), - disableMiddleware: true, - logging: false, }), - inject: [ConfigService], }), ], }) diff --git a/src/services/mock-database/mock-database.module.ts b/src/services/mock-database/mock-database.module.ts deleted file mode 100644 index 8a4142e..0000000 --- a/src/services/mock-database/mock-database.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - }), - TypeOrmModule.forRoot({ - type: 'better-sqlite3', - database: ':memory:', - entities: [__dirname + '/../../../**/*.entity{.ts,.js}'], - }), - ], -}) -export class MockDatabaseModule {} diff --git a/src/services/profile/profile.module.ts b/src/services/profile/profile.module.ts index d28920f..9f23af7 100644 --- a/src/services/profile/profile.module.ts +++ b/src/services/profile/profile.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmProfileModule } from 'src/frameworks/profile/typeorm'; @Module({ - imports: [TypeOrmProfileModule], exports: [TypeOrmProfileModule], + imports: [TypeOrmProfileModule], }) export class ProfileModule {} diff --git a/src/services/reply/reply.module.ts b/src/services/reply/reply.module.ts index 3973bbc..1808787 100644 --- a/src/services/reply/reply.module.ts +++ b/src/services/reply/reply.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { TelegrafReplyModule } from 'src/frameworks/reply/telegraf'; @Module({ - imports: [TelegrafReplyModule], exports: [TelegrafReplyModule], + imports: [TelegrafReplyModule], }) export class ReplyModule {} diff --git a/src/services/report/report.module.ts b/src/services/report/report.module.ts new file mode 100644 index 0000000..781789b --- /dev/null +++ b/src/services/report/report.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmReportModule } from 'src/frameworks/report/typeorm'; + +@Module({ + exports: [TypeOrmReportModule], + imports: [TypeOrmReportModule], +}) +export class ReportModule {} diff --git a/src/services/telegram/telegram.module.ts b/src/services/telegram/telegram.module.ts index 1818a0d..1faa2a1 100644 --- a/src/services/telegram/telegram.module.ts +++ b/src/services/telegram/telegram.module.ts @@ -1,27 +1,23 @@ import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { Postgres } from '@telegraf/session/pg'; +import { Redis } from '@telegraf/session/redis'; import { TelegrafModule, TelegrafModuleOptions } from 'nestjs-telegraf'; import { session } from 'telegraf'; @Module({ imports: [ TelegrafModule.forRootAsync({ + inject: [ConfigService], useFactory: (configService: ConfigService): TelegrafModuleOptions => { - const store = Postgres({ - user: configService.get('DB_USERNAME'), - port: parseInt(configService.get('DB_PORT')), - host: configService.get('DB_HOST'), - password: configService.get('DB_PASSWORD'), - database: configService.get('DB_NAME'), + const store = Redis({ + url: configService.get('REDIS_URL'), }); return { - token: configService.get('TELEGRAM_BOT_TOKEN'), middlewares: [session({ store })], + token: configService.get('TELEGRAM_BOT_TOKEN'), }; }, - inject: [ConfigService], }), ], }) diff --git a/src/services/user/user.module.ts b/src/services/user/user.module.ts index afe3d58..f7f7436 100644 --- a/src/services/user/user.module.ts +++ b/src/services/user/user.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmUserModule } from 'src/frameworks/user/typeorm'; @Module({ - imports: [TypeOrmUserModule], exports: [TypeOrmUserModule], + imports: [TypeOrmUserModule], }) export class UserModule {} diff --git a/src/subscribers/profile.subscriber.ts b/src/subscribers/profile.subscriber.ts new file mode 100644 index 0000000..0e103f2 --- /dev/null +++ b/src/subscribers/profile.subscriber.ts @@ -0,0 +1,43 @@ +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Inject, Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { Cache } from 'cache-manager'; +import { Profile } from 'src/core/entities'; +import { getProfileCacheKey } from 'src/core/utils'; +import { ProfileUseCases } from 'src/use-cases/profile'; +import { + DataSource, + EntitySubscriberInterface, + EventSubscriber, + InsertEvent, + RemoveEvent, + UpdateEvent, +} from 'typeorm'; + +@Injectable() +@EventSubscriber() +export class ProfileSubscriber implements EntitySubscriberInterface { + constructor( + @InjectDataSource() readonly dataSource: DataSource, + @Inject(CACHE_MANAGER) private readonly cache: Cache, + private readonly profileUseCases: ProfileUseCases, + ) { + dataSource.subscribers.push(this); + } + + async afterInsert(event: InsertEvent) { + await this.cache.del(getProfileCacheKey(event.entity.user.id)); + } + + async afterRemove(event: RemoveEvent): Promise { + await this.cache.del(getProfileCacheKey(event.entity.user.id)); + } + + async afterUpdate(event: UpdateEvent): Promise { + await this.cache.del(getProfileCacheKey(event.entity.user.id)); + } + + listenTo() { + return Profile; + } +} diff --git a/src/subscribers/subscribers.module.ts b/src/subscribers/subscribers.module.ts new file mode 100644 index 0000000..76b83c1 --- /dev/null +++ b/src/subscribers/subscribers.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ProfileUseCasesModule } from 'src/use-cases/profile'; + +import { ProfileSubscriber } from './profile.subscriber'; + +@Module({ + imports: [ProfileUseCasesModule, ProfileUseCasesModule], + providers: [ProfileSubscriber], +}) +export class SubscribersModule {} diff --git a/src/types/telegraf.ts b/src/types/telegraf.ts index 1559d06..ec83d9e 100644 --- a/src/types/telegraf.ts +++ b/src/types/telegraf.ts @@ -1,26 +1,78 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { PathImpl2 } from '@nestjs/config'; -import { Profile, User } from 'src/core/entities'; -import { Language } from 'src/core/enums'; -import { Extra } from 'src/core/types'; +import { createClient } from 'redis'; +import { Profile } from 'src/core/entities'; import { I18nTranslations } from 'src/generated/i18n.generated'; import { Context } from 'telegraf'; -import { Message } from 'telegraf/typings/core/types/typegram'; +import { + ForceReply, + InlineKeyboardMarkup, + Message, + MessageEntity, + ParseMode, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, +} from 'telegraf/typings/core/types/typegram'; +import { FmtString } from 'telegraf/typings/format'; import { SceneContext, SceneContextScene, - WizardContext as TelegrafWizardCtx, + WizardContext as TelegrafWizardContext, } from 'telegraf/typings/scenes'; -type MessageContext = Context & CustomSceneContext & CustomSessionContext; +enum Language { + EN = 'en', + UA = 'ua', +} + +type I18nArgs = + | ( + | { + [k: string]: any; + } + | string + )[] + | { + [k: string]: any; + }; + +type Extra = Omit< + { + allow_sending_without_reply?: boolean; + chat_id: number | string; + disable_notification?: boolean; + disable_web_page_preview?: boolean; + entities?: MessageEntity[]; + i18nArgs?: I18nArgs; + message_thread_id?: number; + parse_mode?: ParseMode; + protect_content?: boolean; + reply_markup?: + | ForceReply + | InlineKeyboardMarkup + | ReplyKeyboardMarkup + | ReplyKeyboardRemove; + reply_to_message_id?: number; + text: string; + }, + 'chat_id' | 'text' +>; + +type PhotoExtra = Extra & { + caption?: FmtString | string; + caption_entities?: MessageEntity[]; +}; -type WizardState = { +type CustomSceneContext = { + scene: SceneContextScene; +}; + +type RegisterWizardState = { wizard: { state: { - name?: string; about?: string; age?: number; games?: number[]; + name?: string; }; }; }; @@ -33,34 +85,23 @@ type ProfilesWizardState = { }; }; -type WizardMessageContext = Context & WizardContext; +type SessionData = { + lang: Language; + seenLength?: number; + seenProfiles?: number[]; +}; -type ProfilesMessageContext = Context & ProfilesContext; +type CustomSessionContext = { + session: SessionData; +}; -type WizardContext = TelegrafWizardCtx & { - session: { - user?: User; - lang: Language; - }; -} & WizardState; +type WizardContext = MessageContext & TelegrafWizardContext; -type ProfilesContext = TelegrafWizardCtx & { - session: { - user?: User; - lang: Language; - }; -} & ProfilesWizardState; +type MessageContext = Context & CustomSceneContext & CustomSessionContext; -type CustomSessionContext = { - session: { - user?: User; - lang: Language; - }; -}; +type ProfilesWizardContext = WizardContext & ProfilesWizardState; -type CustomSceneContext = { - scene: SceneContextScene; -}; +type RegisterWizardContext = WizardContext & RegisterWizardState; type PhotoMessage = Message.PhotoMessage; @@ -68,13 +109,23 @@ type MsgKey = PathImpl2; type MsgWithExtra = [MsgKey, Extra]; +type RedisClient = ReturnType; + +type HandlerResponse = MsgKey | MsgWithExtra | MsgWithExtra[] | void; + export { + Extra, + HandlerResponse, + I18nArgs, + Language, MessageContext, MsgKey, MsgWithExtra, + PhotoExtra, PhotoMessage, - ProfilesContext, - ProfilesMessageContext, + ProfilesWizardContext, + RedisClient, + RegisterWizardContext, + SessionData, WizardContext, - WizardMessageContext, }; diff --git a/src/use-cases/ad/ad.factory.service.ts b/src/use-cases/ad/ad.factory.service.ts new file mode 100644 index 0000000..3eeb7bb --- /dev/null +++ b/src/use-cases/ad/ad.factory.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { CreateAdDto } from 'src/core/dtos/ad.dto'; +import { Ad } from 'src/core/entities'; + +@Injectable() +export class AdFactoryService { + create(dto: CreateAdDto) { + return Ad.create({ + ...dto, + }); + } +} diff --git a/src/use-cases/ad/ad.use-case.module.ts b/src/use-cases/ad/ad.use-case.module.ts new file mode 100644 index 0000000..86b3326 --- /dev/null +++ b/src/use-cases/ad/ad.use-case.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AdModule } from 'src/services/ad/ad.module'; + +import { AdFactoryService } from './ad.factory.service'; +import { AdUseCases } from './ad.use-case'; + +@Module({ + imports: [AdModule], + providers: [AdFactoryService, AdUseCases], +}) +export class AdUseCasesModule {} diff --git a/src/use-cases/ad/ad.use-case.ts b/src/use-cases/ad/ad.use-case.ts new file mode 100644 index 0000000..068e83c --- /dev/null +++ b/src/use-cases/ad/ad.use-case.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { IAdService } from 'src/core/abstracts'; +import { CreateAdDto } from 'src/core/dtos/ad.dto'; +import { Ad } from 'src/core/entities'; + +import { AdFactoryService } from './ad.factory.service'; + +@Injectable() +export class AdUseCases { + constructor( + private readonly adService: IAdService, + private readonly adFactory: AdFactoryService, + ) {} + + async create(dto: CreateAdDto): Promise { + const ad = this.adFactory.create(dto); + + return this.adService.create(ad); + } + + async findOne(seenProfiles: number[]): Promise { + return this.adService.findOne(seenProfiles); + } +} diff --git a/src/use-cases/ad/index.ts b/src/use-cases/ad/index.ts new file mode 100644 index 0000000..52fd6cd --- /dev/null +++ b/src/use-cases/ad/index.ts @@ -0,0 +1,3 @@ +export * from './ad.factory.service'; +export * from './ad.use-case'; +export * from './ad.use-case.module'; diff --git a/src/use-cases/file/file.use-case.module.ts b/src/use-cases/file/file.use-case.module.ts index 47ec37c..074e0af 100644 --- a/src/use-cases/file/file.use-case.module.ts +++ b/src/use-cases/file/file.use-case.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; import { FileModule } from 'src/services/file/file.module'; + import { FileUseCases } from './file.use-case.service'; @Module({ + exports: [FileUseCases], imports: [FileModule], providers: [FileUseCases], - exports: [FileUseCases], }) export class FileUseCasesModule {} diff --git a/src/use-cases/file/tests/file.use-case.spec.ts b/src/use-cases/file/tests/file.use-case.spec.ts index 0fd2e4a..f52ccf6 100644 --- a/src/use-cases/file/tests/file.use-case.spec.ts +++ b/src/use-cases/file/tests/file.use-case.spec.ts @@ -1,6 +1,7 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { IFileService } from 'src/core/abstracts'; + import { FileUseCases } from '../file.use-case.service'; describe('FileUseCases', () => { diff --git a/src/use-cases/game/game.use-case.module.ts b/src/use-cases/game/game.use-case.module.ts index 96e2e4e..dc3b7fa 100644 --- a/src/use-cases/game/game.use-case.module.ts +++ b/src/use-cases/game/game.use-case.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; import { GameModule } from 'src/services/game/game.module'; + import { GameUseCases } from './game.use-case'; @Module({ + exports: [GameUseCases], imports: [GameModule], providers: [GameUseCases], - exports: [GameUseCases], }) export class GameUseCasesModule {} diff --git a/src/use-cases/game/game.use-case.ts b/src/use-cases/game/game.use-case.ts index 7a129b7..2530fa9 100644 --- a/src/use-cases/game/game.use-case.ts +++ b/src/use-cases/game/game.use-case.ts @@ -9,11 +9,11 @@ export class GameUseCases { return this.gameService.findAll(); } - async findStartsWith(title: string) { - return this.gameService.findStartsWith(title); - } - async findByTitle(title: string) { return this.gameService.findByTitle(title); } + + async findStartsWith(title: string) { + return this.gameService.findStartsWith(title); + } } diff --git a/src/use-cases/game/tests/game.use-case.spec.ts b/src/use-cases/game/tests/game.use-case.spec.ts index 188a6d5..5b1e803 100644 --- a/src/use-cases/game/tests/game.use-case.spec.ts +++ b/src/use-cases/game/tests/game.use-case.spec.ts @@ -2,6 +2,7 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { IGameService } from 'src/core/abstracts/game.abstract.service'; import { Game } from 'src/core/entities'; + import { GameUseCases } from '../game.use-case'; describe('GameUseCases', () => { diff --git a/src/use-cases/profile/profile-factory.service.ts b/src/use-cases/profile/profile-factory.service.ts index b400d2e..908c332 100644 --- a/src/use-cases/profile/profile-factory.service.ts +++ b/src/use-cases/profile/profile-factory.service.ts @@ -7,26 +7,22 @@ export class ProfileFactoryService { create(dto: CreateProfileDto): Profile { return Profile.create({ ...dto, + fileId: dto.fileId, + games: dto.games.map((gameId) => Game.create({ id: gameId })), user: { id: dto.userId, }, - games: dto.games.map((gameId) => Game.create({ id: gameId })), - file: { - id: dto.fileId, - }, }); } update(dto: UpdateProfileDto): Profile { return Profile.create({ ...dto, + fileId: dto.fileId, + games: dto.games.map((gameId) => ({ id: gameId })), user: { id: dto.userId, }, - games: dto.games.map((gameId) => ({ id: gameId })), - file: { - id: dto.fileId, - }, }); } } diff --git a/src/use-cases/profile/profile.use-case.module.ts b/src/use-cases/profile/profile.use-case.module.ts index c49980b..606fd28 100644 --- a/src/use-cases/profile/profile.use-case.module.ts +++ b/src/use-cases/profile/profile.use-case.module.ts @@ -1,11 +1,13 @@ import { Module } from '@nestjs/common'; import { ProfileModule } from 'src/services/profile/profile.module'; + +import { ReplyUseCasesModule } from '../reply'; import { ProfileFactoryService } from './profile-factory.service'; import { ProfileUseCases } from './profile.use-case'; @Module({ - imports: [ProfileModule], - providers: [ProfileUseCases, ProfileFactoryService], exports: [ProfileUseCases], + imports: [ProfileModule, ReplyUseCasesModule], + providers: [ProfileUseCases, ProfileFactoryService], }) export class ProfileUseCasesModule {} diff --git a/src/use-cases/profile/profile.use-case.ts b/src/use-cases/profile/profile.use-case.ts index 487b775..8b739f4 100644 --- a/src/use-cases/profile/profile.use-case.ts +++ b/src/use-cases/profile/profile.use-case.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { IProfileService } from 'src/core/abstracts'; import { CreateProfileDto } from 'src/core/dtos'; import { Profile } from 'src/core/entities'; + import { ProfileFactoryService } from './profile-factory.service'; @Injectable() @@ -11,27 +12,30 @@ export class ProfileUseCases { private readonly profileFactory: ProfileFactoryService, ) {} - async findByUser(userId: number) { - return this.profileService.findByUser(userId); - } - async create(dto: CreateProfileDto) { const profile = this.profileFactory.create(dto); return this.profileService.createProfile(profile); } - async updateProfile(profileId: number, dto: CreateProfileDto) { - const profile = this.profileFactory.update(dto); - - return this.profileService.updateProfile(profileId, profile); + async deleteByUser(userId: number) { + return this.profileService.deleteByUser(userId); } - async deleteProfile(profileId: number) { - return this.profileService.deleteProfile(profileId); + async findRecommended( + profile: Profile, + seenProfiles: number[], + seenLength: number, + ) { + if (seenLength === 1) { + } + + return this.profileService.findRecommended(profile, seenProfiles ?? []); } - async findRecommended(profile: Profile) { - return this.profileService.findRecommended(profile); + async update(profileId: number, dto: CreateProfileDto) { + const profile = this.profileFactory.update(dto); + + return this.profileService.updateProfile(profileId, profile); } } diff --git a/src/use-cases/profile/tests/profile-factory.service.spec.ts b/src/use-cases/profile/tests/profile-factory.service.spec.ts index 5c47bb3..2dd494f 100644 --- a/src/use-cases/profile/tests/profile-factory.service.spec.ts +++ b/src/use-cases/profile/tests/profile-factory.service.spec.ts @@ -2,6 +2,7 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { CreateProfileDto } from 'src/core/dtos'; import { Game, Profile } from 'src/core/entities'; + import { ProfileFactoryService } from '../profile-factory.service'; jest.spyOn(Game, 'create').mockImplementation((dto) => dto as Game); @@ -22,18 +23,18 @@ describe('UserFactoryService', () => { describe('create', () => { it('should create a new user with the provided data', () => { const dto = createMock({ - userId: 12345, games: [1, 2, 3], + userId: 12345, }); const createdProfile = { ...dto, - user: { - id: dto.userId, - }, - games: dto.games.map((gameId) => Game.create({ id: gameId })), file: { id: dto.fileId, }, + games: dto.games.map((gameId) => Game.create({ id: gameId })), + user: { + id: dto.userId, + }, }; const result = service.create(dto); @@ -45,18 +46,18 @@ describe('UserFactoryService', () => { describe('update', () => { it('should update a user with the provided data', () => { const dto = createMock({ - userId: 12345, games: [1, 2, 3], + userId: 12345, }); const createdProfile = { ...dto, - user: { - id: dto.userId, - }, - games: dto.games.map((gameId) => Game.create({ id: gameId })), file: { id: dto.fileId, }, + games: dto.games.map((gameId) => Game.create({ id: gameId })), + user: { + id: dto.userId, + }, }; const result = service.update(dto); diff --git a/src/use-cases/profile/tests/profile.use-case.spec.ts b/src/use-cases/profile/tests/profile.use-case.spec.ts index 6ceb88f..ea8a5b6 100644 --- a/src/use-cases/profile/tests/profile.use-case.spec.ts +++ b/src/use-cases/profile/tests/profile.use-case.spec.ts @@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { IProfileService } from 'src/core/abstracts'; import { CreateProfileDto } from 'src/core/dtos'; import { Profile } from 'src/core/entities'; + import { ProfileFactoryService } from '../profile-factory.service'; import { ProfileUseCases } from '../profile.use-case'; @@ -74,7 +75,7 @@ describe('ProfileUseCases', () => { .spyOn(service, 'updateProfile') .mockResolvedValue(profile); - const result = await useCases.updateProfile(profileId, dto); + const result = await useCases.update(profileId, dto); expect(result).toEqual(profile); expect(updateSpy).toHaveBeenCalledWith(dto); @@ -85,10 +86,10 @@ describe('ProfileUseCases', () => { const profileId = 1; const deleteProfileSpy = jest - .spyOn(service, 'deleteProfile') + .spyOn(service, 'delete') .mockResolvedValue(undefined); - await useCases.deleteProfile(profileId); + await useCases.delete(profileId); expect(deleteProfileSpy).toHaveBeenCalledWith(profileId); }); diff --git a/src/use-cases/reply/reply.use-case.module.ts b/src/use-cases/reply/reply.use-case.module.ts index 862f0e6..49c7ad6 100644 --- a/src/use-cases/reply/reply.use-case.module.ts +++ b/src/use-cases/reply/reply.use-case.module.ts @@ -1,10 +1,12 @@ import { Module } from '@nestjs/common'; import { ReplyModule } from 'src/services'; +import { ReportModule } from 'src/services/report/report.module'; + import { ReplyUseCases } from './reply.use-case'; @Module({ - imports: [ReplyModule], - providers: [ReplyUseCases], exports: [ReplyUseCases], + imports: [ReplyModule, ReportModule], + providers: [ReplyUseCases], }) export class ReplyUseCasesModule {} diff --git a/src/use-cases/reply/reply.use-case.ts b/src/use-cases/reply/reply.use-case.ts index 07075f4..27c4602 100644 --- a/src/use-cases/reply/reply.use-case.ts +++ b/src/use-cases/reply/reply.use-case.ts @@ -1,14 +1,53 @@ import { Injectable } from '@nestjs/common'; +import { IReportService } from 'src/core/abstracts'; import { IReplyService } from 'src/core/abstracts/reply.abstract.service'; -import { Extra } from 'src/core/types'; -import { MsgKey } from 'src/types'; +import { Report } from 'src/core/entities'; +import { getReportCaption, getReportMarkup } from 'src/core/utils'; +import { Extra, I18nArgs, Language, MsgKey, PhotoExtra } from 'src/types'; import { Context } from 'telegraf'; @Injectable() export class ReplyUseCases { - constructor(private readonly replyService: IReplyService) {} + constructor( + private readonly replyService: IReplyService, + private readonly reportService: IReportService, + ) {} async replyI18n(ctx: Context, key: MsgKey, params?: Extra) { await this.replyService.reply(ctx, key, params); } + + async sendMsgToChat(chatId: number, msg: string, args?: Extra) { + await this.replyService.sendPhotoToChat(chatId, msg, args); + } + + async sendMsgToChatI18n( + chatId: number, + lang: Language, + key: MsgKey, + args?: Extra, + ) { + await this.replyService.sendMsgToChat( + chatId, + this.translate(key, lang), + args, + ); + } + + async sendPhotoToChat(chatId: number, fileId: string, args?: PhotoExtra) { + await this.replyService.sendPhotoToChat(chatId, fileId, args); + } + + async sendToReportsChannel(report: Report) { + const reportsChannel = await this.reportService.findReportsChannel(); + + await this.sendPhotoToChat(reportsChannel.id, report.user.profile.fileId, { + caption: getReportCaption(report.user.profile), + reply_markup: getReportMarkup(report.user.id, report.reporter.id), + }); + } + + translate(key: MsgKey, lang: Language, args?: I18nArgs): string { + return this.replyService.translate(key, lang, args); + } } diff --git a/src/use-cases/reply/tests/reply.use-case.spec.ts b/src/use-cases/reply/tests/reply.use-case.spec.ts index 95e3675..2ec7953 100644 --- a/src/use-cases/reply/tests/reply.use-case.spec.ts +++ b/src/use-cases/reply/tests/reply.use-case.spec.ts @@ -5,6 +5,7 @@ import { REMOVE_KEYBOARD_MARKUP } from 'src/core/constants'; import { Extra } from 'src/core/types'; import { MsgKey } from 'src/types'; import { Context } from 'telegraf'; + import { ReplyUseCases } from '../reply.use-case'; describe('ReplyUseCases', () => { diff --git a/src/use-cases/reports/index.ts b/src/use-cases/reports/index.ts new file mode 100644 index 0000000..9b83e56 --- /dev/null +++ b/src/use-cases/reports/index.ts @@ -0,0 +1,2 @@ +export * from './report.factory.service'; +export * from './report.use-case'; diff --git a/src/use-cases/reports/report.factory.service.ts b/src/use-cases/reports/report.factory.service.ts new file mode 100644 index 0000000..c6b5c9f --- /dev/null +++ b/src/use-cases/reports/report.factory.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { CreateReportDto, CreateReportsChannelDto } from 'src/core/dtos'; +import { Report, ReportsChannel } from 'src/core/entities'; + +@Injectable() +export class ReportFactoryService { + createReport(dto: CreateReportDto) { + return Report.create({ + description: dto.description, + reporter: { + id: dto.reporterId, + }, + user: { + id: dto.userId, + }, + }); + } + + createReportsChannel(dto: CreateReportsChannelDto) { + return ReportsChannel.create({ + ...dto, + }); + } +} diff --git a/src/use-cases/reports/report.use-case.module.ts b/src/use-cases/reports/report.use-case.module.ts new file mode 100644 index 0000000..57b8036 --- /dev/null +++ b/src/use-cases/reports/report.use-case.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ReportModule } from 'src/services/report/report.module'; + +import { ReportFactoryService } from './report.factory.service'; +import { ReportUseCases } from './report.use-case'; + +@Module({ + exports: [ReportUseCases], + imports: [ReportModule], + providers: [ReportFactoryService, ReportUseCases], +}) +export class ReportUseCasesModule {} diff --git a/src/use-cases/reports/report.use-case.ts b/src/use-cases/reports/report.use-case.ts new file mode 100644 index 0000000..1313bfe --- /dev/null +++ b/src/use-cases/reports/report.use-case.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { IReportService } from 'src/core/abstracts'; +import { CreateReportDto, CreateReportsChannelDto } from 'src/core/dtos'; + +import { ReportFactoryService } from './report.factory.service'; + +@Injectable() +export class ReportUseCases { + constructor( + private readonly reportService: IReportService, + private readonly reportFactoryService: ReportFactoryService, + ) {} + + async createReport(dto: CreateReportDto) { + const report = this.reportFactoryService.createReport(dto); + + return this.reportService.createReport(report); + } + + async createReportChannel(dto: CreateReportsChannelDto) { + const reportChannel = this.reportFactoryService.createReportsChannel(dto); + + return this.reportService.createReportsChannel(reportChannel); + } + + async deleteReport(id: number) { + return this.reportService.deleteReport(id); + } + + async findReportById(id: number) { + return this.reportService.findById(id); + } + + async findReportsChannel() { + return this.reportService.findReportsChannel(); + } +} diff --git a/src/use-cases/user/tests/user-factory.service.spec.ts b/src/use-cases/user/tests/user-factory.service.spec.ts index 6faa1d4..69a9882 100644 --- a/src/use-cases/user/tests/user-factory.service.spec.ts +++ b/src/use-cases/user/tests/user-factory.service.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CreateUserDto, UpdateUserDto } from 'src/core/dtos'; import { User } from 'src/core/entities'; + import { UserFactoryService } from '../user-factory.service'; jest.spyOn(User, 'create').mockImplementation((dto) => dto as User); diff --git a/src/use-cases/user/tests/user.use-case.spec.ts b/src/use-cases/user/tests/user.use-case.spec.ts index 542364e..c90a92b 100644 --- a/src/use-cases/user/tests/user.use-case.spec.ts +++ b/src/use-cases/user/tests/user.use-case.spec.ts @@ -4,6 +4,7 @@ import { IUserService } from 'src/core/abstracts'; import { CreateUserDto } from 'src/core/dtos'; import { User } from 'src/core/entities'; import { TypeOrmUserService } from 'src/frameworks/user/typeorm/typeorm-user.service'; + import { UserFactoryService } from '../user-factory.service'; import { UserUseCases } from '../user.use-case'; @@ -51,24 +52,6 @@ describe('UserUseCases', () => { }); }); - describe('update', () => { - it('should update an existing user and return it', async () => { - const dto: CreateUserDto = { id: 12345 }; - const user: User = createMock({ - ...dto, - }); - - jest.spyOn(factory, 'update').mockReturnValueOnce(user); - jest.spyOn(service, 'update').mockImplementationOnce(async () => user); - - const result = await useCases.update(user.id, dto); - - expect(factory.update).toHaveBeenCalledWith(dto); - expect(service.update).toHaveBeenCalledWith(user.id, user); - expect(result).toEqual(user); - }); - }); - describe('findById', () => { it('should find an existing user and return it', async () => { const id = 12345; diff --git a/src/use-cases/user/user.use-case.module.ts b/src/use-cases/user/user.use-case.module.ts index 1b05820..866b5bd 100644 --- a/src/use-cases/user/user.use-case.module.ts +++ b/src/use-cases/user/user.use-case.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { UserModule } from 'src/services'; + import { UserFactoryService } from './user-factory.service'; import { UserUseCases } from './user.use-case'; @Module({ + exports: [UserFactoryService, UserUseCases], imports: [UserModule], providers: [UserFactoryService, UserUseCases], - exports: [UserFactoryService, UserUseCases], }) export class UserUseCasesModule {} diff --git a/src/use-cases/user/user.use-case.ts b/src/use-cases/user/user.use-case.ts index 331958e..f30b66e 100644 --- a/src/use-cases/user/user.use-case.ts +++ b/src/use-cases/user/user.use-case.ts @@ -1,7 +1,8 @@ import { Injectable } from '@nestjs/common'; import { IUserService } from 'src/core/abstracts'; -import { CreateUserDto, UpdateUserDto } from 'src/core/dtos'; +import { CreateUserDto } from 'src/core/dtos'; import { User } from 'src/core/entities'; + import { UserFactoryService } from './user-factory.service'; @Injectable() @@ -17,13 +18,7 @@ export class UserUseCases { return this.userService.create(user); } - async update(userId: number, dto: UpdateUserDto) { - const user = this.userFactory.update(dto); - - return this.userService.update(userId, user); - } - - async findById(userId: number): Promise { + async findById(userId: number): Promise { return this.userService.findById(userId); } }