├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── dependabot-merge.yml ├── .gitignore ├── README.md ├── apps ├── backend │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── nest-cli.json │ ├── package.json │ ├── public │ ├── src │ │ ├── app.controller.spec.ts │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── auth │ │ │ ├── auth.controller.spec.ts │ │ │ ├── auth.controller.ts │ │ │ ├── auth.decorator.ts │ │ │ ├── auth.dto.ts │ │ │ ├── auth.guard.ts │ │ │ ├── auth.module.ts │ │ │ └── auth.service.ts │ │ ├── config │ │ │ ├── app.ts │ │ │ └── config.schema.ts │ │ ├── env.d.ts │ │ ├── event │ │ │ ├── event.controller.spec.ts │ │ │ ├── event.controller.ts │ │ │ └── event.module.ts │ │ ├── main.ts │ │ ├── message │ │ │ ├── message.controller.spec.ts │ │ │ ├── message.controller.ts │ │ │ ├── message.dto.ts │ │ │ ├── message.module.ts │ │ │ ├── message.service.spec.ts │ │ │ └── message.service.ts │ │ ├── session │ │ │ └── session.store.ts │ │ ├── system │ │ │ ├── system.controller.spec.ts │ │ │ ├── system.controller.ts │ │ │ ├── system.module.ts │ │ │ ├── system.service.spec.ts │ │ │ └── system.service.ts │ │ └── whatsapp │ │ │ ├── whatsapp.controller.spec.ts │ │ │ ├── whatsapp.controller.ts │ │ │ ├── whatsapp.module.ts │ │ │ ├── whatsapp.service.spec.ts │ │ │ └── whatsapp.service.ts │ ├── test │ │ ├── app.e2e-spec.ts │ │ └── jest-e2e.json │ ├── tsconfig.build.json │ └── tsconfig.json └── frontend │ ├── .gitignore │ ├── .vscode │ └── extensions.json │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ ├── 000m.jpg │ ├── login-bg.jpg │ ├── logo.svg │ └── vite.svg │ ├── src │ ├── App.vue │ ├── layouts │ │ └── Main.vue │ ├── main.ts │ ├── pages │ │ ├── Home │ │ │ ├── Home.vue │ │ │ └── Widgets │ │ │ │ ├── MemoryUsage.vue │ │ │ │ ├── RecentMessage.vue │ │ │ │ ├── StorageUsage.vue │ │ │ │ ├── UptimeConnection.vue │ │ │ │ ├── UptimeProcess.vue │ │ │ │ ├── WAConnection.vue │ │ │ │ └── WAProcess.vue │ │ ├── Login.vue │ │ └── NotFound.vue │ ├── router.ts │ ├── stores │ │ ├── useProcess.ts │ │ └── useToasts.ts │ ├── style.css │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── libraries ├── database │ ├── package.json │ ├── prisma │ │ └── schema.prisma │ ├── src │ │ ├── client.ts │ │ ├── index.ts │ │ └── seed.ts │ └── tsconfig.json └── whatsapp │ ├── package.json │ ├── src │ ├── commands │ │ ├── logout.ts │ │ ├── memoryUsage.ts │ │ ├── restart.ts │ │ ├── sendMessage.ts │ │ ├── start.ts │ │ └── stop.ts │ ├── main.ts │ └── whatsapp │ │ ├── index.ts │ │ ├── whatsapp.helper.ts │ │ ├── whatsapp.interface.ts │ │ ├── whatsapp.logger.ts │ │ ├── whatsapp.storage.ts │ │ └── whatsapp.ts │ └── tsconfig.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── screenshots └── home.jpg └── turbo.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 2 10 | root = true 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | open-pull-requests-limit: 30 8 | 9 | - package-ecosystem: 'npm' 10 | directory: '/' 11 | schedule: 12 | interval: 'daily' 13 | open-pull-requests-limit: 50 14 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Auto Merge Pull Request 2 | on: pull_request_target 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | contents: write 14 | issues: write 15 | pull-requests: write 16 | 17 | if: ${{ github.actor == 'dependabot[bot]' }} 18 | steps: 19 | - name: Dependabot metadata 20 | id: metadata 21 | uses: dependabot/fetch-metadata@v1.6.0 22 | with: 23 | github-token: '${{ secrets.GITHUB_TOKEN }}' 24 | 25 | - name: Auto-merge Dependabot PRs for semver-minor updates 26 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} 27 | run: gh pr merge --auto --merge "$PR_URL" 28 | env: 29 | PR_URL: ${{github.event.pull_request.html_url}} 30 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 31 | 32 | - name: Auto-merge Dependabot PRs for semver-patch updates 33 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 34 | run: gh pr merge --auto --merge "$PR_URL" 35 | env: 36 | PR_URL: ${{github.event.pull_request.html_url}} 37 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .turbo 2 | node_modules 3 | dist 4 | *.pem 5 | .env 6 | test.js 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Whatsapp Gateway

