├── .env.example ├── nodemon.json ├── src ├── services │ └── whatsapp │ │ ├── type.ts │ │ └── index.ts ├── utils.ts └── index.ts ├── data └── devices.json ├── .gitignore ├── package.json ├── README.md └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | MONGODB_URI=//use your own -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts,json", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "npx ts-node ./src/index.ts" 6 | } -------------------------------------------------------------------------------- /src/services/whatsapp/type.ts: -------------------------------------------------------------------------------- 1 | export enum AttachmentTypes { 2 | 'photo', 3 | // 'video', 4 | // 'audio', 5 | // 'gif', 6 | 'document' 7 | } 8 | 9 | export interface Attachment { 10 | // path?: string; 11 | url: string; 12 | name: string; 13 | filesize: number; 14 | type: AttachmentTypes; 15 | } 16 | 17 | export enum ConnectionState { 18 | 'idle', 19 | 'disconnected', 20 | 'connected' 21 | } 22 | 23 | export interface PreparedPhotoFile { 24 | type: string; 25 | image: Buffer 26 | } 27 | 28 | export interface PreparedVideoFile { 29 | type: string; 30 | video: Buffer; 31 | jpegThumbnail: string 32 | } 33 | 34 | export interface PreparedDocumentFile { 35 | type: string; 36 | document: Buffer; 37 | mimetype: string; 38 | fileName: string 39 | } -------------------------------------------------------------------------------- /data/devices.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uniqueId": "karpin_id", 4 | "title": "HP Admin 1", 5 | "createdAt": "2025-01-01T16:31:54.645Z" 6 | }, 7 | { 8 | "uniqueId": "hp-admin-4", 9 | "title": "HP Admin 3", 10 | "createdAt": "2025-01-02T01:48:19.596Z" 11 | }, 12 | { 13 | "title": "HP Admin 3", 14 | "createdAt": "2025-01-02T03:33:57.659Z" 15 | }, 16 | { 17 | "uniqueId": "hp-admin-42", 18 | "title": "HP Admin 3", 19 | "createdAt": "2025-01-02T03:35:23.638Z" 20 | }, 21 | { 22 | "uniqueId": "hp-admin-423", 23 | "title": "HP Admin 3", 24 | "createdAt": "2025-01-02T03:40:15.704Z" 25 | }, 26 | { 27 | "uniqueId": "hp-admin-4233", 28 | "title": "HP Admin 3", 29 | "createdAt": "2025-01-02T04:01:42.519Z" 30 | }, 31 | { 32 | "uniqueId": "hp-admin-3", 33 | "title": "HP Admin 3", 34 | "createdAt": "2025-01-02T07:26:05.726Z" 35 | } 36 | ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | dist/ 64 | # Local Netlify folder 65 | .netlify 66 | 67 | wa-auth-info/ 68 | wa-auth-creds/ 69 | wa-bots/ 70 | build/ 71 | application.log 72 | tmp/ 73 | .vercel 74 | .DS_Store 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expressjs-baileys", 3 | "version": "1.0.0", 4 | "description": "Expressjs-Baileys is an implementation of Baileys using express.js", 5 | "keywords": [ 6 | "whatsapp", 7 | "js-whatsapp", 8 | "expressjs", 9 | "nodejs", 10 | "whatsapp-api", 11 | "whatsapp-web", 12 | "whatsapp-chat", 13 | "whatsapp-group", 14 | "automation", 15 | "multi-device" 16 | ], 17 | "main": "src/index.ts", 18 | "scripts": { 19 | "dev": "npx nodemon -x", 20 | "build": "rm -rf dist && ./node_modules/@vercel/ncc/dist/ncc/cli.js build -m -d", 21 | "test": "echo \"Error: no test specified\" && exit 1" 22 | }, 23 | "devDependencies": { 24 | "@types/bull": "^4.10.0", 25 | "@types/cors": "^2.8.17", 26 | "@types/express": "^4.17.21", 27 | "@types/node": "^20.11.24", 28 | "@types/qrcode": "^1.5.5", 29 | "@vercel/ncc": "^0.38.3", 30 | "copyfiles": "^2.4.1", 31 | "nodemon": "^3.1.0", 32 | "ts-node": "^10.9.2", 33 | "typescript": "^5.3.3" 34 | }, 35 | "dependencies": { 36 | "@hapi/boom": "^10.0.1", 37 | "@netlify/functions": "^2.7.0", 38 | "@whiskeysockets/baileys": "^6.7.18", 39 | "axios": "^1.7.2", 40 | "bull": "^4.12.2", 41 | "cors": "^2.8.5", 42 | "dotenv": "^16.4.5", 43 | "express": "^4.19.2", 44 | "express-validator": "^7.0.1", 45 | "jimp": "^0.16.13", 46 | "libphonenumber-js": "^1.10.58", 47 | "link-preview-js": "^3.0.5", 48 | "pm2": "^6.0.8", 49 | "qrcode": "^1.5.3", 50 | "serverless-http": "^3.2.0", 51 | "sharp": "^0.32.6", 52 | "winston": "^3.13.0" 53 | }, 54 | "homepage": "https://masbroweb.com", 55 | "author": { 56 | "name": "Daulay Reza", 57 | "email": "daulayreza@gmail.com" 58 | }, 59 | "license": "MIT" 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expressjs-Baileys-Whatsapp-Api-Unofficial 2 | 3 | ## Description 4 | 5 | Expressjs-Baileys is an implementation of [Baileys](https://github.com/WhiskeySockets/Baileys) using express.js, allowing Baileys to be used as a REST API. By using Baileys, you can set up your own WhatsApp API server. This project includes the main feature of supporting multiple accounts, enabling users to log in with more than one WhatsApp account. 6 | 7 | ## Features 8 | 9 | - ✅ Multiple Account 10 | - ✅ QR Code Generator 11 | - ✅ Send Image Message (support for other file types will follow) 12 | - ✅ Logout Request 13 | 14 | ## Installation 15 | 16 | To install the project dependencies, run: 17 | ```bash 18 | npm install 19 | ``` 20 | 21 | To start the development server, run: 22 | ```bash 23 | npm run dev 24 | ``` 25 | 26 | To build the project to JavaScript, run: 27 | ```bash 28 | npm run build 29 | ``` 30 | 31 | ## Usage 32 | 33 | 1. Every request must include `cred_id`, which serves as an identifier. 34 | 2. To check connection status, send a GET request to `/get-state?cred_id=xxx`. 35 | 3. To get a login QR code, send a GET request to `/get-qrcode?cred_id=xxx`. 36 | 4. To log out, send a GET request to `/logout?cred_id=xxx`. 37 | 5. To send a text message, send a POST request to `/send-text-message?cred_id=xxx` with the following JSON body: 38 | ```json 39 | { 40 | "phone_number": "62823xxxxxxx", 41 | "message": "ini pesan multimedia sukses" 42 | } 43 | ``` 44 | 6. To send an image message, send a POST request to `/send-media-message?cred_id=xxx` with the following JSON body: 45 | ```json 46 | { 47 | "phone_number": "62823xxxxxxx", 48 | "media_filename": "your-image-name-with-extension", 49 | "media": "your-image-https-url", 50 | "message": "non mandatory" 51 | } 52 | ``` 53 | ## Additional 54 | To remove old temporary files, set up a cron job to periodically call the endpoint below: 55 | 56 | ``` 57 | [GET] /delete-temp-files?cred_id=xxx 58 | ``` 59 | 60 | ## License 61 | 62 | This project is licensed under the MIT License. 63 | 64 | --- 65 | 66 | ## 🌍 FreePalestine 🇵🇸 67 | 68 | We stand in solidarity with the people of Palestine. Let's work together towards peace, justice, and freedom for all. #FreePalestine -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { mkdirSync, existsSync, createWriteStream, readdirSync, statSync, unlinkSync } from 'fs'; 3 | import { dirname, join } from 'path'; 4 | 5 | export function timeout(ms: number): Promise { 6 | return new Promise(resolve => setTimeout(resolve, ms)); 7 | } 8 | export function replaceHtmlEntities(input: string): string { 9 | const entities: { [key: string]: string } = { 10 | ''': "'", 11 | '&#x27;': "'", 12 | '"': '"', 13 | '&quot;': '"', 14 | '<': '<', 15 | '&lt;': '<', 16 | '>': '>', 17 | '&gt;': '>', 18 | ' ': ' ', 19 | '&nbsp;': ' ', 20 | '©': '©', 21 | '&copy;': '©', 22 | '®': '®', 23 | '&reg;': '®', 24 | '€': '€', 25 | '&euro;': '€', 26 | '&#x2F;': '/', 27 | '/': '/', 28 | '\\\\': '\\', // Replace double backslash with a single backslash 29 | '\/': '/', // Replace forward slash 30 | // '&': '&', // Uncomment this if you need to replace '&' as well 31 | }; 32 | 33 | return input.replace(/'|&#x27;|"|&quot;|<|&lt;|>|&gt;| |&nbsp;|©|&copy;|®|&reg;|€|&euro;|&#x2F;|/|\\\\|\//g, match => entities[match]); 34 | } 35 | 36 | export function downloadTempRemoteFile (credId: string, url: string, saveAs: string): Promise { 37 | // console.log('downloadTempRemoteFile') 38 | return new Promise(async (resolve, reject) => { 39 | // (async () => { 40 | // console.log('downloadTempRemoteFile') 41 | const destinationFile = `tmp/${credId}/` + saveAs; 42 | if (await existsSync(destinationFile)){ 43 | // console.log('destinationFile') 44 | return resolve(destinationFile); 45 | } 46 | // make directory 47 | const dir = dirname(destinationFile); 48 | if (!await existsSync(dir)){ 49 | await mkdirSync(dir, { 50 | recursive: true 51 | }); 52 | // console.log('mkdirSync') 53 | } 54 | try { 55 | } catch (e) { 56 | return reject(e); 57 | } 58 | // save file 59 | axios({ 60 | method: 'get', 61 | url: url, 62 | responseType: 'stream' 63 | }).then(function (response) { 64 | response.data.pipe( 65 | createWriteStream(destinationFile) 66 | .on('finish', function () { 67 | setTimeout(() => { 68 | // create cron job to delete tmp file periodically 69 | resolve(destinationFile) 70 | }, 500); 71 | }).on('error', e => reject(e)) 72 | ) 73 | }).catch(e => reject(e)); 74 | // }) 75 | }); 76 | } 77 | 78 | export function deleteOldTempRemoteFile (credId: string): Promise { 79 | // console.log('downloadTempRemoteFile') 80 | return new Promise(async (resolve, reject) => { 81 | const oneHourAgo = Date.now() - 3600000; // 1 hour in milliseconds 82 | const result = []; 83 | const folderPath = `tmp/${credId}`; 84 | try { 85 | const files = await readdirSync(folderPath); 86 | 87 | for (const file of files) { 88 | const filePath = join(folderPath, file); 89 | const stats = await statSync(filePath); 90 | 91 | if (stats.isFile() && stats.mtimeMs < oneHourAgo) { 92 | result.push(filePath); 93 | } 94 | } 95 | } catch (error) { 96 | // console.error(`Error reading folder: ${error.message}`); 97 | reject(error); 98 | } 99 | 100 | try { 101 | if (result.length > 0) { 102 | for (const filePath of result) { 103 | await unlinkSync(filePath); 104 | console.log(`Deleted file: ${filePath}`); 105 | } 106 | // return res.json({ message: 'No files to delete' }); 107 | } 108 | } catch (error) { 109 | // console.error(`Error deleting files: ${error.message}`); 110 | // throw error; 111 | reject(error); 112 | } 113 | 114 | resolve('success'); 115 | }); 116 | }; 117 | -------------------------------------------------------------------------------- /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": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["es6"], /* 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": "src", /* 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": true, /* 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 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import express from "express"; 3 | import WaService from './services/whatsapp'; 4 | import { AttachmentTypes } from './services/whatsapp/type'; 5 | import { deleteOldTempRemoteFile } from './utils'; 6 | import cors from "cors"; 7 | import { existsSync, writeFileSync, mkdirSync, readFileSync, rmSync } from 'fs'; 8 | import QRCode from 'qrcode'; 9 | import { body, query, validationResult } from "express-validator"; 10 | import { createLogger, format, transports } from 'winston'; 11 | import * as path from 'path'; 12 | 13 | import dotenv from "dotenv"; 14 | 15 | // import Bull , { Job } from 'bull'; 16 | import parsePhoneNumber, { PhoneNumber, isValidPhoneNumber } from 'libphonenumber-js'; 17 | import { Attachment, ConnectionState } from './services/whatsapp/type'; 18 | import { replaceHtmlEntities, timeout } from './utils'; 19 | 20 | dotenv.config(); 21 | const PORT = process.env.PORT || 5000; 22 | const app = express() 23 | 24 | const { combine, timestamp, prettyPrint, colorize, errors, } = format; 25 | // Create a logger instance 26 | const logger = createLogger({ 27 | // level: 'info', // Set the log level 28 | // format: winston.format.json(), // Specify the log format 29 | format: combine( 30 | errors({ stack: true }), // <-- use errors format 31 | colorize(), 32 | timestamp(), 33 | prettyPrint() 34 | ), 35 | transports: [ 36 | new transports.Console(), // Log to console 37 | new transports.File({ filename: 'application.log' }), // Log to a file 38 | ], 39 | }) 40 | 41 | // interface waServiceClassObject { 42 | // [key: string]: WaService; 43 | // } 44 | 45 | interface waServiceClassMap { 46 | [key: string]: T; 47 | } 48 | 49 | 50 | // let waServiceClass: [waServiceClassObject]; 51 | let waServiceClass: waServiceClassMap | undefined = {}; 52 | const credBaseDir = 'wa-auth-creds'; 53 | const qrCodeBasedir = './wa-bots/qr-codes'; 54 | 55 | // const initWaServer = async (): Promise => { 56 | // // console.log('connecting') 57 | // return new Promise(async (resolve) => { 58 | // // (async () => { 59 | // try { 60 | // waServiceClass = new WaService(credId, './wa-auth-info'); 61 | // await waServiceClass.connect(); 62 | // waServiceClass.on('service.whatsapp.qr', async (value) => { 63 | // const dir = `./wa-bots/qr-codes`; 64 | // if (!await existsSync(dir)){ 65 | // await mkdirSync(dir, { recursive: true }); 66 | // } 67 | // await writeFileSync(`${dir}/qr-code-${credId}.txt`, value.qr.toString()) 68 | // }) 69 | // // console.log('resolve') 70 | // resolve(waServiceClass); 71 | // } catch (error) { 72 | // logger.info(`Error initWaServer`, { error }); 73 | // // logger.info(e); 74 | // } 75 | // // })() 76 | // }); 77 | // } 78 | 79 | const initWaServer = (stateId: string): Promise => { 80 | return new Promise(async (resolve) => { 81 | // console.log('waServiceClass.connect()') 82 | // create connection wa service 83 | await waServiceClass[stateId].connect(); 84 | waServiceClass[stateId].on(`service.whatsapp.qr`, async (value) => { 85 | if (!await existsSync(qrCodeBasedir)){ 86 | await mkdirSync(qrCodeBasedir, { recursive: true }); 87 | } 88 | await writeFileSync(`${qrCodeBasedir}/qr-code-${waServiceClass[stateId].getCredId()}.txt`, value.qr.toString()) 89 | }); 90 | // add delay to make sure all connected 91 | await timeout(6000); 92 | resolve(); 93 | }) 94 | } 95 | 96 | const runExpressServer = async () => { 97 | app.use(express.json()); 98 | app.use(cors()); 99 | 100 | app.listen(PORT, () => { 101 | logger.info(`Whatsapp api app listening on port ${PORT}`) 102 | }); 103 | 104 | app.use(async (req, res, next) => { 105 | if (req.query?.cred_id) { 106 | const stateId = req.query.cred_id.toString(); 107 | // console.log(`waServiceClass[${stateId}]`, waServiceClass[stateId]) 108 | if (!waServiceClass[stateId]) { 109 | // init wa service 110 | waServiceClass[stateId] = new WaService(stateId) 111 | waServiceClass[stateId].setCredBaseDir(credBaseDir); 112 | try { 113 | await waServiceClass[stateId].checkConnection(); 114 | } catch (e) { 115 | if (typeof e === 'string' && e === 'waiting for connection') { 116 | await initWaServer(stateId); 117 | } 118 | } 119 | } 120 | } else { 121 | return res.status(400).json('cred_id is required') 122 | } 123 | next() 124 | }) 125 | 126 | 127 | app.get('/', (req, res) => { 128 | res.send('🇵🇸 Free Palestine!') 129 | }) 130 | 131 | app.get('/delete-temp-files', (req, res) => { 132 | // @ts-ignore 133 | const stateId = req.query.cred_id.toString(); 134 | if (!waServiceClass[stateId]) { 135 | return res.status(400).json('connection uninitialized'); 136 | } 137 | deleteOldTempRemoteFile(stateId); 138 | res.json('delete on progress') 139 | }) 140 | 141 | app.get('/logout', async (req, res) => { 142 | // @ts-ignore 143 | const stateId = req.query.cred_id.toString(); 144 | if (!waServiceClass[stateId]) { 145 | return res.status(400).json('connection uninitialized'); 146 | } 147 | 148 | try { 149 | await waServiceClass[stateId].checkConnection() 150 | waServiceClass[stateId].disconnect(); 151 | } catch (error) { 152 | logger.info(error) 153 | } 154 | await timeout(3000); 155 | deleteOldTempRemoteFile(stateId); 156 | res.json('success logout') 157 | }); 158 | 159 | app.get('/restart-web-socket', async (req, res) => { 160 | // @ts-ignore 161 | const stateId = req.query.cred_id.toString(); 162 | if (!waServiceClass[stateId]) { 163 | return res.status(400).json('connection uninitialized'); 164 | } 165 | 166 | try { 167 | // await waServiceClass.checkConnection(); 168 | waServiceClass[stateId].restartWebSocket(); 169 | } catch (error) { 170 | logger.info(error) 171 | } 172 | 173 | res.json('success restart web socket') 174 | }) 175 | 176 | app.get('/restart', async (req, res) => { 177 | // @ts-ignore 178 | const stateId = req.query.cred_id.toString(); 179 | if (!waServiceClass[stateId]) { 180 | return res.status(400).json('connection uninitialized'); 181 | } 182 | 183 | try { 184 | // await waServiceClass.checkConnection(); 185 | waServiceClass[stateId].disconnect(true); 186 | } catch (error) { 187 | logger.info(error) 188 | } 189 | // you must add delay to make sure everything done 190 | await timeout(3000); 191 | 192 | try { 193 | await waServiceClass[stateId].forceReset(); 194 | // const dir = `./wa-bots/qr-codes`; 195 | // if (await existsSync(dir)){ 196 | // await rmSync(dir, { recursive: true, force: true }); 197 | // } 198 | } catch (error) { 199 | logger.info(error) 200 | } 201 | await initWaServer(stateId); 202 | res.json('success restart') 203 | }) 204 | 205 | app.get('/get-qrcode', async (req, res) => { 206 | // @ts-ignore 207 | const stateId = req.query.cred_id.toString(); 208 | if (!waServiceClass[stateId]) { 209 | return res.status(400).json('connection uninitialized'); 210 | } 211 | 212 | try { 213 | await waServiceClass[stateId].checkConnection(); 214 | res.json('connected'); 215 | return ; 216 | } catch (e) { 217 | } 218 | 219 | let qrCodeString: string = ''; 220 | try { 221 | qrCodeString = await readFileSync(`${qrCodeBasedir}/qr-code-${waServiceClass[stateId].getCredId()}.txt`, 'utf-8'); 222 | } catch (err) { 223 | console.error(err) 224 | res.send('qr code not available'); 225 | return ; 226 | } 227 | 228 | try { 229 | qrCodeString = await QRCode.toDataURL(qrCodeString); 230 | res.setHeader("Content-Type", "text/html") 231 | res.send(` 232 | 233 |

