├── migrations ├── 20240510202558_change_value_of_creds_columm │ └── migration.sql ├── migration_lock.toml ├── 20240510203259_add_created_at_columm │ └── migration.sql ├── 20240510201050_unique_botid_key_id │ └── migration.sql ├── 20240510195855_init │ └── migration.sql └── 20240510201932_change_to_sessions_table │ └── migration.sql ├── sample.env ├── schema.prisma ├── package.json ├── logs.js ├── readme.md ├── .gitignore ├── usePrismaDBAuthStore.js └── index.js /migrations/20240510202558_change_value_of_creds_columm/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `sessions` MODIFY `creds` TEXT NULL; 3 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL="mysql://usuario:senha@endereco-do-banco-de-dados:porta/nome-do-banco-de-dados" # ALTERE COM OS DADOS CORRETOS DO DB -------------------------------------------------------------------------------- /migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /migrations/20240510203259_add_created_at_columm/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `sessions` ADD COLUMN `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); 3 | -------------------------------------------------------------------------------- /migrations/20240510201050_unique_botid_key_id/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[botId,keyId]` on the table `AuthKey` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX `AuthKey_botId_keyId_key` ON `AuthKey`(`botId`, `keyId`); 9 | -------------------------------------------------------------------------------- /migrations/20240510195855_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `AuthKey` ( 3 | `id` INTEGER NOT NULL AUTO_INCREMENT, 4 | `botId` VARCHAR(191) NOT NULL, 5 | `keyId` VARCHAR(191) NOT NULL, 6 | `keyJson` VARCHAR(191) NOT NULL, 7 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 8 | `updatedAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 9 | 10 | PRIMARY KEY (`id`) 11 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 12 | -------------------------------------------------------------------------------- /schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "mysql" // modifique para usar postgresql se for sua preferencia 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model Session { 11 | id Int @id @unique @default(autoincrement()) 12 | sessionID String @unique @default(cuid()) 13 | creds String? @db.Text 14 | createdAt DateTime @default(now()) 15 | 16 | @@map("sessions") 17 | } 18 | -------------------------------------------------------------------------------- /migrations/20240510201932_change_to_sessions_table/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `AuthKey` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropTable 8 | DROP TABLE `AuthKey`; 9 | 10 | -- CreateTable 11 | CREATE TABLE `sessions` ( 12 | `id` INTEGER NOT NULL AUTO_INCREMENT, 13 | `sessionID` VARCHAR(191) NOT NULL, 14 | `creds` VARCHAR(191) NULL, 15 | 16 | UNIQUE INDEX `sessions_id_key`(`id`), 17 | UNIQUE INDEX `sessions_sessionID_key`(`sessionID`), 18 | PRIMARY KEY (`id`) 19 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "baileys-with-prisma-save-sessions-on-db", 3 | "version": "1.0.0", 4 | "description": "salvando sessoes do baileys direto em um DB com o prisma", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node .", 9 | "postinstall": "npx prisma migrate deploy" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@adiwajshing/keyed-db": "^0.2.4", 16 | "@prisma/client": "^5.13.0", 17 | "@whiskeysockets/baileys": "^6.6.0", 18 | "dotenv": "^16.4.5", 19 | "pino": "^8.14.1", 20 | "qrcode-terminal": "^0.12.0" 21 | }, 22 | "devDependencies": { 23 | "prisma": "^5.13.0" 24 | } 25 | } -------------------------------------------------------------------------------- /logs.js: -------------------------------------------------------------------------------- 1 | import pino, { multistream } from 'pino' 2 | import stream from 'stream' 3 | 4 | // função para exibir os logs do Baileys de maneira mais legivel 5 | 6 | let bufferStream = new stream.PassThrough(); 7 | 8 | bufferStream.on('data', (chunk) => { 9 | 10 | try { 11 | let logData = JSON.parse(chunk.toString()) 12 | let params = logData?.params ? logData?.params[0] : '' 13 | let msg = logData?.msg ? logData?.msg : '' 14 | console.log('❗', params, '|', msg) 15 | } catch (error) { 16 | console.log('❗', error.message) 17 | } 18 | 19 | }) 20 | 21 | const logger = pino({}, multistream([ 22 | //{ stream: process.stdout }, 23 | { stream: bufferStream } 24 | ])) 25 | 26 | 27 | export default logger -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Baileys com DB prisma para salvar as sessões em um DB (MySQL, Postgres, etc) 2 | 3 | ### Este é um exemplo inicial de como usar o projeto Baileys e armazenar as chaves de autenticação em um banco de dados. 4 | 5 | ## Como usar: 6 | 7 | 1. Crie uma instância do MySQL ou Postgres. 8 | 2. Clone este repositório e instale as dependências com `npm install`. 9 | 3. Copie o arquivo `sample.env` para `.env` e preencha os dados do seu banco. 10 | 4. Na linha 21 do arquivo `index.js`, você pode escolher o nome do bot que será salvo no banco de dados na coluna `sessionID`. 11 | 5. Por fim, execute o projeto com `npm start` . O QR code do bot aparecerá no console. Escaneie-o com seu WhatsApp. 12 | 6. Pronto! O bot foi criado e as chaves de autenticação estão sendo salvas no banco de dados (muito melhor do que armazená-las em arquivos JSON localmente). 13 | - As auth keys estarão na tabela `sessions` com as colunas: `id, sessionID, creds` e `created_at` 14 | 15 | ## Dicas: 16 | 17 | - O `index.js` é apenas o exemplo inicial do projeto Baileys. Sinta-se à vontade para modificá-lo. 18 | - O `usePrismaDBAuthStore.js` já está preparado para usar múltiplos bots no mesmo script, bastando passar um nome diferente para cada um. 19 | - O `usePrismaDBAuthStore.js` utiliza a mesma estrutura da função `useMultiFileAuthState` do Baileys, trocando apenas o salvamento do arquivo `creds.json` para o DB, o restante das keys são salvas no disco normalmente. 20 | - O `logs.js` é uma instância do Pino para exibir os logs do Baileys de maneira mais legível no console. 21 | 22 | ## Finalizando: 23 | 24 | - Sinta-se à vontade para modificar e melhorar este projeto. É bem simples mudar para outros bancos de dados relacionais. 25 | - Pull requests são bem-vindos! 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | t.js 106 | *.mp4 107 | *.mp3 108 | *.gif 109 | *.webp 110 | *.jpg 111 | *.png 112 | *.wav 113 | *.ogg 114 | config.js 115 | sessions -------------------------------------------------------------------------------- /usePrismaDBAuthStore.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | import { WAProto as proto, initAuthCreds, BufferJSON } from "@whiskeysockets/baileys" 4 | import { PrismaClient } from '@prisma/client' 5 | 6 | const prisma = new PrismaClient() 7 | 8 | const fixFileName = (file) => { 9 | if (!file) { 10 | return undefined; 11 | } 12 | const replacedSlash = file.replace(/\//g, '__'); 13 | const replacedColon = replacedSlash.replace(/:/g, '-'); 14 | return replacedColon; 15 | }; 16 | 17 | export async function keyExists(sessionID) { 18 | try { 19 | let key = await prisma.session.findUnique({ where: { sessionID: sessionID } }) 20 | return !!key 21 | } catch (error) { 22 | console.log(`${error}`) 23 | return false 24 | } 25 | } 26 | 27 | export async function saveKey(sessionID, keyJson) { 28 | const jaExiste = await keyExists(sessionID) 29 | try { 30 | if (!jaExiste) return await prisma.session.create({ data: { sessionID: sessionID, creds: JSON.stringify(keyJson) } }); 31 | await prisma.session.update({ where: { sessionID: sessionID }, data: { creds: JSON.stringify(keyJson) } }) 32 | 33 | } catch (error) { 34 | console.log(`${error}`) 35 | return null 36 | } 37 | } 38 | 39 | export async function getAuthKey(sessionID) { 40 | try { 41 | let registro = await keyExists(sessionID) 42 | if (!registro) return null 43 | let auth = await prisma.session.findUnique({ where: { sessionID: sessionID } }) 44 | return JSON.parse(auth?.creds) 45 | } catch (error) { 46 | console.log(`${error}`) 47 | return null 48 | } 49 | } 50 | 51 | async function deleteAuthKey(sessionID) { 52 | 53 | try { 54 | let registro = await keyExists(sessionID) 55 | if (!registro) return; 56 | await prisma.session.delete({ where: { sessionID: sessionID } }) 57 | } catch (error) { 58 | console.log('2', `${error}`) 59 | } 60 | } 61 | 62 | async function fileExists(file) { 63 | try { 64 | const stat = await fs.stat(file); 65 | if (stat.isFile()) return true 66 | } catch (error) { 67 | return; 68 | } 69 | } 70 | 71 | export default async function usePrismaDBAuthStore(sessionID) { 72 | 73 | const localFolder = path.join(process.cwd(), 'sessions', sessionID) 74 | const localFile = (key) => path.join(localFolder, (fixFileName(key) + '.json')) 75 | await fs.mkdir(localFolder, { recursive: true }); 76 | 77 | 78 | async function writeData(data, key) { 79 | const dataString = JSON.stringify(data, BufferJSON.replacer); 80 | 81 | if (key != 'creds') { 82 | await fs.writeFile(localFile(key), dataString) 83 | return; 84 | } 85 | await saveKey(sessionID, dataString) 86 | return; 87 | }; 88 | 89 | async function readData(key) { 90 | try { 91 | 92 | let rawData; 93 | 94 | if (key != 'creds') { 95 | if (!(await fileExists(localFile(key)))) return null; 96 | rawData = await fs.readFile(localFile(key), { encoding: 'utf-8' }) 97 | } else { 98 | rawData = (await getAuthKey(sessionID)) 99 | } 100 | 101 | const parsedData = JSON.parse(rawData, BufferJSON.reviver); 102 | return parsedData; 103 | } catch (error) { 104 | return null; 105 | } 106 | } 107 | 108 | async function removeData(key) { 109 | try { 110 | if (key != 'creds') { 111 | await fs.unlink(localFile(key)) 112 | } else { 113 | await deleteAuthKey(sessionID) 114 | } 115 | } catch (error) { 116 | return; 117 | } 118 | } 119 | 120 | let creds = await readData('creds'); 121 | if (!creds) { 122 | creds = initAuthCreds(); 123 | await writeData(creds, 'creds'); 124 | } 125 | 126 | return { 127 | state: { 128 | creds, 129 | keys: { 130 | get: async (type, ids) => { 131 | const data = {}; 132 | await Promise.all(ids.map(async (id) => { 133 | let value = await readData(`${type}-${id}`); 134 | if (type === 'app-state-sync-key' && value) { 135 | value = proto.Message.AppStateSyncKeyData.fromObject(value); 136 | } 137 | 138 | data[id] = value; 139 | })); 140 | return data; 141 | }, 142 | set: async (data) => { 143 | const tasks = []; 144 | for (const category in data) { 145 | 146 | for (const id in data[category]) { 147 | 148 | const value = data[category][id]; 149 | const key = `${category}-${id}`; 150 | 151 | tasks.push(value ? writeData(value, key) : removeData(key)); 152 | } 153 | } 154 | await Promise.all(tasks); 155 | } 156 | } 157 | }, 158 | saveCreds: () => { 159 | return writeData(creds, 'creds'); 160 | } 161 | }; 162 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import NodeCache from 'node-cache' 2 | import makeWASocket, { 3 | DisconnectReason, fetchLatestBaileysVersion, getAggregateVotesInPollMessage, 4 | makeCacheableSignalKeyStore, isJidBroadcast 5 | } from '@whiskeysockets/baileys' 6 | import dotenv from 'dotenv'; 7 | dotenv.config(); 8 | 9 | import logger from './logs.js' 10 | import usePrismaDBAuthStore from './usePrismaDBAuthStore.js'; 11 | 12 | 13 | const doReplies = !process.argv.includes('--no-reply') 14 | 15 | // external map to store retry counts of messages when decryption/encryption fails 16 | // keep this out of the socket itself, so as to prevent a message decryption/encryption loop across socket restarts 17 | const msgRetryCounterCache = new NodeCache() 18 | 19 | // start a connection 20 | const startSock = async () => { 21 | const { state, saveCreds } = await usePrismaDBAuthStore('bot_teste_123') 22 | // fetch latest version of WA Web 23 | const { version, isLatest } = await fetchLatestBaileysVersion() 24 | console.log(`using WA v${version.join('.')}, isLatest: ${isLatest}`) 25 | 26 | 27 | const sock = makeWASocket.default({ 28 | version, 29 | logger, 30 | printQRInTerminal: true, 31 | auth: { 32 | creds: state.creds, 33 | /** caching makes the store faster to send/recv messages */ 34 | keys: makeCacheableSignalKeyStore(state.keys, logger), 35 | }, 36 | msgRetryCounterCache, 37 | generateHighQualityLinkPreview: true, 38 | // ignore all broadcast messages -- to receive the same 39 | // comment the line below out 40 | shouldIgnoreJid: jid => isJidBroadcast(jid), 41 | // implement to handle retries & poll updates 42 | }) 43 | 44 | const sendMessageWTyping = async (msg, jid) => { 45 | await sock.sendMessage(jid, msg) 46 | } 47 | 48 | // the process function lets you process all events that just occurred 49 | // efficiently in a batch 50 | sock.ev.process( 51 | // events is a map for event name => event data 52 | async (events) => { 53 | // something about the connection changed 54 | // maybe it closed, or we received all offline message or connection opened 55 | if (events['connection.update']) { 56 | const update = events['connection.update'] 57 | const { connection, lastDisconnect } = update 58 | if (connection === 'close') { 59 | // reconnect if not logged out 60 | if ((lastDisconnect?.error)?.output?.statusCode !== DisconnectReason.loggedOut) { 61 | startSock() 62 | } else { 63 | console.log('Connection closed. You are logged out.') 64 | } 65 | } 66 | 67 | console.log('connection update', update) 68 | } 69 | 70 | // credentials updated -- save them 71 | if (events['creds.update']) { 72 | await saveCreds() 73 | } 74 | 75 | if (events.call) { 76 | console.log('recv call event', events.call) 77 | } 78 | 79 | // history received 80 | if (events['messaging-history.set']) { 81 | const { chats, contacts, messages, isLatest } = events['messaging-history.set'] 82 | console.log(`recv ${chats.length} chats, ${contacts.length} contacts, ${messages.length} msgs (is latest: ${isLatest})`) 83 | } 84 | 85 | // received a new message 86 | if (events['messages.upsert']) { 87 | const upsert = events['messages.upsert'] 88 | console.log('recv messages ', JSON.stringify(upsert, undefined, 2)) 89 | 90 | if (upsert.type === 'notify') { 91 | for (const msg of upsert.messages) { 92 | if (!msg.key.fromMe && doReplies) { 93 | console.log('replying to', msg.key.remoteJid) 94 | await sock.readMessages([msg.key]) 95 | await sendMessageWTyping({ text: 'Hello there!' }, msg.key.remoteJid) 96 | } 97 | } 98 | } 99 | } 100 | 101 | // messages updated like status delivered, message deleted etc. 102 | if (events['messages.update']) { 103 | console.log( 104 | JSON.stringify(events['messages.update'], undefined, 2) 105 | ) 106 | 107 | for (const { key, update } of events['messages.update']) { 108 | if (update.pollUpdates) { 109 | const pollCreation = await getMessage(key) 110 | if (pollCreation) { 111 | console.log( 112 | 'got poll update, aggregation: ', 113 | getAggregateVotesInPollMessage({ 114 | message: pollCreation, 115 | pollUpdates: update.pollUpdates, 116 | }) 117 | ) 118 | } 119 | } 120 | } 121 | } 122 | 123 | if (events['message-receipt.update']) { 124 | console.log(events['message-receipt.update']) 125 | } 126 | 127 | if (events['messages.reaction']) { 128 | console.log(events['messages.reaction']) 129 | } 130 | 131 | if (events['presence.update']) { 132 | console.log(events['presence.update']) 133 | } 134 | 135 | if (events['chats.update']) { 136 | console.log(events['chats.update']) 137 | } 138 | 139 | if (events['contacts.update']) { 140 | for (const contact of events['contacts.update']) { 141 | if (typeof contact.imgUrl !== 'undefined') { 142 | const newUrl = contact.imgUrl === null 143 | ? null 144 | : await sock.profilePictureUrl(contact.id).catch(() => null) 145 | console.log( 146 | `contact ${contact.id} has a new profile pic: ${newUrl}`, 147 | ) 148 | } 149 | } 150 | } 151 | 152 | if (events['chats.delete']) { 153 | console.log('chats deleted ', events['chats.delete']) 154 | } 155 | } 156 | ) 157 | 158 | return sock 159 | } 160 | 161 | startSock() --------------------------------------------------------------------------------