3 | (Under Development) 4 |
5 | 6 | ## Preview 7 | ![Homepage](./screenshots/home.jpg "Homepage") 8 | -------------------------------------------------------------------------------- /apps/backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | '@typescript-eslint/no-unused-vars': 'off', 25 | "@typescript-eslint/ban-types": "error" 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /apps/backend/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | !.env 38 | .env.local 39 | -------------------------------------------------------------------------------- /apps/backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /apps/backend/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ pnpm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ pnpm run start 40 | 41 | # watch mode 42 | $ pnpm run start:dev 43 | 44 | # production mode 45 | $ pnpm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ pnpm run test 53 | 54 | # e2e tests 55 | $ pnpm run test:e2e 56 | 57 | # test coverage 58 | $ pnpm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /apps/backend/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "dev": "nest start --watch", 13 | "debug": "nest start --debug --watch", 14 | "prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json", 21 | "prepare": "ts-patch install", 22 | "keygen": "node -p \"require('crypto').randomBytes(16).toString('hex')\"" 23 | }, 24 | "dependencies": { 25 | "@fastify/static": "^6.12.0", 26 | "@nestia/core": "^2.3.4", 27 | "@nestjs/axios": "^3.0.0", 28 | "@nestjs/common": "^10.0.0", 29 | "@nestjs/config": "^3.1.1", 30 | "@nestjs/core": "^10.0.0", 31 | "@nestjs/event-emitter": "^2.0.2", 32 | "@nestjs/platform-express": "^10.0.0", 33 | "@nestjs/platform-fastify": "^10.2.8", 34 | "@nestjs/serve-static": "^4.0.0", 35 | "argon2": "^0.31.2", 36 | "axios": "^1.5.1", 37 | "database": "workspace:^", 38 | "diskusage": "^1.2.0", 39 | "express-session": "^1.17.3", 40 | "fastify": "^4.24.3", 41 | "joi": "^17.11.0", 42 | "reflect-metadata": "^0.1.13", 43 | "rxjs": "^7.8.1", 44 | "whatsapp": "workspace:^" 45 | }, 46 | "devDependencies": { 47 | "@nestia/e2e": "^0.3.6", 48 | "@nestia/sdk": "^2.3.4", 49 | "@nestjs/cli": "^10.0.0", 50 | "@nestjs/schematics": "^10.0.0", 51 | "@nestjs/testing": "^10.0.0", 52 | "@types/express": "^4.17.17", 53 | "@types/express-session": "^1.17.10", 54 | "@types/jest": "^29.5.2", 55 | "@types/node": "^20.3.1", 56 | "@types/supertest": "^2.0.12", 57 | "@typescript-eslint/eslint-plugin": "^6.0.0", 58 | "@typescript-eslint/parser": "^6.0.0", 59 | "eslint": "^8.42.0", 60 | "eslint-config-prettier": "^9.0.0", 61 | "eslint-plugin-prettier": "^5.0.0", 62 | "jest": "^29.5.0", 63 | "nestia": "^5.0.3", 64 | "prettier": "^3.0.0", 65 | "source-map-support": "^0.5.21", 66 | "supertest": "^6.3.3", 67 | "ts-jest": "^29.1.0", 68 | "ts-loader": "^9.4.3", 69 | "ts-node": "^10.9.1", 70 | "ts-patch": "^3.0.2", 71 | "tsconfig-paths": "^4.2.0", 72 | "typescript": "^5.2.2", 73 | "typia": "^5.2.4" 74 | }, 75 | "jest": { 76 | "moduleFileExtensions": [ 77 | "js", 78 | "json", 79 | "ts" 80 | ], 81 | "rootDir": "src", 82 | "testRegex": ".*\\.spec\\.ts$", 83 | "transform": { 84 | "^.+\\.(t|j)s$": "ts-jest" 85 | }, 86 | "collectCoverageFrom": [ 87 | "**/*.(t|j)s" 88 | ], 89 | "coverageDirectory": "../coverage", 90 | "testEnvironment": "node" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /apps/backend/public: -------------------------------------------------------------------------------- 1 | ../frontend/dist -------------------------------------------------------------------------------- /apps/backend/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/backend/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Req } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | import { Request } from 'express'; 4 | 5 | @Controller() 6 | export class AppController { 7 | constructor(private readonly appService: AppService) {} 8 | 9 | @Get() 10 | getHello(@Req() req: Request): string { 11 | return this.appService.getHello(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/backend/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { WhatsappModule } from './whatsapp/whatsapp.module'; 5 | import { MessageModule } from './message/message.module'; 6 | import { EventEmitterModule } from '@nestjs/event-emitter'; 7 | import { EventModule } from './event/event.module'; 8 | import { join } from 'path'; 9 | import { ServeStaticModule } from '@nestjs/serve-static'; 10 | import { ConfigModule } from '@nestjs/config'; 11 | import { SystemModule } from './system/system.module'; 12 | import { AuthModule } from './auth/auth.module'; 13 | import AppConfig from './config/app'; 14 | import ConfigSchema from './config/config.schema'; 15 | import { APP_GUARD } from '@nestjs/core'; 16 | import { AuthGuard } from './auth/auth.guard'; 17 | 18 | @Module({ 19 | imports: [ 20 | ConfigModule.forRoot({ 21 | cache: true, 22 | envFilePath: ['.env.local', '.env'], 23 | isGlobal: true, 24 | load: [AppConfig], 25 | validationSchema: ConfigSchema, 26 | validationOptions: { 27 | allowUnknown: true, 28 | abortEarly: true, 29 | }, 30 | }), 31 | ServeStaticModule.forRoot({ 32 | rootPath: join(__dirname, '..', 'public'), 33 | }), 34 | EventEmitterModule.forRoot(), 35 | WhatsappModule, 36 | MessageModule, 37 | EventModule, 38 | SystemModule, 39 | AuthModule, 40 | ], 41 | controllers: [AppController], 42 | providers: [ 43 | AppService, 44 | { 45 | provide: APP_GUARD, 46 | useClass: AuthGuard, 47 | }, 48 | ], 49 | }) 50 | export class AppModule {} 51 | -------------------------------------------------------------------------------- /apps/backend/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/backend/src/auth/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthController } from './auth.controller'; 3 | 4 | describe('AuthController', () => { 5 | let controller: AuthController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [AuthController], 10 | }).compile(); 11 | 12 | controller = module.get(AuthController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/backend/src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Res, 4 | Req, 5 | ForbiddenException, 6 | Logger, 7 | HttpStatus, 8 | } from '@nestjs/common'; 9 | import { TypedRoute, TypedBody } from '@nestia/core'; 10 | import { SignInDto } from './auth.dto'; 11 | import { Request, Response } from 'express'; 12 | import { AuthService } from './auth.service'; 13 | import { Auth } from './auth.decorator'; 14 | 15 | @Controller('auth') 16 | export class AuthController { 17 | private readonly logger = new Logger(AuthController.name); 18 | 19 | constructor(private readonly auth: AuthService) {} 20 | 21 | @TypedRoute.Post('/login') 22 | /** 23 | * Signs in a user. 24 | * 25 | * @param {SignInDto} body - The sign-in data. 26 | * @param {Response} res - The response object. 27 | * @param {Request} req - The request object. 28 | * @return {Promise} - A promise that resolves when the sign-in process is complete. 29 | */ 30 | async signIn( 31 | @TypedBody() body: SignInDto, 32 | @Res() res: Response, 33 | @Req() req: Request, 34 | ) { 35 | try { 36 | const user = await this.auth.signIn({ 37 | username: body.username, 38 | password: body.password, 39 | }); 40 | 41 | req.session.user = { 42 | uuid: user.id, 43 | }; 44 | 45 | res.status(HttpStatus.OK).send({ 46 | message: 'Successfully signed in', 47 | }); 48 | } catch (error) { 49 | throw new ForbiddenException(error.message ?? 'Failed to sign in'); 50 | } 51 | } 52 | 53 | @Auth('session') 54 | @TypedRoute.Post('/isLoggedIn') 55 | /** 56 | * Checks if the user is logged in. 57 | * 58 | * @param {@Req()} req - The request object. 59 | * @param {@Res()} res - The response object. 60 | * @return {Promise} - The response with the isLoggedIn status. 61 | */ 62 | async isLoggedIn(@Req() req: Request, @Res() res: Response) { 63 | return res.status(HttpStatus.OK).send({ 64 | isLoggedIn: !!req.session.user, 65 | }); 66 | } 67 | 68 | @Auth('session') 69 | @TypedRoute.Post('/logout') 70 | /** 71 | * Destroys the session asynchronously. 72 | * 73 | * @param {@Req()} req - The request object. 74 | * @param {@Res()} res - The response object. 75 | * @return {Promise} A promise that resolves when the session is destroyed. 76 | */ 77 | async logout(@Req() req: Request, @Res() res: Response) { 78 | /** 79 | * Destroys the session asynchronously. 80 | * 81 | * @return {Promise} A promise that resolves when the session is destroyed. 82 | */ 83 | const destroy = () => { 84 | return new Promise((resolve, reject) => { 85 | req.session.destroy((err) => { 86 | resolve(); 87 | }); 88 | 89 | setTimeout(() => { 90 | this.logger.error('Failed to destroy session'); 91 | }, 15_000); 92 | }); 93 | }; 94 | 95 | await destroy().catch(() => {}); 96 | 97 | res.status(HttpStatus.OK).send({ 98 | message: 'Successfully logged out', 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /apps/backend/src/auth/auth.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | export type AuthType = 'session' | 'token' | 'all' | undefined | null; 4 | export const AUTH_TYPE: AuthType = 'session'; 5 | export const Auth = (authType: AuthType = 'session') => 6 | SetMetadata(AUTH_TYPE, authType); 7 | -------------------------------------------------------------------------------- /apps/backend/src/auth/auth.dto.ts: -------------------------------------------------------------------------------- 1 | import { tags } from 'typia'; 2 | 3 | export class SignInDto { 4 | /** 5 | * The username of the user to sign in. 6 | * 7 | */ 8 | username: string & tags.Pattern<'^[a-zA-Z0-9-_.]+$'> & tags.MinLength<4>; 9 | 10 | /** 11 | * The password of the user to sign in. 12 | * 13 | */ 14 | password: string & tags.MinLength<6>; 15 | } 16 | -------------------------------------------------------------------------------- /apps/backend/src/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Injectable, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { Observable } from 'rxjs'; 8 | import { Request } from 'express'; 9 | import { Reflector } from '@nestjs/core'; 10 | import { AUTH_TYPE, AuthType } from './auth.decorator'; 11 | 12 | @Injectable() 13 | export class AuthGuard implements CanActivate { 14 | constructor(private reflector: Reflector) {} 15 | 16 | /** 17 | * Determines whether the user can activate the current route. 18 | * 19 | * @param {ExecutionContext} context - The execution context. 20 | * @return {boolean | Promise | Observable} - Returns a boolean, a promise that resolves to a boolean, or an observable that emits a boolean. 21 | */ 22 | canActivate( 23 | context: ExecutionContext, 24 | ): boolean | Promise | Observable { 25 | const authType = this.reflector.getAllAndOverride(AUTH_TYPE, [ 26 | context.getHandler(), 27 | context.getClass(), 28 | ]); 29 | 30 | if (!authType) { 31 | return true; 32 | } 33 | 34 | const request = context.switchToHttp().getRequest(); 35 | 36 | const isSessionValid = () => (request.session.user?.uuid ? true : false); 37 | const isTokenValid = () => false; 38 | 39 | switch (authType) { 40 | case 'session': 41 | if (!isSessionValid()) 42 | throw new UnauthorizedException('User must be logged in'); 43 | break; 44 | 45 | case 'token': 46 | if (!isTokenValid()) 47 | throw new UnauthorizedException('Token must be provided and valid'); 48 | break; 49 | 50 | case 'all': 51 | const validSession = isSessionValid(); 52 | const token = isTokenValid(); 53 | if (!(validSession || token)) 54 | throw new UnauthorizedException( 55 | 'User must be logged in or Token must be provided and valid', 56 | ); 57 | 58 | break; 59 | } 60 | 61 | if (authType === 'session') { 62 | return isSessionValid(); 63 | } else if (authType === 'token') { 64 | return isTokenValid(); 65 | } else if (authType === 'all') { 66 | return isSessionValid() || isTokenValid(); 67 | } 68 | 69 | return true; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /apps/backend/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { AuthController } from './auth.controller'; 4 | 5 | @Module({ 6 | providers: [AuthService], 7 | controllers: [AuthController], 8 | }) 9 | export class AuthModule {} 10 | -------------------------------------------------------------------------------- /apps/backend/src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { SignInDto } from './auth.dto'; 3 | import { prisma } from 'database'; 4 | import { verify } from 'argon2'; 5 | 6 | @Injectable() 7 | export class AuthService { 8 | /** 9 | * Sign in a user. 10 | * 11 | * @param {SignInDto} params - The sign-in parameters. 12 | * @return {Promise} The signed-in user. 13 | */ 14 | async signIn(params: SignInDto) { 15 | const user = await prisma.user.findFirst({ 16 | where: { 17 | username: params.username, 18 | }, 19 | }); 20 | 21 | if (user === null) { 22 | throw new Error(`User ${params.username} not found`); 23 | } else if ((await verify(user.password, params.password)) === false) { 24 | throw new Error('Wrong password'); 25 | } 26 | 27 | return user; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/backend/src/config/app.ts: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | port: parseInt(process.env.PORT ?? '3000', 10), 3 | hostname: process.env.HOSTNAME ?? '0.0.0.0', 4 | encryption: process.env.ENCRYPTION_KEY ?? '', 5 | frontend: { 6 | url: process.env.FRONTEND_URL?.split(',') ?? null, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /apps/backend/src/config/config.schema.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | 3 | export default Joi.object({ 4 | PORT: Joi.number().default(3000), 5 | HOST: Joi.string().ip().default('0.0.0.0'), 6 | ENCRYPTION_KEY: Joi.string().required(), 7 | FRONTEND_URL: Joi.string().required(), 8 | }); 9 | -------------------------------------------------------------------------------- /apps/backend/src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'express-session' { 2 | interface SessionData { 3 | user?: { 4 | uuid: string; 5 | } | null; 6 | } 7 | } 8 | 9 | export {}; 10 | -------------------------------------------------------------------------------- /apps/backend/src/event/event.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { EventController } from './event.controller'; 3 | 4 | describe('EventController', () => { 5 | let controller: EventController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [EventController], 10 | }).compile(); 11 | 12 | controller = module.get(EventController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/backend/src/event/event.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Sse, MessageEvent } from '@nestjs/common'; 2 | import { EventEmitter2 } from '@nestjs/event-emitter'; 3 | import { Observable, fromEvent, map, merge } from 'rxjs'; 4 | import { WhatsappService } from '../whatsapp/whatsapp.service'; 5 | 6 | @Controller('event') 7 | export class EventController { 8 | constructor( 9 | private readonly event: EventEmitter2, 10 | private readonly whatsapp: WhatsappService, 11 | ) {} 12 | 13 | @Sse('qr') 14 | getQr(): Observable { 15 | return fromEvent(this.event, 'qr.update').pipe( 16 | map((data: string) => { 17 | return new MessageEvent('message', { data }); 18 | }), 19 | ); 20 | } 21 | 22 | @Sse('connection-state') 23 | getConnectionState(): Observable { 24 | const connectionUpdate = fromEvent(this.event, 'connection.update'); 25 | const processState = fromEvent(this.event, 'process.state'); 26 | return merge(connectionUpdate, processState).pipe( 27 | map(() => { 28 | const { 29 | workerStartedAt, 30 | connectedAt, 31 | workerState, 32 | connectionState, 33 | hasSession, 34 | } = this.whatsapp; 35 | 36 | return new MessageEvent('message', { 37 | data: { 38 | connectionState, 39 | connectedAt, 40 | workerState, 41 | workerStartedAt, 42 | hasSession, 43 | }, 44 | }); 45 | }), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/backend/src/event/event.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EventController } from './event.controller'; 3 | 4 | @Module({ 5 | controllers: [EventController], 6 | }) 7 | export class EventModule {} 8 | -------------------------------------------------------------------------------- /apps/backend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import * as session from 'express-session'; 5 | import { NestExpressApplication } from '@nestjs/platform-express'; 6 | import { SessionStore } from './session/session.store'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule); 10 | const config = app.get(ConfigService); 11 | 12 | app.set('trust proxy', 1); 13 | 14 | app.use( 15 | session({ 16 | secret: config.getOrThrow('encryption'), 17 | resave: false, 18 | rolling: true, 19 | saveUninitialized: false, 20 | cookie: { 21 | httpOnly: true, 22 | secure: 'auto', 23 | maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days 24 | }, 25 | name: 'wases', 26 | store: new SessionStore(), 27 | }), 28 | ); 29 | 30 | app.enableCors({ 31 | origin: config.get('frontend.url') ?? true, 32 | credentials: true, 33 | }); 34 | 35 | app.enableShutdownHooks(); 36 | 37 | app.setGlobalPrefix('api'); 38 | 39 | await app.listen(3000, '0.0.0.0'); 40 | } 41 | bootstrap(); 42 | -------------------------------------------------------------------------------- /apps/backend/src/message/message.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MessageController } from './message.controller'; 3 | 4 | describe('MessageController', () => { 5 | let controller: MessageController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [MessageController], 10 | }).compile(); 11 | 12 | controller = module.get(MessageController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/backend/src/message/message.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpException, Res } from '@nestjs/common'; 2 | import { TypedRoute, TypedBody } from '@nestia/core'; 3 | import { MessageService } from './message.service'; 4 | import type { Response } from 'express'; 5 | import { ISendMediaMessage, ISendTextMessage } from './message.dto'; 6 | 7 | @Controller('message') 8 | export class MessageController { 9 | constructor(private readonly message: MessageService) {} 10 | 11 | @TypedRoute.Post('/sendTextMessage') 12 | /** 13 | * Send a text message. 14 | * 15 | * @param {Response} res - The response object. 16 | * @param {ISendTextMessage} body - The body of the message. 17 | * @return {Promise} - A promise that resolves to void. 18 | */ 19 | async sendTextMessage( 20 | @Res() res: Response, 21 | @TypedBody() body: ISendTextMessage, 22 | ) { 23 | try { 24 | const result = await this.message.sendTextMessage({ 25 | to: body.to, 26 | message: body.message, 27 | }); 28 | 29 | return res.send(result); 30 | } catch (error) { 31 | throw new HttpException( 32 | error.message ?? error ?? 'Failed to send message', 33 | 500, 34 | ); 35 | } 36 | } 37 | 38 | @TypedRoute.Post('/sendMediaMessage') 39 | async sendMediaMessage( 40 | @Res() res: Response, 41 | @TypedBody() body: ISendMediaMessage, 42 | ) { 43 | try { 44 | const result = await this.message.sendMediaMessage(body); 45 | return res.send({ 46 | message: result, 47 | }); 48 | } catch (error) { 49 | throw new HttpException( 50 | error.message ?? error ?? 'Failed to send message', 51 | 500, 52 | ); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /apps/backend/src/message/message.dto.ts: -------------------------------------------------------------------------------- 1 | export interface ISendTextMessage { 2 | /** 3 | * The phone number of the recipient. 4 | */ 5 | to: string; 6 | 7 | /** 8 | * The message to be sent. 9 | */ 10 | message: string; 11 | } 12 | 13 | export interface ISendMediaMessage { 14 | /** 15 | * The phone number of the recipient. 16 | */ 17 | to: string; 18 | 19 | /** 20 | * The message to be sent. 21 | */ 22 | message: string; 23 | 24 | /** 25 | * The media to be sent. 26 | */ 27 | mediaUrl: string; 28 | 29 | /** 30 | * The media type. 31 | * 32 | * @default 'auto' auto detect mime-types and set to document for fallback 33 | */ 34 | mediaType?: 'audio' | 'image' | 'video' | 'document' | 'auto'; 35 | 36 | /** 37 | * The name of the file 38 | * only for document type 39 | */ 40 | fileName?: string; 41 | } 42 | -------------------------------------------------------------------------------- /apps/backend/src/message/message.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MessageService } from './message.service'; 3 | import { MessageController } from './message.controller'; 4 | import { HttpModule } from '@nestjs/axios'; 5 | 6 | @Module({ 7 | imports: [HttpModule], 8 | providers: [MessageService], 9 | controllers: [MessageController], 10 | }) 11 | export class MessageModule {} 12 | -------------------------------------------------------------------------------- /apps/backend/src/message/message.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MessageService } from './message.service'; 3 | 4 | describe('MessageService', () => { 5 | let service: MessageService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [MessageService], 10 | }).compile(); 11 | 12 | service = module.get(MessageService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/backend/src/message/message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { WhatsappService } from '../whatsapp/whatsapp.service'; 3 | import { ISendMediaMessage, ISendTextMessage } from './message.dto'; 4 | import { HttpService } from '@nestjs/axios'; 5 | import { writeFileSync, mkdirSync, existsSync, rmSync } from 'fs'; 6 | import { resolve as pathResolve } from 'path'; 7 | 8 | @Injectable() 9 | export class MessageService { 10 | constructor( 11 | private readonly whatsapp: WhatsappService, 12 | private readonly http: HttpService, 13 | ) {} 14 | 15 | /** 16 | * Sends a text message. 17 | * 18 | * @param {ISendTextMessage} to - The recipient of the message. 19 | * @return {Promise} - A promise that resolves to the response of the send command. 20 | */ 21 | async sendTextMessage({ to, message }: ISendTextMessage): Promise { 22 | try { 23 | // Limit the message to 4096 characters 24 | message = message.substring(0, 4096 - 1); 25 | const res = await this.whatsapp.sendCommand('SEND_MESSAGE', { 26 | to, 27 | data: { 28 | text: message, 29 | }, 30 | }); 31 | 32 | return res; 33 | } catch (error) { 34 | throw new Error(error.message ?? error ?? 'Failed to send message'); 35 | } 36 | } 37 | 38 | /** 39 | * Sends a media message. 40 | * 41 | * @param {ISendMediaMessage} sendMediaMessage - Object containing the parameters for the media message. 42 | * @param {string} sendMediaMessage.to - The recipient of the media message. 43 | * @param {string} sendMediaMessage.mediaUrl - The URL of the media file to be sent. 44 | * @param {string} sendMediaMessage.message - The message to be sent along with the media file. 45 | * @param {string} sendMediaMessage.fileName - The name of the media file. 46 | * @param {string} sendMediaMessage.mediaType - The type of media (audio, image, video, document). 47 | * @return {Promise} Resolves with the response data if the message is sent successfully. 48 | * @throws {Error} Throws an error if the message fails to send. 49 | */ 50 | async sendMediaMessage({ 51 | to, 52 | mediaUrl, 53 | message, 54 | fileName, 55 | mediaType, 56 | }: ISendMediaMessage): Promise { 57 | const res = await this.detectMimeTypes(mediaUrl); 58 | const mimeTypes = res.mimeTypes ?? null; 59 | if (!mediaType || mediaType == 'auto') { 60 | mediaType = res.type as any; 61 | } 62 | 63 | let data: any = {}; 64 | const mediaPath = new URL(mediaUrl); 65 | 66 | if (mediaType == 'audio') { 67 | data = { 68 | audio: { 69 | url: mediaPath, 70 | mimetype: mimeTypes, 71 | }, 72 | }; 73 | } else if (mediaType == 'image') { 74 | data = { 75 | image: { 76 | url: mediaPath, 77 | mimetype: mimeTypes, 78 | }, 79 | caption: message, 80 | }; 81 | } else if (mediaType == 'video') { 82 | data = { 83 | video: { 84 | url: mediaPath, 85 | mimetype: mimeTypes, 86 | }, 87 | caption: message, 88 | }; 89 | } else { 90 | data = { 91 | document: { 92 | url: mediaPath, 93 | mimetype: mimeTypes, 94 | }, 95 | caption: message, 96 | mimetype: mimeTypes, 97 | fileName, 98 | }; 99 | } 100 | 101 | try { 102 | // Limit the message to 4096 characters 103 | message = message.substring(0, 4096 - 1); 104 | const res = await this.whatsapp.sendCommand('SEND_MESSAGE', { 105 | to, 106 | data, 107 | }); 108 | 109 | if (res.status) { 110 | return res.data; 111 | } 112 | throw new Error(res.data); 113 | } catch (error) { 114 | rmSync(mediaPath); 115 | throw new Error(error.message ?? error ?? 'Failed to send message'); 116 | } 117 | } 118 | 119 | /** 120 | * Detects the MIME type of a given URL. 121 | * 122 | * @param {string} url - The URL to detect the MIME type for. 123 | * @return {Promise<{ type: string, mimeTypes: string }>} - The detected MIME type and its corresponding type. 124 | */ 125 | async detectMimeTypes( 126 | url: string, 127 | ): Promise<{ type: string; mimeTypes: string }> { 128 | const header = (await this.http.axiosRef.head(url)).headers; 129 | const fileType = 130 | header?.['Content-Type']?.toString() || 131 | header?.['content-type']?.toString(); 132 | 133 | const imageMimeType = ['image/gif', 'image/jpeg', 'image/png']; 134 | const videoMimeType = [ 135 | 'video/mpeg', 136 | 'video/mp4', 137 | 'video/quicktime', 138 | 'video/x-ms-wmv', 139 | 'video/x-msvideo', 140 | 'video/x-flv', 141 | 'video/webm', 142 | ]; 143 | const audioMimeType = [ 144 | 'audio/mpeg', 145 | 'audio/x-ms-wma', 146 | 'audio/vnd.rn-realaudio', 147 | 'audio/x-wav', 148 | ]; 149 | 150 | if (!fileType) { 151 | return { 152 | type: 'document', 153 | mimeTypes: fileType, 154 | }; 155 | } else if (imageMimeType.includes(fileType)) { 156 | return { 157 | type: 'image', 158 | mimeTypes: fileType, 159 | }; 160 | } else if (videoMimeType.includes(fileType)) { 161 | return { 162 | type: 'video', 163 | mimeTypes: fileType, 164 | }; 165 | } else if (audioMimeType.includes(fileType)) { 166 | return { 167 | type: 'audio', 168 | mimeTypes: fileType, 169 | }; 170 | } 171 | 172 | return { 173 | type: 'document', 174 | mimeTypes: fileType, 175 | }; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /apps/backend/src/session/session.store.ts: -------------------------------------------------------------------------------- 1 | import { SessionData, Store } from 'express-session'; 2 | import { prisma, Prisma } from 'database'; 3 | 4 | /** 5 | * An object containing a list of sessions where the key is the 6 | * ID of the session and the value is the SessionData 7 | */ 8 | export interface ISessions { 9 | [key: string]: SessionData; 10 | } 11 | 12 | /** 13 | * Runs a callback with a number of arguments on the next tick 14 | * 15 | * @param callback the function to run in the future 16 | * @param args the arguments for the `callback` function when it is run 17 | */ 18 | export const defer = void, A extends unknown[]>( 19 | callback: T, 20 | ...args: A 21 | ) => { 22 | setImmediate(() => { 23 | callback(...args); 24 | }); 25 | }; 26 | 27 | export class SessionStore extends Store { 28 | private readonly model = prisma.userSession; 29 | private readonly logger = console; 30 | private checkInterval?: NodeJS.Timeout; 31 | 32 | constructor() { 33 | super(); 34 | this.startInterval(); 35 | } 36 | 37 | /** 38 | * Starts an interval for running a function periodically. 39 | * 40 | * @param {Function} onIntervalError - Optional callback function to handle errors that occur during the interval. 41 | * @return {void} This function does not return a value. 42 | */ 43 | public startInterval(onIntervalError?: (err: unknown) => void): void { 44 | if (this.checkInterval) return; 45 | 46 | const ms = 1000 * 60 * 60 * 24; // 1 day 47 | this.stopInterval(); 48 | this.checkInterval = setInterval(async () => { 49 | try { 50 | await this.prune(); 51 | } catch (err: unknown) { 52 | if (onIntervalError !== undefined) onIntervalError(err); 53 | } 54 | }, Math.floor(ms)); 55 | } 56 | 57 | /** 58 | * Stops the interval. 59 | * 60 | * @return {void} No return value. 61 | */ 62 | public stopInterval(): void { 63 | if (this.checkInterval) clearInterval(this.checkInterval); 64 | } 65 | 66 | async prune() { 67 | this.logger.log('Checking for any expired sessions...'); 68 | const sessions = await this.model.findMany({ 69 | select: { 70 | expiresAt: true, 71 | sid: true, 72 | }, 73 | }); 74 | 75 | const p = this.model; 76 | for (const session of sessions) { 77 | const now = new Date(); 78 | const remainingSec = (session.expiresAt.valueOf() - now.valueOf()) / 1000; 79 | this.logger.log(`session:${session.sid} expires in ${remainingSec}sec`); 80 | if (now.valueOf() >= session.expiresAt.valueOf()) { 81 | const sid = session.sid; 82 | this.logger.log(`Deleting session with sid: ${sid}`); 83 | const foundSession = await p.findUnique({ where: { sid } }); 84 | if (foundSession !== null) await p.delete({ where: { sid } }); 85 | } 86 | } 87 | } 88 | 89 | async all( 90 | callback?: (err?: unknown, all?: ISessions) => void, 91 | ): Promise { 92 | try { 93 | const sessions = await this.model.findMany({ 94 | select: { sid: true, data: true }, 95 | }); 96 | 97 | const result = sessions 98 | .map(({ sid, data }) => [sid, data as unknown as SessionData] as const) 99 | .reduce( 100 | (prev, [sid, data]) => ({ ...prev, [sid]: data }), 101 | {}, 102 | ); 103 | 104 | if (callback) defer(callback, undefined, result); 105 | 106 | return result; 107 | } catch (e: unknown) { 108 | this.logger.error(`all(): ${String(e)}`); 109 | if (callback) defer(callback, e); 110 | } 111 | } 112 | 113 | /** 114 | * Clears the data in the model. 115 | * 116 | * @param {Function} [callback] - An optional callback function to be called when the operation is complete or an error occurs. 117 | * @return {Promise} - A promise that resolves when the data is cleared. 118 | */ 119 | async clear(callback?: (err?: unknown) => void): Promise { 120 | try { 121 | await this.model.deleteMany(); 122 | 123 | if (callback) defer(callback); 124 | } catch (e: unknown) { 125 | if (callback) defer(callback, e); 126 | } 127 | } 128 | 129 | /** 130 | * Destroys the session(s) identified by the given session ID(s). 131 | * 132 | * @param {string|string[]} sid - The session ID(s) to destroy. 133 | * @param {(err?: unknown) => void} [callback] - An optional callback function to be called after the session(s) have been destroyed. 134 | * @return {Promise} A promise that resolves when the session(s) have been destroyed. 135 | */ 136 | async destroy( 137 | sid: string | string[], 138 | callback?: (err?: unknown) => void, 139 | ): Promise { 140 | try { 141 | if (Array.isArray(sid)) { 142 | await Promise.all(sid.map(async (id) => this.destroy(id, callback))); 143 | } else { 144 | // Calling deleteMany to prevent an error from being thrown. Fix for issue 91 145 | await this.model.deleteMany({ 146 | where: { sid }, 147 | }); 148 | } 149 | } catch (e: unknown) { 150 | // NOTE: Attempts to delete non-existent sessions land here 151 | if (callback) defer(callback, e); 152 | 153 | return; 154 | } 155 | 156 | if (callback) defer(callback); 157 | } 158 | 159 | /** 160 | * Retrieves a session data object based on the session ID. 161 | * 162 | * @param {string} sid - The session ID. 163 | * @param {(err?: unknown, val?: SessionData) => void} [callback] - Optional callback function. 164 | * @return {Promise} A Promise that resolves to the session data object, or undefined if the session does not exist. 165 | */ 166 | async get( 167 | sid: string, 168 | callback?: (err?: unknown, val?: SessionData) => void, 169 | ): Promise { 170 | const p = this.model; 171 | 172 | const session = await p 173 | .findUnique({ 174 | where: { sid }, 175 | }) 176 | .catch(() => null); 177 | 178 | if (session === null) { 179 | callback?.(); 180 | return undefined; 181 | } 182 | 183 | try { 184 | // If session has has expired (allowing for missing 'expiresAt' and 'sid' fields) 185 | if ( 186 | session.sid && 187 | session.expiresAt && 188 | new Date().valueOf() >= session.expiresAt.valueOf() 189 | ) { 190 | this.logger.log(`Session with sid: ${sid} expired; deleting.`); 191 | await p.delete({ where: { sid } }); 192 | callback?.(); 193 | return undefined; 194 | } 195 | 196 | const result = session.data as unknown as SessionData; 197 | if (callback) defer(callback, undefined, result); 198 | 199 | return result; 200 | } catch (e: unknown) { 201 | this.logger.error(`get(): ${String(e)}`); 202 | if (callback) defer(callback, e); 203 | } 204 | } 205 | 206 | /** 207 | * Retrieves the length of the array and optionally calls the provided callback with the result. 208 | * 209 | * @param {function} callback - An optional callback function that will be called with the result or an error. 210 | * @param {unknown} err - The error object passed to the callback function if an error occurred. 211 | * @param {number} length - The length of the array. 212 | * @return {Promise} - A promise that resolves to the length of the array or undefined. 213 | */ 214 | async length( 215 | callback?: (err: unknown, length: number) => void, 216 | ): Promise { 217 | try { 218 | const sessions = await this.model.findMany({ 219 | select: { sid: true }, // Limit what gets sent back; can't be empty. 220 | }); 221 | 222 | const itemCount = sessions.length; 223 | if (callback) defer(callback, undefined, itemCount); 224 | 225 | return itemCount; 226 | } catch (e: unknown) { 227 | if (callback) defer(callback, e, 0); 228 | } 229 | } 230 | 231 | /** 232 | * Sets a session data for a given session ID. 233 | * 234 | * @param {string} sid - The session ID. 235 | * @param {SessionData} session - The session data. 236 | * @param {(err?: unknown) => void} [callback] - An optional callback function. 237 | * @return {Promise} - A promise that resolves when the session data is set. 238 | */ 239 | async set( 240 | sid: string, 241 | session: SessionData, 242 | callback?: (err?: unknown) => void, 243 | ): Promise { 244 | const expiresAt = session.cookie.expires ?? new Date(); 245 | 246 | const data: Prisma.UserSessionCreateInput = { 247 | sid, 248 | expiresAt, 249 | data: session as unknown as Prisma.InputJsonObject, 250 | }; 251 | 252 | try { 253 | await this.model.upsert({ 254 | where: { 255 | sid, 256 | }, 257 | update: data, 258 | create: data, 259 | }); 260 | } catch (error) { 261 | this.logger.error(`set(): ${String(error)}`); 262 | if (callback) defer(callback, error); 263 | } 264 | 265 | if (callback) defer(callback); 266 | } 267 | 268 | /** 269 | * Updates the session data and expiresAt timestamp for a given session ID. 270 | * 271 | * @param {string} sid - The session ID. 272 | * @param {SessionData} session - The session data. 273 | * @param {(err?: unknown) => void} [callback] - An optional callback function. 274 | * @return {Promise} - A Promise that resolves when the update is complete. 275 | */ 276 | async touch( 277 | sid: string, 278 | session: SessionData, 279 | callback?: (err?: unknown) => void, 280 | ): Promise { 281 | const expiresAt = session.cookie.expires ?? new Date(); 282 | 283 | const data: Prisma.UserSessionCreateInput = { 284 | sid, 285 | expiresAt, 286 | data: session as unknown as Prisma.InputJsonObject, 287 | }; 288 | 289 | try { 290 | await this.model.upsert({ 291 | where: { 292 | sid, 293 | }, 294 | update: data, 295 | create: data, 296 | }); 297 | } catch (error) { 298 | this.logger.error(`touch(): ${String(error)}`); 299 | if (callback) defer(callback, error); 300 | } 301 | 302 | if (callback) defer(callback); 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /apps/backend/src/system/system.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SystemController } from './system.controller'; 3 | 4 | describe('SystemController', () => { 5 | let controller: SystemController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [SystemController], 10 | }).compile(); 11 | 12 | controller = module.get(SystemController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/backend/src/system/system.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Res } from '@nestjs/common'; 2 | import { TypedRoute } from '@nestia/core'; 3 | import { Response } from 'express'; 4 | import { SystemService } from './system.service'; 5 | 6 | @Controller('system') 7 | export class SystemController { 8 | constructor(private readonly system: SystemService) {} 9 | 10 | @TypedRoute.Get('/disk') 11 | async diskUsage(@Res() res: Response) { 12 | const data = await this.system.diskUsage(); 13 | res.send(data); 14 | } 15 | 16 | @TypedRoute.Get('/memory') 17 | async memoryUsage(@Res() res: Response) { 18 | const data = await this.system.memoryUsage(); 19 | res.send(data); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/backend/src/system/system.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common'; 2 | import { SystemService } from './system.service'; 3 | import { SystemController } from './system.controller'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [SystemService], 8 | controllers: [SystemController], 9 | }) 10 | export class SystemModule {} 11 | -------------------------------------------------------------------------------- /apps/backend/src/system/system.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SystemService } from './system.service'; 3 | 4 | describe('SystemService', () => { 5 | let service: SystemService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [SystemService], 10 | }).compile(); 11 | 12 | service = module.get(SystemService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/backend/src/system/system.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { WhatsappService } from '../whatsapp/whatsapp.service'; 3 | import { check, DiskUsage } from 'diskusage'; 4 | import { totalmem, freemem } from 'os'; 5 | import { memoryUsage } from 'process'; 6 | 7 | @Injectable() 8 | export class SystemService { 9 | constructor(private readonly whatsapp: WhatsappService) {} 10 | 11 | /** 12 | * Calculates the disk usage of the system. 13 | * 14 | * @return {Promise} The total disk usage in bytes. 15 | */ 16 | async diskUsage(): Promise { 17 | return check('/'); 18 | } 19 | 20 | /** 21 | * Retrieves the memory usage of the application and server. 22 | * 23 | * @returns {object} The memory usage information. 24 | * - `process`: The memory usage of the application process. 25 | * - `server`: The memory usage of the server. 26 | * - `other`: The memory usage of other resources. 27 | * - `total`: The total memory available. 28 | */ 29 | async memoryUsage() { 30 | const processUsage = ( 31 | await this.whatsapp.sendCommand('MEMORY_USAGE') 32 | ).data; 33 | const serverUsage = memoryUsage.rss(); 34 | const total = totalmem(); 35 | const other = freemem() - serverUsage - serverUsage; 36 | 37 | return { 38 | process: processUsage, 39 | server: serverUsage, 40 | other: other, 41 | total: total, 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/backend/src/whatsapp/whatsapp.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { WhatsappController } from './whatsapp.controller'; 3 | 4 | describe('WhatsappController', () => { 5 | let controller: WhatsappController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [WhatsappController], 10 | }).compile(); 11 | 12 | controller = module.get(WhatsappController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/backend/src/whatsapp/whatsapp.controller.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Controller, Res } from '@nestjs/common'; 2 | import { TypedRoute } from '@nestia/core'; 3 | import { WhatsappService } from './whatsapp.service'; 4 | import { Response } from 'express'; 5 | 6 | @Controller('whatsapp') 7 | export class WhatsappController { 8 | constructor(private whatsapp: WhatsappService) {} 9 | 10 | @TypedRoute.Get('/') 11 | async index(@Res() res: Response) { 12 | const { 13 | workerStartedAt, 14 | connectedAt, 15 | workerState, 16 | connectionState, 17 | hasSession, 18 | qr, 19 | } = this.whatsapp; 20 | 21 | return res.send({ 22 | connectionState, 23 | connectedAt, 24 | workerState, 25 | workerStartedAt, 26 | hasSession, 27 | qr, 28 | }); 29 | } 30 | 31 | @TypedRoute.Get('/start') 32 | /** 33 | * Starts the asynchronous operation. 34 | * 35 | * @return {Promise<{ status: boolean }>} A promise that resolves with an object containing the status. 36 | * @throws {HttpException} If an error occurs during the operation. 37 | */ 38 | async start() { 39 | try { 40 | await this.whatsapp.start(); 41 | 42 | return { 43 | status: true, 44 | }; 45 | } catch (error) { 46 | throw new HttpException( 47 | { 48 | message: error?.message ?? 'Failed to start connection', 49 | }, 50 | 500, 51 | ); 52 | } 53 | } 54 | 55 | @TypedRoute.Get('/stop') 56 | /** 57 | * Stops the execution of the function. 58 | * 59 | * @return {Promise<{ status: boolean }>} An object indicating the status of the function execution. 60 | * @throws {HttpException} If there was an error stopping the connection. 61 | */ 62 | async stop() { 63 | try { 64 | await this.whatsapp.stop(); 65 | 66 | return { 67 | status: true, 68 | }; 69 | } catch (error) { 70 | throw new HttpException( 71 | { 72 | message: error?.message ?? 'Failed to stop connection', 73 | }, 74 | 500, 75 | ); 76 | } 77 | } 78 | 79 | @TypedRoute.Get('restart') 80 | /** 81 | * Restarts the function by stopping and then starting it. 82 | * 83 | * @throws {HttpException} If there is an error during restart, a 500 HTTP exception is thrown with a message. 84 | */ 85 | async restart() { 86 | try { 87 | await this.stop(); 88 | await this.start(); 89 | } catch (error) { 90 | throw new HttpException( 91 | { 92 | message: error?.message ?? 'Failed to restart connection', 93 | }, 94 | 500, 95 | ); 96 | } 97 | } 98 | 99 | @TypedRoute.Get('connect') 100 | /** 101 | * Asynchronously connects to the worker. 102 | * 103 | * @return {Promise<{ status: boolean }>} An object with the status of the connection. 104 | * @throws {HttpException} If failed to connect to the worker. 105 | */ 106 | async connect() { 107 | try { 108 | await this.whatsapp.connect(); 109 | 110 | return { 111 | status: true, 112 | }; 113 | } catch (error) { 114 | throw new HttpException( 115 | { 116 | message: error?.message ?? 'Failed to connect to the worker', 117 | }, 118 | 500, 119 | ); 120 | } 121 | } 122 | 123 | @TypedRoute.Get('disconnect') 124 | /** 125 | * Disconnects from the worker. 126 | * 127 | * @return {Promise<{ status: boolean }>} The status of the disconnection. 128 | * @throws {HttpException} If there was an error disconnecting from the worker. 129 | */ 130 | async disconnect() { 131 | try { 132 | await this.whatsapp.disconnect(); 133 | 134 | return { 135 | status: true, 136 | }; 137 | } catch (error) { 138 | throw new HttpException( 139 | { 140 | message: error?.message ?? 'Failed to disconnect from the worker', 141 | }, 142 | 500, 143 | ); 144 | } 145 | } 146 | 147 | @TypedRoute.Get('reconnect') 148 | /** 149 | * Reconnects to the worker. 150 | * 151 | * @return {Promise} A promise that resolves when the reconnection is successful. 152 | * @throws {HttpException} If there is an error during reconnection. 153 | */ 154 | async reconnect() { 155 | try { 156 | await this.disconnect(); 157 | await this.connect(); 158 | } catch (error) { 159 | throw new HttpException( 160 | { 161 | message: error?.message ?? 'Failed to reconnect to the worker', 162 | }, 163 | 500, 164 | ); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /apps/backend/src/whatsapp/whatsapp.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common'; 2 | import { WhatsappService } from './whatsapp.service'; 3 | import { WhatsappController } from './whatsapp.controller'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [WhatsappService], 8 | controllers: [WhatsappController], 9 | exports: [WhatsappService], 10 | }) 11 | export class WhatsappModule {} 12 | -------------------------------------------------------------------------------- /apps/backend/src/whatsapp/whatsapp.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { WhatsappService } from './whatsapp.service'; 3 | 4 | describe('WhatsappService', () => { 5 | let service: WhatsappService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [WhatsappService], 10 | }).compile(); 11 | 12 | service = module.get(WhatsappService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/backend/src/whatsapp/whatsapp.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Logger, 3 | Injectable, 4 | OnModuleInit, 5 | OnModuleDestroy, 6 | } from '@nestjs/common'; 7 | import { fork, type ChildProcess } from 'child_process'; 8 | import { resolve as pathResolve } from 'path'; 9 | import { EventEmitter2 } from '@nestjs/event-emitter'; 10 | import { prisma } from 'database'; 11 | 12 | interface ISendCommand { 13 | command: string; 14 | status: boolean; 15 | data: K; 16 | } 17 | 18 | @Injectable() 19 | export class WhatsappService implements OnModuleInit, OnModuleDestroy { 20 | private _worker: ChildProcess | null = null; 21 | private workerPath: string; 22 | private logger = new Logger(WhatsappService.name); 23 | private _connectionState: 'open' | 'connecting' | 'close' = 'close'; 24 | private _workerStartedAt: Date | null = null; 25 | private _connectedAt: Date | null = null; 26 | private _hasSession = false; 27 | private _qr: string | null = null; 28 | 29 | /** 30 | * Constructor for the class. 31 | * 32 | */ 33 | constructor(private readonly event: EventEmitter2) { 34 | const home = pathResolve(process.cwd(), '..', '..'); 35 | this.workerPath = pathResolve(home, 'libraries/whatsapp/dist/main.js'); 36 | } 37 | 38 | /** 39 | * Executes the necessary cleanup tasks when the module is destroyed. 40 | * 41 | * No parameters are required. 42 | * 43 | * Does not return any value. 44 | */ 45 | onModuleDestroy() { 46 | try { 47 | this.stop().then(() => { 48 | this.disconnect(); 49 | }); 50 | } catch (error) { 51 | this.logger.error(error); 52 | } 53 | } 54 | 55 | /** 56 | * Called when the module is initialized. 57 | * 58 | * @return {Promise} Returns a promise that resolves when the function finishes executing. 59 | */ 60 | async onModuleInit(): Promise { 61 | await this.connect(); 62 | } 63 | 64 | /** 65 | * Sends a command to the worker and returns a promise that resolves with the response. 66 | * 67 | * @param {string} command - The command to be sent to the worker. 68 | * @param {any} data - Optional data to be sent along with the command. 69 | * @return {Promise} A promise that resolves with the response from the worker. 70 | */ 71 | async sendCommand( 72 | command: string, 73 | data: any = null, 74 | ): Promise> { 75 | return new Promise((resolve, reject) => { 76 | if (!this._worker || this._worker.killed) { 77 | reject(new Error('Worker is not running')); 78 | return; 79 | } 80 | 81 | const res = this._worker.send({ 82 | command, 83 | data, 84 | }); 85 | 86 | if (res) { 87 | /** 88 | * A callback function that handles the response from the worker. 89 | * 90 | * @param {any} response - The response object from the worker. 91 | */ 92 | const callback = (response: any) => { 93 | if (response.command === command) { 94 | this._worker?.removeListener('message', callback); 95 | resolve(response); 96 | } 97 | }; 98 | 99 | this._worker.on('message', callback); 100 | 101 | setTimeout(() => { 102 | reject(new Error('Failed to send command, timeout')); 103 | }, 10_000); 104 | 105 | return; 106 | } 107 | 108 | reject(new Error('Failed to send command')); 109 | }); 110 | } 111 | 112 | /** 113 | * Starts the function asynchronously. 114 | * 115 | * @return {Promise} A Promise that resolves once the function has started. 116 | */ 117 | async start(): Promise { 118 | return new Promise((resolve, reject) => { 119 | if (!this._worker || this._worker.killed) { 120 | reject(new Error('Worker is not running')); 121 | return; 122 | } 123 | 124 | const res = this._worker?.send({ 125 | command: 'START', 126 | }); 127 | 128 | if (res) { 129 | const callback = (response: any) => { 130 | if ( 131 | !( 132 | response.command === 'CONNECTION_UPDATED' && 133 | response.data.status !== 'close' 134 | ) 135 | ) { 136 | return; 137 | } 138 | this._worker?.removeListener('message', callback); 139 | resolve(); 140 | return; 141 | }; 142 | 143 | this._worker.on('message', callback); 144 | setTimeout(() => { 145 | this._worker?.removeListener('message', callback); 146 | reject(new Error('Failed to start worker, timeout')); 147 | }, 10_000); 148 | return; 149 | } 150 | 151 | reject(new Error('Failed to start worker')); 152 | }); 153 | } 154 | 155 | /** 156 | * Stops the execution of the function and returns a promise that resolves when the function is stopped or rejects with an error message. 157 | * 158 | * @return {Promise} A promise that resolves when the function is stopped or rejects with an error message. 159 | */ 160 | async stop(): Promise { 161 | return new Promise((resolve, reject) => { 162 | if (!this._worker || this._worker.killed) { 163 | resolve(); 164 | return; 165 | } 166 | 167 | const res = this._worker?.send({ 168 | command: 'STOP', 169 | }); 170 | 171 | if (res) { 172 | const callback = (response: any) => { 173 | if ( 174 | !( 175 | response.command === 'CONNECTION_UPDATED' && 176 | response.data.status == 'close' 177 | ) 178 | ) { 179 | return; 180 | } 181 | this._worker?.removeListener('message', callback); 182 | resolve(); 183 | return; 184 | }; 185 | 186 | this._worker.on('message', callback); 187 | setTimeout(() => { 188 | this._worker?.removeListener('message', callback); 189 | reject(new Error('Failed to stop whatsapp server, timeout')); 190 | }, 10_000); 191 | return; 192 | } 193 | 194 | reject(new Error('Failed to stop to whatsapp server')); 195 | }); 196 | } 197 | 198 | /** 199 | * Asynchronously restarts the function. 200 | * 201 | * @return {Promise} Returns a Promise that resolves to true if the function is successfully restarted, or false if an error occurs. 202 | */ 203 | async restart() { 204 | try { 205 | await this.start(); 206 | await this.stop(); 207 | return true; 208 | } catch (error) { 209 | this.logger.error(error); 210 | return false; 211 | } 212 | } 213 | 214 | /** 215 | * Connects to the worker process. 216 | * 217 | * @return {Promise} Returns a promise that resolves to true if the connection is successful, or false if there is already an existing connection. 218 | */ 219 | async connect(): Promise { 220 | if (this._worker) { 221 | return false; 222 | } 223 | 224 | this._worker = fork(this.workerPath, { 225 | detached: true, 226 | stdio: ['ignore', 'pipe', 'pipe', 'ipc'], 227 | }); 228 | 229 | this._worker?.unref(); 230 | this._worker.stdout?.on('data', (res) => { 231 | console.log(res.toString()); 232 | }); 233 | 234 | this._worker.stderr?.on('data', (res) => { 235 | console.log(res.toString()); 236 | }); 237 | 238 | this._worker.once('spawn', () => { 239 | this._workerStartedAt = new Date(); 240 | this.event.emit('process.state', 'connected'); 241 | }); 242 | 243 | this._worker.on('error', (err) => { 244 | this.logger.fatal( 245 | `Failed to spawn whatsapp process: ${err.message}`, 246 | err.stack, 247 | ); 248 | }); 249 | 250 | this._worker.on('exit', () => { 251 | this._worker?.removeAllListeners(); 252 | this._worker?.stderr?.removeAllListeners(); 253 | this._worker?.stdin?.removeAllListeners(); 254 | this._worker?.stdout?.removeAllListeners(); 255 | this._worker?.kill('SIGKILL'); 256 | this._workerStartedAt = null; 257 | this._connectedAt = null; 258 | this._connectionState = 'close'; 259 | this._qr = null; 260 | this._worker = null; 261 | this.event.emit('process.state', 'disconnected'); 262 | }); 263 | 264 | this._worker.on('message', async (res: any) => { 265 | switch (res.command) { 266 | case 'CONNECTION_UPDATED': 267 | this._connectionState = res.data.status; 268 | this._connectedAt = 269 | this._connectionState === 'open' ? new Date() : null; 270 | this._hasSession = (await prisma.session.count()) > 0; 271 | if (this._connectionState !== 'connecting') { 272 | this._qr = null; 273 | } 274 | this.event.emit('connection.update', this._connectionState); 275 | break; 276 | case 'QR_UPDATED': 277 | this._qr = res.data; 278 | this.event.emit('qr.update', res.data); 279 | break; 280 | } 281 | }); 282 | 283 | return true; 284 | } 285 | 286 | /** 287 | * Disconnects the worker process. 288 | * 289 | * @return {Promise} A promise that resolves to true if the worker was successfully disconnected, or rejects with an error message if there was an issue disconnecting the worker. 290 | */ 291 | async disconnect(): Promise { 292 | return new Promise((resolve, reject) => { 293 | if (!this._worker || this._worker?.killed) { 294 | resolve(true); 295 | return; 296 | } 297 | 298 | const res = this._worker.kill('SIGTERM'); 299 | if (res) { 300 | const callback = () => { 301 | this._worker?.stdin?.removeAllListeners(); 302 | this._worker?.stdout?.removeAllListeners(); 303 | this._worker?.stderr?.removeAllListeners(); 304 | this._worker?.removeAllListeners(); 305 | this._workerStartedAt = null; 306 | this._connectedAt = null; 307 | this._connectionState = 'close'; 308 | this._qr = null; 309 | this.event.emit('process.state', 'disconnected'); 310 | resolve(true); 311 | }; 312 | this._worker.once('exit', callback); 313 | setTimeout(() => { 314 | reject(new Error('Failed to kill worker, timeout')); 315 | }, 10_000); 316 | return; 317 | } 318 | 319 | reject(new Error('Failed to kill worker')); 320 | }); 321 | } 322 | 323 | /** 324 | * Reconnects to the worker process. 325 | * 326 | * @return {Promise} A promise that resolves to true if the reconnection is successful, otherwise false. 327 | */ 328 | async reconnect(): Promise { 329 | try { 330 | await this.disconnect(); 331 | await this.connect(); 332 | return true; 333 | } catch (error) { 334 | this.logger.error(error); 335 | return false; 336 | } 337 | } 338 | 339 | /** 340 | * Returns the worker. 341 | * 342 | * @return {ChildProcess | null} the worker 343 | */ 344 | get worker(): ChildProcess | null { 345 | return this._worker; 346 | } 347 | 348 | /** 349 | * Retrieves the current connection state. 350 | * 351 | * @return {"open" | "connecting" | "close"} The current connection state. 352 | */ 353 | get connectionState(): 'open' | 'connecting' | 'close' { 354 | return this._connectionState ?? 'close'; 355 | } 356 | 357 | /** 358 | * Retrieves the current worker state. 359 | * 360 | * @return {"connected" | "disconnected"} The current worker state. 361 | */ 362 | get workerState(): 'connected' | 'disconnected' { 363 | if (!this._worker) { 364 | return 'disconnected'; 365 | } 366 | return this._worker?.connected ? 'connected' : 'disconnected'; 367 | } 368 | 369 | /** 370 | * Get the date when the object was connected. 371 | * 372 | * @return {Date | null} The date when the object was connected. 373 | */ 374 | get connectedAt(): Date | null { 375 | return this._connectedAt; 376 | } 377 | 378 | /** 379 | * Get the value of the workerStartedAt property. 380 | * 381 | * @return {Date | null} The value of the workerStartedAt property. 382 | */ 383 | get workerStartedAt(): Date | null { 384 | return this._workerStartedAt; 385 | } 386 | 387 | /** 388 | * Returns a boolean value indicating whether a session exists. 389 | * 390 | * @return {boolean} The value indicating whether a session exists. 391 | */ 392 | get hasSession(): boolean { 393 | return this._hasSession; 394 | } 395 | 396 | /** 397 | * Get the value of qr. 398 | * 399 | * @return {boolean} The value of qr. 400 | */ 401 | get qr(): string | null { 402 | return this._qr; 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /apps/backend/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/backend/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/backend/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": true, 16 | "noImplicitAny": true, 17 | "strictBindCallApply": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "plugins": [ 21 | { 22 | "transform": "@nestia/core/lib/transform", 23 | /** 24 | * Validate request body. 25 | * 26 | * - "assert": Use typia.assert() function 27 | * - "is": Use typia.is() function 28 | * - "validate": Use typia.validate() function 29 | * - "assertEquals": Use typia.assertEquals() function 30 | * - "equals": Use typia.equals() function 31 | * - "validateEquals": Use typia.validateEquals() function 32 | */ 33 | "validate": "assert", 34 | /** 35 | * Validate JSON typed response body. 36 | * 37 | * - "assert": Use typia.assertStringify() function 38 | * - "is": Use typia.isStringify() function 39 | * - "validate": Use typia.validateStringify() function 40 | * - "stringify": Use typia.stringify() function, but dangerous 41 | * - null: Just use JSON.stringify() function, without boosting 42 | */ 43 | "stringify": "assert" 44 | }, 45 | { 46 | "transform": "typia/lib/transform" 47 | } 48 | ] 49 | } 50 | } -------------------------------------------------------------------------------- /apps/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | !.env 27 | .env.local 28 | -------------------------------------------------------------------------------- /apps/frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "@rollup/plugin-alias": "^5.0.1", 13 | "@tabler/core": "1.0.0-beta20", 14 | "@tabler/icons-vue": "^2.40.0", 15 | "@types/bootstrap": "^5.2.8", 16 | "@vitejs/plugin-vue": "^4.2.3", 17 | "animate.css": "^4.1.1", 18 | "bootstrap": "^5.3.2", 19 | "date-fns": "^2.30.0", 20 | "destr": "^2.0.2", 21 | "ofetch": "^1.3.3", 22 | "pinia": "^2.1.7", 23 | "pretty-bytes": "^6.1.1", 24 | "qrcode.vue": "^3.4.1", 25 | "typescript": "^5.0.2", 26 | "vite": "^4.4.5", 27 | "vue": "^3.3.7", 28 | "vue-router": "^4.2.5", 29 | "vue-tsc": "^1.8.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/frontend/public/000m.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermaysha/whatsapp-gateway/43983d8c1f05602650f17c834d092847fdf00335/apps/frontend/public/000m.jpg -------------------------------------------------------------------------------- /apps/frontend/public/login-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermaysha/whatsapp-gateway/43983d8c1f05602650f17c834d092847fdf00335/apps/frontend/public/login-bg.jpg -------------------------------------------------------------------------------- /apps/frontend/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 73 | -------------------------------------------------------------------------------- /apps/frontend/src/layouts/Main.vue: -------------------------------------------------------------------------------- 1 | 215 | 216 | 219 | -------------------------------------------------------------------------------- /apps/frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import '@tabler/core/dist/css/tabler.min.css' 4 | import 'animate.css' 5 | import 'bootstrap' 6 | import './style.css' 7 | import App from './App.vue' 8 | 9 | import { router } from './router' 10 | 11 | const pinia = createPinia() 12 | const app = createApp(App) 13 | app.use(pinia) 14 | app.use(router) 15 | app.mount('#app') 16 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Home/Home.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 67 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Home/Widgets/MemoryUsage.vue: -------------------------------------------------------------------------------- 1 | 78 | 123 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Home/Widgets/RecentMessage.vue: -------------------------------------------------------------------------------- 1 | 309 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Home/Widgets/StorageUsage.vue: -------------------------------------------------------------------------------- 1 | 41 | 75 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Home/Widgets/UptimeConnection.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 53 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Home/Widgets/UptimeProcess.vue: -------------------------------------------------------------------------------- 1 | 34 | 51 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Home/Widgets/WAConnection.vue: -------------------------------------------------------------------------------- 1 | 138 | 278 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Home/Widgets/WAProcess.vue: -------------------------------------------------------------------------------- 1 | 85 | 171 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Login.vue: -------------------------------------------------------------------------------- 1 | 97 | 98 | 108 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/NotFound.vue: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /apps/frontend/src/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | 3 | const routes = [ 4 | { 5 | path: "/login", 6 | name: "login", 7 | component: () => import("./pages/Login.vue"), 8 | }, 9 | { 10 | path: "/", 11 | component: () => import("./layouts/Main.vue"), 12 | children: [ 13 | { 14 | path: "/", 15 | name: "Home", 16 | component: () => import("./pages/Home/Home.vue"), 17 | }, 18 | ], 19 | }, 20 | { 21 | path: "/:pathMatch(.*)*", 22 | name: "NotFound", 23 | component: () => import("./pages/NotFound.vue"), 24 | }, 25 | ]; 26 | 27 | export const router = createRouter({ 28 | history: createWebHashHistory(), 29 | routes, 30 | }); 31 | -------------------------------------------------------------------------------- /apps/frontend/src/stores/useProcess.ts: -------------------------------------------------------------------------------- 1 | import { defineStore, acceptHMRUpdate } from 'pinia' 2 | import { ref, computed } from 'vue' 3 | import { useToasts } from './useToasts'; 4 | import { destr } from 'destr' 5 | import { ofetch } from 'ofetch' 6 | 7 | export const useProcess = defineStore('process', () => { 8 | const toast = useToasts(); 9 | 10 | const connectionState = ref<"close" | "open" | "connecting">('close') 11 | const connectedAt = ref(null) 12 | const workerState = ref<"connected" | "disconnected">('disconnected') 13 | const workerStartedAt = ref(null) 14 | const isLoading = ref(true); 15 | const hasSession = ref(false); 16 | const qr = ref(null); 17 | const isProcessConnected = computed(() => { 18 | return workerState.value === 'connected' 19 | }) 20 | const isConnected = computed(() => { 21 | return connectionState.value === 'open' 22 | }) 23 | 24 | const reset = () => { 25 | connectedAt.value = null; 26 | connectionState.value = "close"; 27 | workerState.value = "disconnected"; 28 | workerStartedAt.value = null; 29 | hasSession.value = false; 30 | } 31 | 32 | function getData() { 33 | isLoading.value = true; 34 | const result = ofetch('/whatsapp', { 35 | baseURL: import.meta.env.VITE_API_URL, 36 | credentials: "include", 37 | }); 38 | 39 | result.catch((err) => { 40 | console.error(err) 41 | toast.dispatch({ 42 | title: "Error", 43 | message: err.message ?? "Failed to fetch whatsapp status", 44 | type: "danger", 45 | }); 46 | isLoading.value = false 47 | reset(); 48 | }); 49 | 50 | result.then(async (res) => { 51 | connectedAt.value = res.connectedAt ? new Date(res.connectedAt) : null; 52 | connectionState.value = res.connectionState; 53 | workerState.value = res.workerState; 54 | workerStartedAt.value = res.workerStartedAt ? new Date(res.workerStartedAt) : null; 55 | hasSession.value = res.hasSession; 56 | qr.value = res.qr; 57 | isLoading.value = false; 58 | }) 59 | } 60 | 61 | function listen() { 62 | const event = new EventSource(new URL("event/connection-state", import.meta.env.VITE_API_URL), { 63 | withCredentials: true, 64 | }); 65 | event.addEventListener('error', (e) => { 66 | toast.dispatch({ 67 | title: "Event Listener Error", 68 | message: "Connection error to State Event Listener", 69 | type: "danger", 70 | }) 71 | console.error(e) 72 | reset(); 73 | }) 74 | event.addEventListener('message', (ev) => { 75 | const res = destr(ev.data) 76 | connectedAt.value = res.connectedAt ? new Date(res.connectedAt) : null; 77 | connectionState.value = res.connectionState; 78 | workerState.value = res.workerState; 79 | workerStartedAt.value = res.workerStartedAt ? new Date(res.workerStartedAt) : null; 80 | hasSession.value = res.hasSession; 81 | isLoading.value = false; 82 | }) 83 | } 84 | 85 | return { 86 | connectionState, 87 | connectedAt, 88 | workerState, 89 | workerStartedAt, 90 | hasSession, 91 | qr, 92 | isLoading, 93 | isProcessConnected, 94 | isConnected, 95 | getData, 96 | listen 97 | } 98 | }) 99 | 100 | 101 | if (import.meta.hot) { 102 | import.meta.hot.accept(acceptHMRUpdate(useProcess, import.meta.hot)) 103 | } 104 | -------------------------------------------------------------------------------- /apps/frontend/src/stores/useToasts.ts: -------------------------------------------------------------------------------- 1 | import { defineStore, acceptHMRUpdate } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | interface IToast { 5 | type: 'info' | 'danger' | 'success' | 'warning' | 'primary', 6 | title: string, 7 | message: string 8 | } 9 | 10 | export const useToasts = defineStore('toasts', () => { 11 | const data = ref([]) 12 | 13 | function dispatch(toastData: IToast) { 14 | data.value.push(toastData); 15 | } 16 | 17 | function reset() { 18 | data.value = []; 19 | } 20 | 21 | return { 22 | data, dispatch, reset 23 | } 24 | }) 25 | 26 | 27 | if (import.meta.hot) { 28 | import.meta.hot.accept(acceptHMRUpdate(useToasts, import.meta.hot)) 29 | } 30 | -------------------------------------------------------------------------------- /apps/frontend/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --tblr-font-sans-serif: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /apps/frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_API_URL: string 5 | // more env variables... 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv 10 | } 11 | -------------------------------------------------------------------------------- /apps/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /apps/frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import alias from "@rollup/plugin-alias"; 4 | import { resolve } from "path"; 5 | 6 | const projectRootDir = resolve(__dirname); 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [ 11 | vue(), 12 | alias({ 13 | entries: [ 14 | { 15 | find: "@", 16 | replacement: resolve(projectRootDir, "src"), 17 | }, 18 | ], 19 | }), 20 | ], 21 | }); 22 | -------------------------------------------------------------------------------- /libraries/database/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "database", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "files": [ 8 | "dist/**" 9 | ], 10 | "scripts": { 11 | "build": "tsc", 12 | "db:seed": "prisma db seed", 13 | "db:push": "prisma db push", 14 | "db:pull": "prisma db pull", 15 | "format": "prisma format", 16 | "migrate:dev": "prisma migrate dev", 17 | "migrate:status": "prisma migrate status", 18 | "migrate:reset": "prisma migrate reset", 19 | "migrate:deploy": "prisma migrate deploy", 20 | "generate": "prisma generate" 21 | }, 22 | "keywords": [], 23 | "author": "", 24 | "license": "ISC", 25 | "devDependencies": { 26 | "@types/node": "^20.8.7", 27 | "prisma": "^5.4.2", 28 | "tsx": "^3.14.0", 29 | "typescript": "^5.2.2" 30 | }, 31 | "dependencies": { 32 | "@prisma/client": "^5.4.2", 33 | "argon2": "^0.31.2" 34 | }, 35 | "prisma": { 36 | "seed": "tsx ./src/seed.ts" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /libraries/database/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model Session { 11 | id String @id @default(uuid()) @db.Uuid 12 | name String 13 | data Json @db.JsonB 14 | userId String @db.Uuid 15 | 16 | updatedAt DateTime @updatedAt @db.Timestamptz() 17 | createdAt DateTime @default(now()) @db.Timestamptz() 18 | 19 | user User @relation(fields: [userId], references: [id]) 20 | 21 | @@unique([name]) 22 | } 23 | 24 | model User { 25 | id String @id @default(uuid()) @db.Uuid 26 | username String @unique 27 | password String 28 | fullname String 29 | 30 | createdAt DateTime @db.Timestamptz() 31 | updatedAt DateTime @updatedAt() @db.Timestamptz() 32 | 33 | session Session[] 34 | } 35 | 36 | model UserSession { 37 | id String @id @default(uuid()) @db.Uuid 38 | sid String @unique 39 | data Json @db.JsonB 40 | expiresAt DateTime @db.Timestamptz() 41 | } 42 | -------------------------------------------------------------------------------- /libraries/database/src/client.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { PrismaClient } from '@prisma/client' 3 | 4 | declare global { 5 | var prisma: PrismaClient | undefined 6 | } 7 | 8 | export const prisma: PrismaClient = global.prisma || new PrismaClient() 9 | 10 | if (process.env.NODE_ENV !== 'production') global.prisma = prisma 11 | -------------------------------------------------------------------------------- /libraries/database/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@prisma/client' 2 | export * from './client' 3 | 4 | prisma?.$connect() 5 | -------------------------------------------------------------------------------- /libraries/database/src/seed.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "./client"; 2 | import { hash } from 'argon2' 3 | 4 | ;(async () => { 5 | const now = new Date(); 6 | const pass = await hash('12345678') 7 | await prisma.user.upsert({ 8 | where: { 9 | username: 'admin', 10 | }, 11 | update: {}, 12 | create: { 13 | username: 'admin', 14 | password: pass.toString(), 15 | fullname: 'Administrator', 16 | createdAt: now, 17 | updatedAt: now, 18 | } 19 | }) 20 | })() 21 | -------------------------------------------------------------------------------- /libraries/database/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "CommonJS", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | "declarationMap": false, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /libraries/whatsapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatsapp", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/main.js", 6 | "files": [ 7 | "dist/**" 8 | ], 9 | "scripts": { 10 | "build": "tsc" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@hapi/boom": "^10.0.1", 17 | "@whiskeysockets/baileys": "^6.5.0", 18 | "database": "workspace:*", 19 | "link-preview-js": "^3.0.5", 20 | "node-cache": "^5.1.2", 21 | "pino": "^8.16.0", 22 | "sharp": "^0.32.6" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^20.8.7", 26 | "pino-pretty": "^10.2.3", 27 | "qrcode-terminal": "^0.12.0", 28 | "tsup": "^7.2.0", 29 | "typescript": "^5.2.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /libraries/whatsapp/src/commands/logout.ts: -------------------------------------------------------------------------------- 1 | import { type Whatapp } from "../whatsapp"; 2 | import { type Logger } from 'pino' 3 | 4 | export default (wa: Whatapp, _logger: Logger) => { 5 | return { 6 | name: 'LOGOUT', 7 | async handler(_data: any) { 8 | wa.logout(); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libraries/whatsapp/src/commands/memoryUsage.ts: -------------------------------------------------------------------------------- 1 | import { type Whatapp } from "../whatsapp"; 2 | import { type Logger } from 'pino' 3 | 4 | export default (wa: Whatapp, _logger: Logger) => { 5 | return { 6 | name: 'MEMORY_USAGE', 7 | async handler(_data: any) { 8 | const rss = process.memoryUsage.rss(); 9 | 10 | process.send?.({ 11 | command: 'MEMORY_USAGE', 12 | status: true, 13 | data: rss, 14 | }) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libraries/whatsapp/src/commands/restart.ts: -------------------------------------------------------------------------------- 1 | import { type Whatapp } from "../whatsapp"; 2 | import { type Logger } from 'pino' 3 | 4 | export default (wa: Whatapp, _logger: Logger) => { 5 | return { 6 | name: 'RESTART', 7 | async handler(_data: any) { 8 | wa.stop().then(() => { 9 | wa.start(); 10 | }); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /libraries/whatsapp/src/commands/sendMessage.ts: -------------------------------------------------------------------------------- 1 | import { type Whatapp } from "../whatsapp"; 2 | import { type Logger } from 'pino' 3 | 4 | interface ISendMessage { 5 | to: string 6 | data: any 7 | } 8 | 9 | export default (wa: Whatapp, logger: Logger) => { 10 | return { 11 | name: 'SEND_MESSAGE', 12 | async handler(data: ISendMessage) { 13 | try { 14 | if (!wa.socket) { 15 | process.send?.({ 16 | command: 'SEND_MESSAGE', 17 | status: false, 18 | data: 'Failed to send message, WhatsApp is not running', 19 | }) 20 | return; 21 | } 22 | 23 | const socket = wa.socket; 24 | 25 | await socket.waitForSocketOpen(); 26 | const [result] = await socket.onWhatsApp(data.to) 27 | 28 | if (!result?.exists) { 29 | process.send?.({ 30 | command: 'SEND_MESSAGE', 31 | status: false, 32 | data: 'Failed to send message, phone number not registered on WhatsApp', 33 | }) 34 | return 35 | } 36 | await socket.sendPresenceUpdate('available'); 37 | await socket.sendPresenceUpdate('composing'); 38 | 39 | await socket.sendMessage(result.jid, data.data) 40 | 41 | await socket.sendPresenceUpdate('available'); 42 | process.send?.({ 43 | command: 'SEND_MESSAGE', 44 | status: true, 45 | data: 'Message sent successfully', 46 | }) 47 | } catch (err) { 48 | const error = err as any; 49 | process.send?.({ 50 | command: 'SEND_MESSAGE', 51 | status: false, 52 | data: error.message ?? error ?? 'Failed to send message', 53 | }) 54 | } 55 | return 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /libraries/whatsapp/src/commands/start.ts: -------------------------------------------------------------------------------- 1 | import { type Whatapp } from "../whatsapp"; 2 | import { type Logger } from 'pino' 3 | 4 | export default (wa: Whatapp, _logger: Logger) => { 5 | return { 6 | name: 'START', 7 | async handler(_data: any) { 8 | wa.start(); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libraries/whatsapp/src/commands/stop.ts: -------------------------------------------------------------------------------- 1 | import { type Whatapp } from "../whatsapp"; 2 | import { type Logger } from 'pino' 3 | 4 | export default (wa: Whatapp, _logger: Logger) => { 5 | return { 6 | name: 'STOP', 7 | async handler(_data: any) { 8 | wa.stop(); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libraries/whatsapp/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Whatapp } from "./whatsapp"; 2 | import { readdirSync, statSync } from 'fs' 3 | import { join } from 'path' 4 | import { logger as parentLogger } from "./whatsapp/whatsapp.logger"; 5 | 6 | /** 7 | * Recursively retrieves all files in a given directory. 8 | * 9 | * @param {string} dir - The directory to search for files. 10 | * @param {string[]} files_ - An optional array to store the file names. 11 | * @return {string[]} - An array containing the names of all files in the directory. 12 | */ 13 | function getFiles(dir: string, files_: string[] = []): string[] { 14 | dir = join(__dirname, dir) 15 | const files = readdirSync(dir); 16 | for (const i in files){ 17 | const name = join(dir, files[i]); 18 | if (statSync(name).isDirectory()){ 19 | getFiles(name, files_); 20 | } else { 21 | files_.push(name); 22 | } 23 | } 24 | return files_; 25 | } 26 | 27 | /** 28 | * Initializes the main worker process. 29 | * 30 | * @return {Promise} The function does not return anything. 31 | */ 32 | const main = async (): Promise => { 33 | const wa = new Whatapp(); 34 | const logger = parentLogger.child({ module: 'Main Worker' }); 35 | 36 | if (!process.send) { 37 | console.log("Node is not running in a worker process"); 38 | process.exit(0); 39 | } 40 | 41 | const files = getFiles('./commands') 42 | const commands: Map = new Map(); 43 | for (const file of files) { 44 | try { 45 | const command = (await import(file)).default(wa, logger); 46 | 47 | commands.set(command.name, command.handler); 48 | } catch (error) { 49 | console.warn('Could not load command', file, error); 50 | } 51 | } 52 | 53 | process.on('message', async (msg: any) => { 54 | if (commands.has(msg.command)) { 55 | await commands.get(msg.command)(msg.data ?? undefined); 56 | } 57 | }); 58 | 59 | process.on('disconnect', async () => { 60 | await wa.stop(); 61 | process.exit(0); 62 | }) 63 | } 64 | 65 | main(); 66 | -------------------------------------------------------------------------------- /libraries/whatsapp/src/whatsapp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './whatsapp' 2 | -------------------------------------------------------------------------------- /libraries/whatsapp/src/whatsapp/whatsapp.helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Encodes the given object into a format suitable for transmission or storage. 3 | * 4 | * @param {any} obj - The object to encode. 5 | * @return {any} The encoded object. 6 | */ 7 | export function encodeBuffer(obj: any): any { 8 | if (typeof obj !== 'object' || !obj) { 9 | return obj 10 | } 11 | 12 | if (Buffer.isBuffer(obj) || obj instanceof Uint8Array) { 13 | return { 14 | type: 'Buffer', 15 | data: Buffer.from(obj).toString('base64'), 16 | } 17 | } 18 | 19 | if (Array.isArray(obj)) { 20 | return obj.map(encodeBuffer) 21 | } 22 | 23 | const newObj: any = {} 24 | for (const key in obj) { 25 | if (Object.hasOwnProperty.call(obj, key)) { 26 | newObj[key] = encodeBuffer(obj[key]) 27 | } 28 | } 29 | return newObj 30 | } 31 | 32 | /** 33 | * Decodes a buffer object or an array of buffer objects. 34 | * 35 | * @param {any} obj - The object to be decoded. 36 | * @return {any} The decoded object. 37 | */ 38 | export function decodeBuffer(obj: any): any { 39 | if ( 40 | typeof obj !== 'object' || 41 | !obj || 42 | Buffer.isBuffer(obj) || 43 | obj instanceof Uint8Array 44 | ) { 45 | return obj 46 | } 47 | 48 | if (obj.type === 'Buffer') { 49 | return Buffer.from(obj.data, 'base64') 50 | } 51 | 52 | if (Array.isArray(obj)) { 53 | return obj.map(decodeBuffer) 54 | } 55 | 56 | const newObj: any = {} 57 | for (const key in obj) { 58 | if (Object.hasOwnProperty.call(obj, key)) { 59 | newObj[key] = decodeBuffer(obj[key]) 60 | } 61 | } 62 | return newObj 63 | } 64 | -------------------------------------------------------------------------------- /libraries/whatsapp/src/whatsapp/whatsapp.interface.ts: -------------------------------------------------------------------------------- 1 | import type { AuthenticationState } from '@whiskeysockets/baileys' 2 | 3 | /** 4 | * Represents the state of authentication for MongoDB. 5 | */ 6 | export interface IDBAuthState { 7 | /** 8 | * The current authentication state. 9 | */ 10 | state: AuthenticationState 11 | 12 | /** 13 | * Saves the credentials and returns a promise that resolves when the credentials are saved successfully. 14 | */ 15 | saveCreds: () => Promise 16 | 17 | /** 18 | * Clears the credentials and returns a promise that resolves when the credentials are cleared successfully. 19 | */ 20 | clearCreds: () => Promise 21 | } 22 | -------------------------------------------------------------------------------- /libraries/whatsapp/src/whatsapp/whatsapp.logger.ts: -------------------------------------------------------------------------------- 1 | import pino from "pino"; 2 | 3 | export const logger = pino({ 4 | enabled: true, 5 | name: 'Whatsapp Worker', 6 | timestamp: () => `,"time":"${new Date().toJSON()}"`, 7 | }); 8 | -------------------------------------------------------------------------------- /libraries/whatsapp/src/whatsapp/whatsapp.storage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type AuthenticationCreds, 3 | type SignalDataTypeMap, 4 | initAuthCreds, 5 | proto, 6 | } from "@whiskeysockets/baileys"; 7 | import { Prisma, prisma } from "database"; 8 | import type { IDBAuthState } from "./whatsapp.interface"; 9 | import { encodeBuffer, decodeBuffer } from "./whatsapp.helper"; 10 | import { logger as parentLogger } from "./whatsapp.logger"; 11 | 12 | /** 13 | * Retrieves the authentication state from the database for a given device ID. 14 | * 15 | * @return {Promise} - A promise that resolves with the authentication state. 16 | */ 17 | export async function useDBAuthState(): Promise { 18 | const logger = parentLogger.child({ 19 | name: "Whatsapp Storage", 20 | }); 21 | const maxRetries = 10; 22 | 23 | /** 24 | * Logs the given error. 25 | * 26 | * @param {any} error - The error to be logged. 27 | * @return {void} This function does not return anything. 28 | */ 29 | const logError = (error: any): void => { 30 | if (error instanceof Prisma.PrismaClientKnownRequestError) { 31 | logger.error( 32 | { 33 | error: { 34 | code: error.code, 35 | message: error.message, 36 | meta: error.meta, 37 | }, 38 | }, 39 | error.message 40 | ); 41 | } else if (error instanceof Prisma.PrismaClientInitializationError) { 42 | logger.error({ 43 | code: error.errorCode, 44 | message: error.message, 45 | }); 46 | } else { 47 | const err = error as Error; 48 | logger.error({ 49 | message: err.message, 50 | }); 51 | } 52 | }; 53 | 54 | /** 55 | * Writes data to the database. 56 | * 57 | * @param {any} value - The data to be written. 58 | * @param {string} name - The credentials name for database access. 59 | */ 60 | const writeData = async (value: any, name: string) => { 61 | const data = encodeBuffer(value); 62 | return prisma.session.upsert({ 63 | where: { 64 | name, 65 | }, 66 | create: { 67 | name, 68 | data: data as Prisma.InputJsonValue, 69 | }, 70 | update: { 71 | data, 72 | }, 73 | }); 74 | }; 75 | 76 | /** 77 | * Retrieves data from the database based on the given credentials. 78 | * 79 | * @param {string} name - The credentials used to authenticate the user. 80 | * @return {Promise} - The parsed data retrieved from the database, or null if no data is found. 81 | */ 82 | const readData = async (name: string): Promise => { 83 | const result = await prisma.session.findUnique({ 84 | select: { 85 | data: true, 86 | }, 87 | where: { 88 | name, 89 | }, 90 | }); 91 | 92 | if (result?.data) { 93 | return decodeBuffer(result.data); 94 | } 95 | return null; 96 | }; 97 | 98 | /** 99 | * Removes data based on the given credentials. 100 | * 101 | * @param {string} name - The credentials to use for removal. 102 | */ 103 | const removeData = async (name: string) => { 104 | return prisma.session.delete({ 105 | where: { 106 | name, 107 | }, 108 | }); 109 | }; 110 | 111 | /** 112 | * Clears the data by deleting multiple sessions that match the given device ID. 113 | * 114 | * @return {Promise} A promise that resolves when the data is cleared. 115 | */ 116 | const clearData = async (): Promise => { 117 | let retries = 1; 118 | 119 | while (retries <= maxRetries) { 120 | try { 121 | await prisma.session.deleteMany({ 122 | where: {}, 123 | }); 124 | break; 125 | } catch (error) { 126 | logError(error); 127 | retries++; 128 | } 129 | } 130 | }; 131 | 132 | const creds: AuthenticationCreds = 133 | (await readData("creds")) || initAuthCreds(); 134 | 135 | return { 136 | state: { 137 | creds, 138 | keys: { 139 | /** 140 | * Retrieves data of a specified type for a given set of IDs. 141 | * 142 | * @param {T} type - The type of data to retrieve. 143 | * @param {string[]} ids - An array of IDs for which to retrieve data. 144 | * @return {Promise<{ [id: string]: SignalDataTypeMap[T] }>} - A promise that resolves with an object containing the retrieved data, where each ID is mapped to its corresponding data. 145 | */ 146 | get: async ( 147 | type: T, 148 | ids: string[] 149 | ): Promise<{ [id: string]: SignalDataTypeMap[T] }> => { 150 | const data: { [id: string]: SignalDataTypeMap[T] } = {}; 151 | for (const id of ids) { 152 | let value = await readData(`${type}-${id}`); 153 | if (type === "app-state-sync-key" && value) { 154 | value = proto.Message.AppStateSyncKeyData.fromObject(value); 155 | } 156 | data[id] = value; 157 | } 158 | return data; 159 | }, 160 | 161 | /** 162 | * Sets the data in the given object. 163 | * 164 | * @param {Record>} data - The data to be set. 165 | * @return {Promise} A promise that resolves when the data is set. 166 | */ 167 | set: async ( 168 | data: Record> 169 | ): Promise => { 170 | for (const [category, entries] of Object.entries(data)) { 171 | for (const [id, value] of Object.entries(entries)) { 172 | const key = `${category}-${id}`; 173 | if (value) { 174 | await writeData(value, key); 175 | } else { 176 | await removeData(key); 177 | } 178 | } 179 | } 180 | }, 181 | 182 | /** 183 | * Clears the data asynchronously. 184 | * 185 | * @return {Promise} A promise that resolves when the data is cleared. 186 | */ 187 | clear: (): Promise => { 188 | return clearData(); 189 | }, 190 | }, 191 | }, 192 | 193 | /** 194 | * Saves the credentials. 195 | * 196 | * @return {Promise} - A promise that resolves when the credentials are saved. 197 | */ 198 | saveCreds: async (): Promise => { 199 | let retries = 1; 200 | while (retries <= maxRetries) { 201 | try { 202 | await writeData(creds, "creds"); 203 | break; 204 | } catch (error) { 205 | logError(error); 206 | retries++; 207 | } 208 | } 209 | }, 210 | 211 | /** 212 | * Clears the credentials. 213 | * 214 | * @return {Promise} - No return value. 215 | */ 216 | clearCreds: (): Promise => { 217 | return clearData(); 218 | }, 219 | }; 220 | } 221 | -------------------------------------------------------------------------------- /libraries/whatsapp/src/whatsapp/whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type WASocket, 3 | type WAMessageKey, 4 | type WAMessageContent, 5 | makeWASocket, 6 | DisconnectReason, 7 | fetchLatestWaWebVersion, 8 | } from "@whiskeysockets/baileys"; 9 | import { logger } from "./whatsapp.logger"; 10 | import { prisma, Prisma } from "database"; 11 | import { useDBAuthState } from "./whatsapp.storage"; 12 | import NodeCache from "node-cache"; 13 | import { Boom } from "@hapi/boom"; 14 | import { EventEmitter } from "events"; 15 | 16 | export class Whatapp { 17 | /** 18 | * The WebSocket connection object. 19 | */ 20 | public socket: WASocket | null = null; 21 | 22 | /** 23 | * The local event emitter. 24 | */ 25 | public localEvent: EventEmitter = new EventEmitter() 26 | 27 | /** 28 | * Initializes a new instance of the class. 29 | * 30 | * @return {void} - There is no return value. 31 | */ 32 | constructor() { 33 | prisma 34 | ?.$connect() 35 | .then(() => { 36 | logger.info("Connected to the database"); 37 | }) 38 | .catch((error) => { 39 | if (error instanceof Prisma.PrismaClientInitializationError) { 40 | logger.error( 41 | { 42 | code: error.errorCode, 43 | message: error.message, 44 | }, 45 | error.message 46 | ); 47 | } else { 48 | logger.error( 49 | {}, 50 | error?.message ?? "Error connecting to the database" 51 | ); 52 | } 53 | }); 54 | } 55 | 56 | /** 57 | * Starts the WhatsApp connection. 58 | * 59 | * @return {Promise} Returns true if the connection is successfully started. 60 | */ 61 | async start(): Promise { 62 | if (this.socket) { 63 | logger.info('WhatsApp is already running'); 64 | return true; 65 | } 66 | 67 | const axiosConfig = { 68 | headers: { 69 | "User-Agent": 70 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.57", 71 | Accept: 72 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 73 | "Accept-Encoding": "gzip, deflate, br", 74 | "Accept-Language": "en-US,en;q=0.9,id;q=0.8", 75 | "Cache-Control": "max-age=0", 76 | }, 77 | }; 78 | const msgRetryCounterCache = new NodeCache(); 79 | 80 | const { state, saveCreds, clearCreds } = await useDBAuthState(); 81 | const { version } = await fetchLatestWaWebVersion(axiosConfig); 82 | 83 | logger.info({}, `Trying to connect to WhatsApp with version ${version}`); 84 | 85 | const socket = makeWASocket({ 86 | version, 87 | logger: logger as any, 88 | printQRInTerminal: process.env.NODE_ENV === "development", 89 | browser: ["Windows", "Desktop", "10.0.22621"], 90 | auth: state, 91 | msgRetryCounterCache, 92 | generateHighQualityLinkPreview: true, 93 | getMessage: this.getMessage, 94 | markOnlineOnConnect: true, 95 | options: axiosConfig, 96 | }); 97 | 98 | socket.ev.process(async (events) => { 99 | if (events["connection.update"]) { 100 | const update = events["connection.update"]; 101 | const { connection, lastDisconnect, qr } = update; 102 | 103 | // Update QR code 104 | if (qr) { 105 | process.send?.({ 106 | command: "QR_UPDATED", 107 | data: qr, 108 | }); 109 | } 110 | 111 | if (connection === "close") { 112 | // reconnect if not logged out 113 | const statusCode = (lastDisconnect?.error as Boom)?.output 114 | ?.statusCode; 115 | process.send?.({ 116 | command: "CONNECTION_UPDATED", 117 | data: { 118 | status: connection, 119 | statusCode, 120 | }, 121 | }); 122 | 123 | this.socket = null; 124 | this.localEvent.emit('close', statusCode); 125 | 126 | if (statusCode === 400) { 127 | logger.info("Connection manually closed"); 128 | } else if (statusCode === DisconnectReason.loggedOut) { 129 | logger.info("Connection closed. You are logged out."); 130 | await clearCreds(); 131 | } else { 132 | this.start(); 133 | } 134 | } 135 | 136 | if (connection === "open") { 137 | this.socket = socket; 138 | } 139 | 140 | if (connection === "connecting" || connection === "open") { 141 | process.send?.({ 142 | command: "CONNECTION_UPDATED", 143 | data: { 144 | status: connection, 145 | }, 146 | }); 147 | } 148 | } 149 | 150 | if (events["creds.update"]) { 151 | await saveCreds(); 152 | } 153 | }); 154 | 155 | return true; 156 | } 157 | 158 | /** 159 | * Logs out the user from the current session. 160 | * 161 | * @return {Promise} A Promise that resolves when the logout process is complete. 162 | */ 163 | async logout(): Promise { 164 | if (!this.socket) { 165 | logger.info('Whatsapp is not running, skipping logout'); 166 | return new Promise((resolve) => resolve()); 167 | } 168 | 169 | await this.socket.logout("Manually logouted"); 170 | 171 | return new Promise((resolve) => { 172 | const handleResponse = (status: number) => { 173 | this.localEvent.removeListener('close', handleResponse) 174 | resolve() 175 | } 176 | 177 | this.localEvent.on('close', handleResponse) 178 | 179 | setTimeout(handleResponse, 30_000) 180 | }) 181 | } 182 | 183 | /** 184 | * Stops the function execution and closes the connection. 185 | * 186 | * @return {Promise} - A promise that resolves when the connection is closed. 187 | */ 188 | async stop(): Promise { 189 | if (!this.socket) { 190 | logger.info('Whatsapp is not running, skipping stop'); 191 | return new Promise((resolve) => resolve()); 192 | } 193 | 194 | this.socket.end( 195 | new Boom("Connection closed manually", { 196 | statusCode: 400, 197 | }) 198 | ); 199 | 200 | return new Promise((resolve) => { 201 | const handleResponse = (status: number) => { 202 | this.localEvent.removeListener('close', handleResponse) 203 | resolve() 204 | } 205 | 206 | this.localEvent.on('close', handleResponse) 207 | 208 | setTimeout(handleResponse, 30_000) 209 | }) 210 | } 211 | 212 | /** 213 | * Retrieves a message based on a given key. 214 | * 215 | * @param {WAMessageKey} key - The key used to retrieve the message. 216 | * @return {Promise} - The retrieved message content, or undefined if the message does not exist. 217 | */ 218 | private async getMessage( 219 | key: WAMessageKey 220 | ): Promise { 221 | return undefined; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /libraries/whatsapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 13 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 14 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 15 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 16 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 17 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 18 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 19 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 20 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 21 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 22 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 23 | /* Modules */ 24 | "module": "CommonJS", /* Specify what module code is generated. */// "rootDir": "./", /* Specify the root folder within your source files. */ 25 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 26 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 27 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 28 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 29 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 30 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 31 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 32 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 33 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 34 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 35 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 36 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 37 | // "resolveJsonModule": true, /* Enable importing .json files. */ 38 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | /* JavaScript Support */ 41 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 42 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 43 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 50 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 51 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 52 | // "removeComments": true, /* Disable emitting comments. */ 53 | // "noEmit": true, /* Disable emitting files from a compilation. */ 54 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 55 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 56 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 57 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | /* Interop Constraints */ 69 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 70 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 73 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. *//* Type Checking */ 74 | "strict": true, /* Enable all strict type-checking options. */// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 75 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 76 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 77 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 78 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 79 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 80 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 81 | "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 82 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 83 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 84 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 85 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 86 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 87 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 88 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 89 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 90 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 91 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 92 | /* Completeness */ 93 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 94 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 95 | "strictNullChecks": true 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatsapp-gateway", 3 | "version": "0.0.0", 4 | "description": "Whatsapp Gateway", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "turbo build", 8 | "start": "turbo start" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/vermaysha/whatsapp-gateway.git" 13 | }, 14 | "keywords": [ 15 | "whatsapp", 16 | "whatsapp-api", 17 | "whatsapp-gateway" 18 | ], 19 | "author": { 20 | "name": "Ashary Vermaysha", 21 | "email": "vermaysha@gmail.com" 22 | }, 23 | "license": "SEE LICENSE IN LICENSE", 24 | "bugs": { 25 | "url": "https://github.com/vermaysha/whatsapp-gateway/issues" 26 | }, 27 | "homepage": "https://github.com/vermaysha/whatsapp-gateway#readme", 28 | "engines": { 29 | "node": ">=16.8.0" 30 | }, 31 | "packageManager": "^pnpm@8.9.0", 32 | "devDependencies": { 33 | "turbo": "^1.10.16" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - apps/* 3 | - libraries/* 4 | -------------------------------------------------------------------------------- /screenshots/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermaysha/whatsapp-gateway/43983d8c1f05602650f17c834d092847fdf00335/screenshots/home.jpg -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "outputs": [ 6 | "dist/**" 7 | ], 8 | "dependsOn": ["^build"] 9 | }, 10 | "start": { 11 | "cache": false, 12 | "dependsOn": ["^build"], 13 | "outputs": ["dist/**"], 14 | "persistent": true 15 | } 16 | } 17 | } 18 | --------------------------------------------------------------------------------