Scan Segera

234 | `) 235 | } catch (err) { 236 | console.error(err) 237 | res.send('failed to get qr code') 238 | } 239 | }) 240 | 241 | // app.get('/keep-alive', async (req, res) => { 242 | // try { 243 | // if (process.env.KEEP_ALIVE_NUMBER) { 244 | // // await waServiceClass.checkConnection() 245 | // // waMessageQueue.add({ 246 | // // to: process.env.KEEP_ALIVE_NUMBER ? process.env.KEEP_ALIVE_NUMBER : '', 247 | // // message: 248 | // // '*REPORT '+ credId +'*\n\nStatus: *Active*\nLast Update: *' + new Date().toLocaleString() + '*\nStamp: *' + ( (Math.random() + 1).toString(36).substring(7) ) + '*\n\n\n\nTerima Kasih Telah menggunakan layanan kami\n\n*Masbroweb.com*' 249 | // // }); 250 | // await waServiceClass.sendTextMessage( 251 | // process.env.KEEP_ALIVE_NUMBER ? process.env.KEEP_ALIVE_NUMBER : '', 252 | // '*REPORT '+ credId +'*\n\nStatus: *Active*\nLast Update: *' + new Date().toLocaleString() + '*\nStamp: *' + ( (Math.random() + 1).toString(36).substring(7) ) + '*\n\n\n\nTerima Kasih Telah menggunakan layanan kami\n\n*Masbroweb.com*' 253 | // ); 254 | // } 255 | // res.send('success send message queue') 256 | // } catch (e) { 257 | // console.error('error send message', e) 258 | // res.send('failed send message') 259 | // } 260 | // }) 261 | 262 | app.get('/get-state', async (req, res) => { 263 | // @ts-ignore 264 | const stateId = req.query.cred_id.toString(); 265 | if (!waServiceClass[stateId]) { 266 | return res.status(400).json('connection uninitialized'); 267 | } 268 | 269 | if (await waServiceClass[stateId].getState() === ConnectionState.idle) { 270 | await waServiceClass[stateId].initializeConnection() 271 | await timeout(5000); 272 | } 273 | 274 | try { 275 | await waServiceClass[stateId].checkConnection(); 276 | res.json('connected'); 277 | } catch (e) { 278 | console.error('error get state', e) 279 | return res.status(400).json(typeof e === 'string' ? e : 'failed check connection') 280 | } 281 | }) 282 | 283 | app.post('/send-text-message', 284 | body('phone_number').notEmpty().escape(), 285 | body('message').notEmpty().escape(), 286 | async (req, res) => { 287 | // @ts-ignore 288 | const stateId = req.query.cred_id.toString(); 289 | if (!waServiceClass[stateId]) { 290 | return res.status(400).json('connection uninitialized'); 291 | } 292 | 293 | const errors = validationResult(req); 294 | if (!errors.isEmpty()) { 295 | return res.status(400).json({ errors: errors.array() }); 296 | } 297 | 298 | const phoneNumber = isValidPhoneNumber(req.body.phone_number, 'ID') ? parsePhoneNumber(req.body.phone_number, 'ID') : null 299 | if (phoneNumber) { 300 | req.body.phone_number = phoneNumber.number.toString().replace("+", ""); 301 | } else { 302 | return res.status(400).json({ errors: [ 303 | { 304 | value: req.body.phone_number, 305 | msg: 'Invalid phone number', 306 | param: 'phone_number', 307 | location: 'body' 308 | } 309 | ] }); 310 | } 311 | 312 | if (await waServiceClass[stateId].getState() === ConnectionState.idle) { 313 | await waServiceClass[stateId].initializeConnection() 314 | await timeout(5000); 315 | } 316 | // logger.info('connection: ' + (await waServiceClass[stateId].getState())) 317 | 318 | try { 319 | await waServiceClass[stateId].checkConnection(); 320 | // setTimeout(() => { 321 | // let message = req.body.message.replaceAll('&#x2F;', "/"); 322 | // message = message.replaceAll('/', "/"); 323 | // console.log('req.body.message', req.body.message) 324 | await waServiceClass[stateId].sendTextMessage(req.body.phone_number, replaceHtmlEntities(req.body.message)) 325 | // , 7000}); 326 | // waMessageQueue.add({ 327 | // to: req.body.phone_number, 328 | // message: req.body.message 329 | // }); 330 | res.json('success') 331 | } catch (e) { 332 | logger.info(e) 333 | if (e === 'waiting for connection') { 334 | return res.status(400).json('please wait a second') 335 | } else if (e === 'no active connection found') { 336 | return res.status(400).json('please scan barcode') 337 | // return res.redirect('/scan-barcode') 338 | } else if (e === 'number not exists') { 339 | return res.status(400).json('number not exists') 340 | // return res.redirect('/scan-barcode') 341 | } 342 | // @ts-ignore 343 | if (e && e.message && e.message === 'Connection Closed') { 344 | logger.info('masuk kondisi Connection Closed') 345 | await waServiceClass[stateId].initializeConnection() 346 | // await initWaServer(stateId); 347 | } 348 | res.status(500).json('failed send message') 349 | } 350 | }); 351 | 352 | app.post('/send-media-message', 353 | body('phone_number').notEmpty().escape(), 354 | body('message').escape(), 355 | body('media').notEmpty(), 356 | body('media_filename').notEmpty(), 357 | async (req, res) => { 358 | // @ts-ignore 359 | const stateId = req.query.cred_id.toString(); 360 | if (!waServiceClass[stateId]) { 361 | return res.status(400).json('connection uninitialized'); 362 | } 363 | 364 | const errors = validationResult(req); 365 | if (!errors.isEmpty()) { 366 | return res.status(400).json({ errors: errors.array() }); 367 | } 368 | 369 | const phoneNumber = isValidPhoneNumber(req.body.phone_number, 'ID') ? parsePhoneNumber(req.body.phone_number, 'ID') : null 370 | if (phoneNumber) { 371 | req.body.phone_number = phoneNumber.number.toString().replace("+", ""); 372 | } else { 373 | return res.status(400).json({ errors: [ 374 | { 375 | value: req.body.phone_number, 376 | msg: 'Invalid phone number', 377 | param: 'phone_number', 378 | location: 'body' 379 | } 380 | ] }); 381 | } 382 | 383 | try { 384 | await waServiceClass[stateId].checkConnection(); 385 | // setTimeout(() => { 386 | let message = ''; 387 | // console.log('req.body.message', req.body.message) 388 | if (req.body.message) { 389 | message = replaceHtmlEntities(req.body.message) 390 | } 391 | 392 | const pathname = new URL(req.body.media).pathname; 393 | const extension = path.extname(pathname).slice(1); 394 | 395 | 396 | let fileType: AttachmentTypes; 397 | if (['png','jpeg', 'jpg'].includes(extension)) { 398 | fileType = AttachmentTypes.photo 399 | } else { 400 | fileType = AttachmentTypes.document 401 | } 402 | 403 | const media: Attachment = { 404 | url: req.body.media, 405 | name: req.body.media_filename, 406 | filesize: 0, 407 | type: fileType 408 | } 409 | waServiceClass[stateId].sendMediaMessage(req.body.phone_number, media, message) 410 | res.json('success') 411 | } catch (e) { 412 | logger.info(e) 413 | if (e === 'waiting for connection') { 414 | return res.status(400).json('please wait a second') 415 | } else if (e === 'no active connection found') { 416 | return res.status(400).json('please scan barcode') 417 | // return res.redirect('/scan-barcode') 418 | } else if (e === 'number not exists') { 419 | return res.status(400).json('number not exists') 420 | // return res.redirect('/scan-barcode') 421 | } 422 | res.status(500).json('failed send message') 423 | } 424 | }); 425 | // return app; 426 | } 427 | runExpressServer(); 428 | // console.log('connected') 429 | // export default app; -------------------------------------------------------------------------------- /src/services/whatsapp/index.ts: -------------------------------------------------------------------------------- 1 | import makeWASocket, { Browsers, DisconnectReason, useMultiFileAuthState, WASocket, fetchLatestWaWebVersion } from '@whiskeysockets/baileys'; 2 | // import QRCode from 'qrcode'; 3 | import { /* writeFileSync, */ unlinkSync, readFileSync, mkdirSync, existsSync, rmSync, writeFileSync, createWriteStream } from 'fs'; 4 | import { Attachment, ConnectionState, PreparedPhotoFile, PreparedVideoFile, PreparedDocumentFile, AttachmentTypes } from './type'; 5 | import { dirname, join } from 'path' 6 | import { Boom } from '@hapi/boom' 7 | import { tmpdir } from 'os' 8 | import { EventEmitter } from 'events'; 9 | import axios from 'axios'; 10 | import { downloadTempRemoteFile } from './../../utils'; 11 | import { createLogger, format, transports } from 'winston'; 12 | // const { exec } = require("child_process"); 13 | // const pathToFfmpeg = require('ffmpeg-static'); 14 | 15 | const { combine, timestamp, prettyPrint, colorize, errors, } = format; 16 | // Create a logger instance 17 | const logger = createLogger({ 18 | // level: 'info', // Set the log level 19 | // format: winston.format.json(), // Specify the log format 20 | format: combine( 21 | errors({ stack: true }), // <-- use errors format 22 | colorize(), 23 | timestamp(), 24 | prettyPrint() 25 | ), 26 | transports: [ 27 | new transports.Console(), // Log to console 28 | new transports.File({ filename: 'application.log' }), // Log to a file 29 | ], 30 | }) 31 | 32 | interface ConnectionObject { 33 | [key: string]: WASocket; 34 | } 35 | 36 | export default class WhatsApp extends EventEmitter { 37 | private connections: ConnectionObject; 38 | private credId: string; 39 | private credBaseDir: string = ''; 40 | private state: ConnectionState; 41 | constructor (credId: string) { 42 | super(); 43 | this.credId = credId; 44 | // this.credBaseDir = credBaseDir; 45 | this.state = ConnectionState.idle; 46 | this.connections = {}; 47 | } 48 | 49 | getCredId (): string { 50 | return this.credId 51 | } 52 | 53 | setCredBaseDir (credBaseDir: string): void { 54 | this.credBaseDir = credBaseDir; 55 | } 56 | 57 | getConnections (): { [key: string]: WASocket } { 58 | return this.connections; 59 | } 60 | 61 | findConnection (): WASocket | null { 62 | return this.connections[this.credId] ? this.connections[this.credId] : null; 63 | } 64 | 65 | setConnection (sock: WASocket): WASocket { 66 | return this.connections[this.credId] = sock; 67 | } 68 | 69 | // private async extractVideoThumb( 70 | // path: string, 71 | // destPath: string, 72 | // time: string, 73 | // size: { width: number, height: number }, 74 | // ): Promise { 75 | // return new Promise((resolve, reject) => { 76 | // const cmd = `${pathToFfmpeg} -ss ${time} -i ${path.replace(/ /g, '\\ ')} -y -vf scale=${size.width}:-1 -vframes 1 -f image2 ${destPath}` 77 | // exec(cmd, (err: Error) => { 78 | // if(err) { 79 | // reject(err) 80 | // } 81 | // resolve() 82 | // }) 83 | // }) 84 | // } 85 | 86 | restartWebSocket (): void { 87 | const conn = this.findConnection() 88 | if (conn) { 89 | this.setState(ConnectionState.idle) 90 | conn.end(new Error("restart")) 91 | } 92 | } 93 | 94 | async removeConnection (force = false): Promise { 95 | if (this.connections[this.credId]) { 96 | if (force) { 97 | try { 98 | this.connections[this.credId].logout() 99 | // this.connections[this.credId].ws.close() 100 | // this.connections[this.credId].end(new Error('force close')) 101 | } catch (e) {} 102 | } else { 103 | this.connections[this.credId].logout() 104 | } 105 | delete this.connections[this.credId] 106 | const dir = this.credBaseDir + '/' + this.credId; 107 | if (await existsSync(dir)){ 108 | await rmSync(dir, { recursive: true, force: true }); 109 | } 110 | } 111 | } 112 | 113 | forceReset (): Promise { 114 | return new Promise(async (resolve) => { 115 | // (async () => { 116 | const dir = this.credBaseDir + '/' + this.getCredId(); 117 | if (await existsSync(dir)){ 118 | await rmSync(dir, { recursive: true, force: true }); 119 | } 120 | return resolve(null) 121 | // })() 122 | }); 123 | } 124 | 125 | async setState (state: ConnectionState) { 126 | if (state !== this.state) { 127 | this.state = state; 128 | // const dir = `./wa-bots/states`; 129 | // if (!await existsSync(dir)){ 130 | // await mkdirSync(dir, { recursive: true }); 131 | // } 132 | // await writeFileSync(`${dir}/state-${this.credId}.txt`, state.toString()); 133 | this.triggerEvent('state', state); 134 | } 135 | } 136 | 137 | async getState (): Promise { 138 | return Promise.resolve(this.state); 139 | // const state: string = await readFileSync(`./wa-bots/states/state-${this.credId}.txt`, 'utf-8'); 140 | // return Promise.resolve(parseInt(state)); 141 | } 142 | 143 | private triggerEvent (eventName: string, value: any): void { 144 | this.emit(`service.whatsapp.${eventName}`, value); 145 | } 146 | 147 | async initializeConnection (): Promise { 148 | const dir = this.credBaseDir; 149 | if (!await existsSync(dir)){ 150 | await mkdirSync(dir, { recursive: true }); 151 | } 152 | const { version, isLatest } = await fetchLatestWaWebVersion({}); 153 | const { state, saveCreds } = await useMultiFileAuthState(`${dir}/` + this.credId) 154 | const sock = makeWASocket({ 155 | version, 156 | syncFullHistory: true, 157 | browser: Browsers.windows('Desktop'), 158 | // markOnlineOnConnect: false, 159 | printQRInTerminal: false, 160 | auth: state, 161 | generateHighQualityLinkPreview: true, 162 | retryRequestDelayMs: 3000 163 | }); 164 | console.log('run generateQR') 165 | this.generateQR(sock) 166 | 167 | sock.ev.on('creds.update', () => { 168 | saveCreds() 169 | }) 170 | 171 | this.setConnection(sock) 172 | return this.findConnection() 173 | } 174 | 175 | async generateQR (sock: WASocket): Promise { 176 | return new Promise((resolve, reject) => { 177 | sock.ev.on('connection.update', async (update) => { 178 | console.log('update.connection ', update.connection ) 179 | if (update.connection === 'close' && (update.lastDisconnect?.error as Boom)?.output?.statusCode === DisconnectReason.restartRequired) { 180 | // create a new socket, this socket is now useless 181 | await this.initializeConnection() 182 | } else if (update.connection === 'open') { 183 | this.setState(ConnectionState.connected) 184 | } 185 | // console.log('credId', this.credId) 186 | // console.log('wa-update', update) 187 | // if (update.connection === 'open') { 188 | // this.setState(ConnectionState.connected) 189 | // } else if (update.connection === 'close') { 190 | // const shouldReconnect = (update.lastDisconnect?.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; 191 | // logger.info('connection closed due to ', update.lastDisconnect?.error, ', reconnecting ', shouldReconnect) 192 | // // console.log('connection closed due to ', update.lastDisconnect?.error, ', reconnecting ', shouldReconnect) 193 | // // reconnect if not logged out 194 | // // if(shouldReconnect) { 195 | // // this.setState(ConnectionState.connected) 196 | // // } else { 197 | // // this.setState(ConnectionState.disconnected) 198 | // // } 199 | // if(shouldReconnect) { 200 | // this.setState(ConnectionState.connected) 201 | // // this.initializeConnection() 202 | // } else { 203 | // // await this.initializeConnection() 204 | // // sock.end(new Error("restart")) 205 | // this.setState(ConnectionState.disconnected) 206 | // } 207 | 208 | // // console.log('update.connection', update.connection) 209 | // // this.setState(ConnectionState.disconnected) 210 | // } 211 | // else { 212 | // this.setState(credId, ConnectionState.disconnected) 213 | // } 214 | // const dir = `./wa-bots/qr-codes`; 215 | // if (!await existsSync(dir)){ 216 | // await mkdirSync(dir, { recursive: true }); 217 | // } 218 | // const qrFilePath = `${dir}/qr-code-${this.credId}.png`; 219 | // if (update.isNewLogin) { 220 | // console.log('isNewLogin') 221 | // this.initializeConnection() 222 | // await unlinkSync(qrFilePath); 223 | // } 224 | if (update.qr) { 225 | // console.log('get qr', update.qr) 226 | this.setState(ConnectionState.disconnected) 227 | this.triggerEvent('qr', { 228 | // path: qrFilePath, 229 | qr: update.qr 230 | }) 231 | // console.log('Scan the QR code with your WhatsApp app.') 232 | resolve(update.qr) 233 | // QRCode.toFile(qrFilePath, update.qr, { 234 | // errorCorrectionLevel: 'H', 235 | // }).then(() => { 236 | // this.triggerEvent('qr', { 237 | // path: qrFilePath, 238 | // qr: update.qr 239 | // }) 240 | // // console.log('Scan the QR code with your WhatsApp app.') 241 | // resolve(qrFilePath) 242 | // }).catch(err => { 243 | // reject(err) 244 | // }) 245 | } 246 | }) 247 | }) 248 | } 249 | 250 | async connect (): Promise { 251 | return new Promise(async (resolve, reject) => { 252 | // (async () => { 253 | try { 254 | let sock = this.findConnection(); 255 | // this.setState(ConnectionState.idle) 256 | 257 | if (!sock) { 258 | // console.log('initializeConnection') 259 | sock = await this.initializeConnection() 260 | } 261 | 262 | setTimeout(async () => { 263 | // console.log('state', await this.getState()); 264 | this.triggerEvent('state', await this.getState()); 265 | }, 3000); 266 | 267 | // sock.ev.on('connection.update', (update) => { 268 | // if (update.connection === 'open') { 269 | // // sock.auth 270 | // console.log('sock.auth', sock) 271 | // this.setSessionToDB(credId, sock) 272 | // } 273 | // }) 274 | 275 | resolve(sock) 276 | } catch (error) { 277 | reject(error) 278 | } 279 | // })() 280 | }); 281 | } 282 | 283 | async disconnect (force = false): Promise { 284 | // this.setState(ConnectionState.idle) 285 | return new Promise((resolve, reject) => { 286 | try { 287 | this.removeConnection(force) 288 | resolve(null); 289 | // delete folder wa-bot-info 290 | } catch (error) { 291 | reject(error) 292 | } 293 | }); 294 | // setTimeout(() => { 295 | // this.setState(ConnectionState.disconnected) 296 | // }, 1500); 297 | } 298 | 299 | async checkConnection (): Promise { 300 | return new Promise(async (resolve, reject) => { 301 | // (async () => { 302 | try { 303 | const conn = this.findConnection() 304 | const state = await this.getState() 305 | if (state === ConnectionState.idle) { 306 | return reject('waiting for connection') 307 | } 308 | if (state === ConnectionState.disconnected || !conn) { 309 | return reject('no active connection found') 310 | } 311 | return resolve(state) 312 | } catch (error) { 313 | return reject(error) 314 | } 315 | // })() 316 | }) 317 | } 318 | 319 | async sendTextMessage (destinationNumber: string, messageContent: string): Promise { 320 | return new Promise(async (resolve, reject) => { 321 | // (async () => { 322 | try { 323 | if (!destinationNumber || !messageContent) { 324 | return reject('missing required parameters') 325 | } 326 | 327 | const formattedRecipient = `${destinationNumber}@c.us` 328 | if (!/^[\d]+@c.us$/.test(formattedRecipient)) { 329 | return reject('invalid recipient format') 330 | } 331 | 332 | const conn = this.findConnection() 333 | const state = await this.getState() 334 | if (state === ConnectionState.idle) { 335 | return reject('waiting for connection') 336 | } 337 | if (state === ConnectionState.disconnected || !conn) { 338 | return reject('no active connection found') 339 | } 340 | 341 | // const [result] = await conn.onWhatsApp(formattedRecipient); 342 | // @ts-ignore 343 | const [result] = await conn.onWhatsApp(formattedRecipient) 344 | if (result.exists) { 345 | 346 | } else { 347 | return reject('number not exists') 348 | } 349 | 350 | 351 | await conn.sendMessage(formattedRecipient, { text: messageContent }) 352 | return resolve(`success send message to ${formattedRecipient} with message ${messageContent}`) 353 | } catch (error) { 354 | return reject(error) 355 | } 356 | // })() 357 | }) 358 | } 359 | 360 | // downloadTempRemoteFile (url: string, saveAs: string): Promise { 361 | // // console.log('downloadTempRemoteFile') 362 | // return new Promise(async (resolve, reject) => { 363 | // // (async () => { 364 | // // console.log('downloadTempRemoteFile') 365 | // const destinationFile = `tmp/${this.getCredId()}/` + saveAs; 366 | // if (await existsSync(destinationFile)){ 367 | // // console.log('destinationFile') 368 | // return resolve(destinationFile); 369 | // } 370 | // // make directory 371 | // const dir = dirname(destinationFile); 372 | // if (!await existsSync(dir)){ 373 | // await mkdirSync(dir, { 374 | // recursive: true 375 | // }); 376 | // // console.log('mkdirSync') 377 | // } 378 | // try { 379 | // } catch (e) { 380 | // return reject(e); 381 | // } 382 | // // save file 383 | // axios({ 384 | // method: 'get', 385 | // url: url, 386 | // responseType: 'stream' 387 | // }).then(function (response) { 388 | // response.data.pipe( 389 | // createWriteStream(destinationFile) 390 | // .on('finish', function () { 391 | // setTimeout(() => { 392 | // // create cron job to delete tmp file periodically 393 | // resolve(destinationFile) 394 | // }, 500); 395 | // }).on('error', e => reject(e)) 396 | // ) 397 | // }).catch(e => reject(e)); 398 | // // }) 399 | // }); 400 | // } 401 | 402 | async sendMediaMessage (destinationNumber: string, file: Attachment, messageContent: string): Promise { 403 | return new Promise(async (resolve, reject) => { 404 | // (async () => { 405 | try { 406 | if (!destinationNumber || !file || !file.url) { 407 | return reject('missing required parameters') 408 | } 409 | 410 | const formattedRecipient = `${destinationNumber}@c.us` 411 | if (!/^[\d]+@c.us$/.test(formattedRecipient)) { 412 | return reject('invalid recipient format') 413 | } 414 | 415 | const conn = this.findConnection() 416 | const state = await this.getState() 417 | if (state === ConnectionState.idle) { 418 | return reject('waiting for connection') 419 | } 420 | if (state === ConnectionState.disconnected || !conn) { 421 | return reject('no active connection found') 422 | } 423 | 424 | const [result] = await conn.onWhatsApp(formattedRecipient); 425 | if (!result.exists) { 426 | return reject('number not exists') 427 | } 428 | 429 | // const savedFile = await downloadTempRemoteFile(this.getCredId(), file.url, file.name); 430 | // console.log('savedFile', savedFile) 431 | 432 | if (file.type === AttachmentTypes.photo) { 433 | await conn.sendMessage(formattedRecipient, { 434 | // image: readFileSync(savedFile), 435 | image: { url: file.url }, 436 | caption: messageContent 437 | // gifPlayback: true 438 | }); 439 | } /* else if (file.type === 'video') { 440 | // generate thumbnail 441 | let jpegThumbnail = null; 442 | const imgFilename = join(tmpdir(), ( 'BAE5' + Math.floor(Math.random() * 10) ) + '.jpg') 443 | try { 444 | await this.extractVideoThumb(file.path, imgFilename, '00:00:00', { width: 32, height: 32 }) 445 | const buff = await readFileSync(imgFilename) 446 | jpegThumbnail = buff.toString('base64') 447 | await unlinkSync(imgFilename) 448 | } catch(err) { 449 | return reject(err) 450 | } 451 | await conn.sendMessage(formattedRecipient, { 452 | video: readFileSync(file.path), 453 | caption: messageContent, 454 | jpegThumbnail: jpegThumbnail 455 | // gifPlayback: true 456 | }); 457 | } */ else { 458 | const ext = file.url.split('.').pop(); 459 | let mimetype = 'application/pdf'; 460 | if (ext === 'csv') { 461 | mimetype = 'text/csv'; 462 | } else if (ext === 'doc' || ext === 'docx') { 463 | mimetype = 'application/msword'; 464 | } else if (ext === 'xls' || ext === 'xlsx') { 465 | mimetype = 'application/vnd.ms-excel'; 466 | } else if (ext === 'ppt' || ext === 'pptx') { 467 | mimetype = 'application/vnd.ms-powerpoint'; 468 | } 469 | await conn.sendMessage(formattedRecipient, { 470 | document: { 471 | url: file.url 472 | }, 473 | caption: messageContent, 474 | mimetype: mimetype, 475 | fileName: file.name, 476 | // gifPlayback: true 477 | }); 478 | } 479 | 480 | return resolve(`success send message to ${formattedRecipient} with media ${file.url}`) 481 | } catch (error) { 482 | return reject(error) 483 | } 484 | // })() 485 | }) 486 | } 487 | 488 | // async prepareMediaMessage (file: Attachment): Promise { 489 | // return new Promise((resolve, reject) => { 490 | // (async () => { 491 | // if (!file || !file.path) { 492 | // return reject('missing required parameters') 493 | // } 494 | // try { 495 | // if (file.type === 'photo') { 496 | // const result: PreparedPhotoFile = { 497 | // type: file.type, 498 | // image: readFileSync(file.path) 499 | // }; 500 | // return resolve(result); 501 | // } else if (file.type === 'video') { 502 | // // generate thumbnail 503 | // let jpegThumbnail = null; 504 | // const imgFilename = join(tmpdir(), ( 'BAE5' + Math.floor(Math.random() * 10) ) + '.jpg') 505 | // try { 506 | // await this.extractVideoThumb(file.path, imgFilename, '00:00:00', { width: 32, height: 32 }) 507 | // const buff = await readFileSync(imgFilename) 508 | // jpegThumbnail = buff.toString('base64') 509 | // await unlinkSync(imgFilename) 510 | // } catch(err) { 511 | // return reject(err) 512 | // } 513 | // const result: PreparedVideoFile = { 514 | // type: file.type, 515 | // video: readFileSync(file.path), 516 | // jpegThumbnail: jpegThumbnail 517 | // }; 518 | // return resolve(result); 519 | // } else { 520 | // const ext = file.path.split('.').pop(); 521 | // let mimetype = 'application/pdf'; 522 | // if (ext === 'csv') { 523 | // mimetype = 'text/csv'; 524 | // } else if (ext === 'doc' || ext === 'docx') { 525 | // mimetype = 'application/msword'; 526 | // } else if (ext === 'xls' || ext === 'xlsx') { 527 | // mimetype = 'application/vnd.ms-excel'; 528 | // } else if (ext === 'ppt' || ext === 'pptx') { 529 | // mimetype = 'application/vnd.ms-powerpoint'; 530 | // } 531 | // const result: PreparedDocumentFile = { 532 | // type: file.type, 533 | // document: readFileSync(file.path), 534 | // mimetype: mimetype, 535 | // fileName: file.name, 536 | // }; 537 | // return resolve(result); 538 | // } 539 | // } catch (error) { 540 | // return reject(error) 541 | // } 542 | // })() 543 | // }) 544 | // } 545 | 546 | // async sendPreparedMediaMessage (destinationNumber: string, preparedFile: PreparedPhotoFile | PreparedVideoFile | PreparedDocumentFile, messageContent: string): Promise { 547 | // return new Promise((resolve, reject) => { 548 | // (async () => { 549 | // try { 550 | // if (!destinationNumber) { 551 | // return reject('missing required parameters') 552 | // } 553 | 554 | // const formattedRecipient = `${destinationNumber}@c.us` 555 | // if (!/^[\d]+@c.us$/.test(formattedRecipient)) { 556 | // return reject('invalid recipient format') 557 | // } 558 | 559 | // const conn = this.findConnection() 560 | // const state = await this.getState() 561 | // if (state === ConnectionState.idle) { 562 | // return reject('waiting for connection') 563 | // } 564 | // if (state === ConnectionState.disconnected || !conn) { 565 | // return reject('no active connection found') 566 | // } 567 | 568 | // if (preparedFile.type === 'photo') { 569 | // preparedFile = preparedFile as PreparedPhotoFile; 570 | // await conn.sendMessage(formattedRecipient, { 571 | // image: preparedFile.image, 572 | // caption: messageContent 573 | // // gifPlayback: true 574 | // }); 575 | // } else if (preparedFile.type === 'video') { 576 | // preparedFile = preparedFile as PreparedVideoFile; 577 | // await conn.sendMessage(formattedRecipient, { 578 | // video: preparedFile.video, 579 | // caption: messageContent, 580 | // jpegThumbnail: preparedFile.jpegThumbnail 581 | // // gifPlayback: true 582 | // }); 583 | // } else { 584 | // preparedFile = preparedFile as PreparedDocumentFile; 585 | // await conn.sendMessage(formattedRecipient, { 586 | // document: preparedFile.document, 587 | // caption: messageContent, 588 | // mimetype: preparedFile.mimetype, 589 | // fileName: preparedFile.fileName 590 | // // gifPlayback: true 591 | // }); 592 | // } 593 | 594 | // return resolve(`success send message to ${formattedRecipient} with media`) 595 | // } catch (error) { 596 | // return reject(error) 597 | // } 598 | // })() 599 | // }) 600 | // } 601 | } --------------------------------------------------------------------------------