From 81e78bb14a76d5dda5f5f57084bd5275ce677366 Mon Sep 17 00:00:00 2001 From: Royhan <60024250+ookamiiixd@users.noreply.github.com> Date: Sat, 11 Mar 2023 02:51:48 +0700 Subject: [PATCH 1/4] add qr route --- .env.example | 2 +- src/controllers/session.ts | 6 ++++-- src/routes/sessions.ts | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 8a9b1f05..534848e6 100644 --- a/.env.example +++ b/.env.example @@ -3,5 +3,5 @@ PORT="3000" DATABASE_URL="mysql://root:12345@localhost:3306/baileys_api" RECONNECT_INTERVAL="5000" MAX_RECONNECT_RETRIES="5" -SSE_MAX_QR_GENERATION="10" +MAX_QR_GENERATION="10" LOG_LEVEL="warn" \ No newline at end of file diff --git a/src/controllers/session.ts b/src/controllers/session.ts index 538590d2..9a07b413 100644 --- a/src/controllers/session.ts +++ b/src/controllers/session.ts @@ -16,8 +16,10 @@ export const find: RequestHandler = (req, res) => res.status(200).json({ message: 'Session found' }); export const status: RequestHandler = (req, res) => { - const session = getSession(req.params.sessionId)!; - res.status(200).json({ status: getSessionStatus(session) }); + +export const qr: RequestHandler = (req, res) => { + const session = Session.get(req.params.sessionId)!; + res.status(200).json({ qr: session.QR() }); }; export const add: RequestHandler = async (req, res) => { diff --git a/src/routes/sessions.ts b/src/routes/sessions.ts index 3dd3cbad..ec074624 100644 --- a/src/routes/sessions.ts +++ b/src/routes/sessions.ts @@ -8,6 +8,7 @@ const router = Router(); router.get('/', controller.list); router.get('/:sessionId', sessionValidator, controller.find); router.get('/:sessionId/status', sessionValidator, controller.status); +router.get('/:sessionId/qr', sessionValidator, controller.qr); router.post('/add', body('sessionId').isString().notEmpty(), requestValidator, controller.add); router.get('/:sessionId/add-sse', controller.addSSE); router.delete('/:sessionId', sessionValidator, controller.del); From 2e27a59476a38dd5265ef818fe87708320128606 Mon Sep 17 00:00:00 2001 From: Royhan <60024250+ookamiiixd@users.noreply.github.com> Date: Sat, 11 Mar 2023 02:53:08 +0700 Subject: [PATCH 2/4] refactor to improve readibility --- .eslintrc.js | 1 + README.md | 18 +- src/controllers/contact.ts | 31 ++- src/controllers/group.ts | 21 +- src/controllers/message.ts | 20 +- src/controllers/misc.ts | 8 +- src/controllers/session.ts | 26 +- src/middlewares/session-validator.ts | 4 +- src/utils.ts | 9 + src/wa.ts | 395 +++++++++++++++------------ yarn.lock | 366 ++++++++++++++++++++++++- 11 files changed, 642 insertions(+), 257 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 6c7e090d..0502dfd1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,5 +10,6 @@ module.exports = { '@typescript-eslint/consistent-type-imports': 'error', '@typescript-eslint/no-unused-vars': 'error', '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-explicit-any': 'off', }, }; diff --git a/README.md b/README.md index 993ca554..191c16d6 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ An implementation of [@adiwajshing/Baileys](https://github.com/adiwajshing/Baile ## Requirements -- **NodeJS** version **14.5.0** or higher -- **Prisma** [supported databases](https://www.prisma.io/docs/reference/database-reference/supported-databases). Tested on MySQL and PostgreSQL +- **NodeJS** version **14.7.0** or higher +- **Prisma** [supported databases](https://github.com/ookamiiixd/baileys-store#supported-databases). Tested on MySQL and PostgreSQL ## Installation @@ -17,7 +17,9 @@ An implementation of [@adiwajshing/Baileys](https://github.com/adiwajshing/Baile npm install ``` -4. Build the project using the `build` script +4. Follow guide in the `Setup` section to setup your database first + +5. Build the project using the `build` script ```sh npm run build @@ -61,8 +63,8 @@ RECONNECT_INTERVAL="5000" # Maximum Reconnect Attempts MAX_RECONNECT_RETRIES="5" -# Maximum SSE QR Generation Attempts -SSE_MAX_QR_GENERATION="10" +# Maximum QR Generation Attempts +MAX_QR_GENERATION="10" # Pino Logger Level LOG_LEVEL="warn" @@ -70,18 +72,18 @@ LOG_LEVEL="warn" ## Usage -1. Make sure you have completed the **Installation** and **Setup** step +1. Make sure you have completed all of the step in the **Installation** and **Setup** section 1. You can then start the app using the `start` script ```sh npm run start ``` -1. Now the endpoint should be available according to your environment variables configuration. Default is at `http://localhost:3000` +Now the endpoint should be available according to your environment variables configuration. Default is at `http://localhost:3000` ## API Docs -The API documentation is available online [here](https://documenter.getpostman.com/view/18988925/2s8Z73zWbg). You can also import the **Postman Collection File** `(postman_collection.json)` into your Postman App alternatively +The API documentation is available online [here](https://documenter.getpostman.com/view/18988925/2s8Z73zWbg). You can also import the **Postman Collection File** `(postman_collection.json)` into your Postman App alternatively. The online version is always up-to-date with current development version, so make sure you read the right documentation version ## Notes diff --git a/src/controllers/contact.ts b/src/controllers/contact.ts index 0fe2dfe4..89e280a9 100644 --- a/src/controllers/contact.ts +++ b/src/controllers/contact.ts @@ -1,18 +1,21 @@ +import { serializePrisma } from '@ookamiiixd/baileys-store'; import type { RequestHandler } from 'express'; import { logger, prisma } from '../shared'; -import { getSession, jidExists } from '../wa'; +import { Session } from '../wa'; import { makePhotoURLHandler } from './misc'; export const list: RequestHandler = async (req, res) => { try { const { sessionId } = req.params; const { cursor = undefined, limit = 25 } = req.query; - const contacts = await prisma.contact.findMany({ - cursor: cursor ? { pkId: Number(cursor) } : undefined, - take: Number(limit), - skip: cursor ? 1 : 0, - where: { id: { endsWith: 's.whatsapp.net' }, sessionId }, - }); + const contacts = ( + await prisma.contact.findMany({ + cursor: cursor ? { pkId: Number(cursor) } : undefined, + take: Number(limit), + skip: cursor ? 1 : 0, + where: { id: { endsWith: 's.whatsapp.net' }, sessionId }, + }) + ).map((m) => serializePrisma(m)); res.status(200).json({ data: contacts, @@ -30,8 +33,8 @@ export const list: RequestHandler = async (req, res) => { export const listBlocked: RequestHandler = async (req, res) => { try { - const session = getSession(req.params.sessionId)!; - const data = await session.fetchBlocklist(); + const session = Session.get(req.params.sessionId)!; + const data = await session.socket.fetchBlocklist(); res.status(200).json(data); } catch (e) { const message = 'An error occured during blocklist fetch'; @@ -42,13 +45,13 @@ export const listBlocked: RequestHandler = async (req, res) => { export const updateBlock: RequestHandler = async (req, res) => { try { - const session = getSession(req.params.sessionId)!; + const session = Session.get(req.params.sessionId)!; const { jid, action = 'block' } = req.body; - const exists = await jidExists(session, jid); + const exists = await session.jidExists(jid); if (!exists) return res.status(400).json({ error: 'Jid does not exists' }); - await session.updateBlockStatus(jid, action); + await session.socket.updateBlockStatus(jid, action); res.status(200).json({ message: `Contact ${action}ed` }); } catch (e) { const message = 'An error occured during blocklist update'; @@ -60,9 +63,9 @@ export const updateBlock: RequestHandler = async (req, res) => { export const check: RequestHandler = async (req, res) => { try { const { sessionId, jid } = req.params; - const session = getSession(sessionId)!; + const session = Session.get(sessionId)!; - const exists = await jidExists(session, jid); + const exists = await session.jidExists(jid); res.status(200).json({ exists }); } catch (e) { const message = 'An error occured during jid check'; diff --git a/src/controllers/group.ts b/src/controllers/group.ts index 63d35930..c4cea477 100644 --- a/src/controllers/group.ts +++ b/src/controllers/group.ts @@ -1,18 +1,21 @@ +import { serializePrisma } from '@ookamiiixd/baileys-store'; import type { RequestHandler } from 'express'; import { logger, prisma } from '../shared'; -import { getSession } from '../wa'; +import { Session } from '../wa'; import { makePhotoURLHandler } from './misc'; export const list: RequestHandler = async (req, res) => { try { const { sessionId } = req.params; const { cursor = undefined, limit = 25 } = req.query; - const groups = await prisma.contact.findMany({ - cursor: cursor ? { pkId: Number(cursor) } : undefined, - take: Number(limit), - skip: cursor ? 1 : 0, - where: { id: { endsWith: 'g.us' }, sessionId }, - }); + const groups = ( + await prisma.contact.findMany({ + cursor: cursor ? { pkId: Number(cursor) } : undefined, + take: Number(limit), + skip: cursor ? 1 : 0, + where: { id: { endsWith: 'g.us' }, sessionId }, + }) + ).map((m) => serializePrisma(m)); res.status(200).json({ data: groups, @@ -31,8 +34,8 @@ export const list: RequestHandler = async (req, res) => { export const find: RequestHandler = async (req, res) => { try { const { sessionId, jid } = req.params; - const session = getSession(sessionId)!; - const data = await session.groupMetadata(jid); + const session = Session.get(sessionId)!; + const data = await session.socket.groupMetadata(jid); res.status(200).json(data); } catch (e) { const message = 'An error occured during group metadata fetch'; diff --git a/src/controllers/message.ts b/src/controllers/message.ts index d2d71ca4..ef216013 100644 --- a/src/controllers/message.ts +++ b/src/controllers/message.ts @@ -4,7 +4,7 @@ import { serializePrisma } from '@ookamiiixd/baileys-store'; import type { RequestHandler } from 'express'; import { logger, prisma } from '../shared'; import { delay as delayMs } from '../utils'; -import { getSession, jidExists } from '../wa'; +import { Session } from '../wa'; export const list: RequestHandler = async (req, res) => { try { @@ -36,12 +36,12 @@ export const list: RequestHandler = async (req, res) => { export const send: RequestHandler = async (req, res) => { try { const { jid, type = 'number', message, options } = req.body; - const session = getSession(req.params.sessionId)!; + const session = Session.get(req.params.sessionId)!; - const exists = await jidExists(session, jid, type); + const exists = await session.jidExists(jid, type); if (!exists) return res.status(400).json({ error: 'JID does not exists' }); - const result = await session.sendMessage(jid, message, options); + const result = await session.socket.sendMessage(jid, message, options); res.status(200).json(result); } catch (e) { const message = 'An error occured during message send'; @@ -51,7 +51,7 @@ export const send: RequestHandler = async (req, res) => { }; export const sendBulk: RequestHandler = async (req, res) => { - const session = getSession(req.params.sessionId)!; + const session = Session.get(req.params.sessionId)!; const results: { index: number; result: proto.WebMessageInfo | undefined }[] = []; const errors: { index: number; error: string }[] = []; @@ -60,14 +60,14 @@ export const sendBulk: RequestHandler = async (req, res) => { { jid, type = 'number', delay = 1000, message, options }, ] of req.body.entries()) { try { - const exists = await jidExists(session, jid, type); + const exists = await session.jidExists(jid, type); if (!exists) { errors.push({ index, error: 'JID does not exists' }); continue; } if (index > 0) await delayMs(delay); - const result = await session.sendMessage(jid, message, options); + const result = await session.socket.sendMessage(jid, message, options); results.push({ index, result }); } catch (e) { const message = 'An error occured during message send'; @@ -83,7 +83,7 @@ export const sendBulk: RequestHandler = async (req, res) => { export const download: RequestHandler = async (req, res) => { try { - const session = getSession(req.params.sessionId)!; + const session = Session.get(req.params.sessionId)!; const message = req.body as WAMessage; const type = Object.keys(message.message!)[0] as keyof proto.IMessage; const content = message.message![type] as WAGenericMediaMessage; @@ -91,10 +91,10 @@ export const download: RequestHandler = async (req, res) => { message, 'buffer', {}, - { logger, reuploadRequest: session.updateMediaMessage } + { logger, reuploadRequest: session.socket.updateMediaMessage } ); - res.setHeader('Content-Type', content.mimetype!); + if (content.mimetype) res.setHeader('Content-Type', content.mimetype); res.write(buffer); res.end(); } catch (e) { diff --git a/src/controllers/misc.ts b/src/controllers/misc.ts index d77fdc9b..3cebd6ae 100644 --- a/src/controllers/misc.ts +++ b/src/controllers/misc.ts @@ -1,18 +1,18 @@ import type { RequestHandler } from 'express'; import { logger } from '../shared'; -import { getSession, jidExists } from '../wa'; +import { Session } from '../wa'; export const makePhotoURLHandler = (type: 'number' | 'group' = 'number'): RequestHandler => async (req, res) => { try { const { sessionId, jid } = req.params; - const session = getSession(sessionId)!; + const session = Session.get(sessionId)!; - const exists = await jidExists(session, jid, type); + const exists = await session.jidExists(jid, type); if (!exists) return res.status(400).json({ error: 'Jid does not exists' }); - const url = await session.profilePictureUrl(jid, 'image'); + const url = await session.socket.profilePictureUrl(jid, 'image'); res.status(200).json({ url }); } catch (e) { const message = 'An error occured during photo fetch'; diff --git a/src/controllers/session.ts b/src/controllers/session.ts index 9a07b413..e5e4d71f 100644 --- a/src/controllers/session.ts +++ b/src/controllers/session.ts @@ -1,21 +1,17 @@ import type { RequestHandler } from 'express'; -import { - createSession, - deleteSession, - getSession, - getSessionStatus, - listSessions, - sessionExists, -} from '../wa'; +import { Session } from '../wa'; export const list: RequestHandler = (req, res) => { - res.status(200).json(listSessions()); + res.status(200).json(Session.list()); }; export const find: RequestHandler = (req, res) => res.status(200).json({ message: 'Session found' }); export const status: RequestHandler = (req, res) => { + const session = Session.get(req.params.sessionId)!; + res.status(200).json({ status: session.status() }); +}; export const qr: RequestHandler = (req, res) => { const session = Session.get(req.params.sessionId)!; @@ -23,10 +19,10 @@ export const qr: RequestHandler = (req, res) => { }; export const add: RequestHandler = async (req, res) => { - const { sessionId, readIncomingMessages, ...socketConfig } = req.body; + const { sessionId, readIncomingMessages, proxy, webhook, ...socketConfig } = req.body; - if (sessionExists(sessionId)) return res.status(400).json({ error: 'Session already exists' }); - createSession({ sessionId, res, readIncomingMessages, socketConfig }); + if (Session.exists(sessionId)) return res.status(400).json({ error: 'Session already exists' }); + Session.create({ sessionId, res, readIncomingMessages, proxy, webhook, socketConfig }); }; export const addSSE: RequestHandler = async (req, res) => { @@ -37,15 +33,15 @@ export const addSSE: RequestHandler = async (req, res) => { Connection: 'keep-alive', }); - if (sessionExists(sessionId)) { + if (Session.exists(sessionId)) { res.write(`data: ${JSON.stringify({ error: 'Session already exists' })}\n\n`); res.end(); return; } - createSession({ sessionId, res, SSE: true }); + Session.create({ sessionId, res, SSE: true }); }; export const del: RequestHandler = async (req, res) => { - await deleteSession(req.params.sessionId); + await Session.delete(req.params.sessionId); res.status(200).json({ message: 'Session deleted' }); }; diff --git a/src/middlewares/session-validator.ts b/src/middlewares/session-validator.ts index e13707b6..b6356fc8 100644 --- a/src/middlewares/session-validator.ts +++ b/src/middlewares/session-validator.ts @@ -1,8 +1,8 @@ import type { RequestHandler } from 'express'; -import { sessionExists } from '../wa'; +import { Session } from '../wa'; const validate: RequestHandler = (req, res, next) => { - if (!sessionExists(req.params.sessionId)) + if (!Session.exists(req.params.sessionId)) return res.status(404).json({ error: 'Session not found' }); next(); }; diff --git a/src/utils.ts b/src/utils.ts index 98eaa4f5..d2c3ff92 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,12 @@ export function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } + +export function pick>(obj: T, keys: (keyof T)[]): Partial { + return keys.reduce((acc, key) => { + if (key in obj) { + acc[key] = obj[key]; + } + return acc; + }, {} as Partial); +} diff --git a/src/wa.ts b/src/wa.ts index c2fc71f0..937300f2 100644 --- a/src/wa.ts +++ b/src/wa.ts @@ -1,4 +1,4 @@ -import type { ConnectionState, proto, SocketConfig, WASocket } from '@adiwajshing/baileys'; +import type { BaileysEventMap, ConnectionState, proto, SocketConfig } from '@adiwajshing/baileys'; import makeWASocket, { Browsers, DisconnectReason, @@ -7,28 +7,34 @@ import makeWASocket, { } from '@adiwajshing/baileys'; import type { Boom } from '@hapi/boom'; import { initStore, Store, useSession } from '@ookamiiixd/baileys-store'; +import axios from 'axios'; import type { Response } from 'express'; -// import { writeFile } from 'fs/promises'; -// import { join } from 'path'; +import ProxyAgent from 'proxy-agent'; import { toDataURL } from 'qrcode'; import type { WebSocket } from 'ws'; import { logger, prisma } from './shared'; -import { delay } from './utils'; +import { delay, pick } from './utils'; -type Session = WASocket & { - destroy: () => Promise; - store: Store; -}; - -const sessions = new Map(); +const sessions = new Map(); const retries = new Map(); -const SSEQRGenerations = new Map(); +const QRGenerations = new Map(); const RECONNECT_INTERVAL = Number(process.env.RECONNECT_INTERVAL || 0); const MAX_RECONNECT_RETRIES = Number(process.env.MAX_RECONNECT_RETRIES || 5); -const SSE_MAX_QR_GENERATION = Number(process.env.SSE_MAX_QR_GENERATION || 5); +const MAX_QR_GENERATION = Number(process.env.MAX_QR_GENERATION || 5); const SESSION_CONFIG_ID = 'session-config'; +function shouldReconnect(sessionId: string) { + let attempts = retries.get(sessionId) ?? 1; + + if (attempts < MAX_RECONNECT_RETRIES) { + attempts += 1; + retries.set(sessionId, attempts); + return true; + } + return false; +} + export async function init() { initStore({ prisma, logger }); const sessions = await prisma.session.findMany({ @@ -37,43 +43,134 @@ export async function init() { }); for (const { sessionId, data } of sessions) { - const { readIncomingMessages, ...socketConfig } = JSON.parse(data); - createSession({ sessionId, readIncomingMessages, socketConfig }); + const { readIncomingMessages, proxy, ...socketConfig } = JSON.parse(data); + Session.create({ sessionId, readIncomingMessages, proxy, socketConfig }); } } -function shouldReconnect(sessionId: string) { - let attempts = retries.get(sessionId) ?? 0; - - if (attempts < MAX_RECONNECT_RETRIES) { - attempts += 1; - retries.set(sessionId, attempts); - return true; - } - return false; -} - -type createSessionOptions = { +type SessionOptions = { sessionId: string; res?: Response; SSE?: boolean; readIncomingMessages?: boolean; + proxy?: string; + webhook?: { + url: string[]; + events: 'all' | (keyof BaileysEventMap)[]; + }; socketConfig?: SocketConfig; }; -export async function createSession(options: createSessionOptions) { - const { sessionId, res, SSE = false, readIncomingMessages = false, socketConfig } = options; - const configID = `${SESSION_CONFIG_ID}-${sessionId}`; - let connectionState: Partial = { connection: 'close' }; +export class Session { + private connectionState: Partial = { connection: 'close' }; + private lastGeneratedQR: string | null = null; + public readonly socket: ReturnType; + + constructor( + private readonly sessionState: Awaited>, + private readonly options: SessionOptions + ) { + const { sessionId, socketConfig, proxy } = options; + this.socket = makeWASocket({ + printQRInTerminal: true, + browser: Browsers.ubuntu('Chrome'), + generateHighQualityLinkPreview: true, + ...socketConfig, + logger, + agent: proxy ? new ProxyAgent(proxy) : undefined, + auth: { + creds: sessionState.state.creds, + keys: makeCacheableSignalKeyStore(sessionState.state.keys, logger), + }, + shouldIgnoreJid: (jid) => isJidBroadcast(jid), + getMessage: async (key) => { + const data = await prisma.message.findFirst({ + where: { remoteJid: key.remoteJid!, id: key.id!, sessionId }, + }); + return (data?.message || undefined) as proto.IMessage | undefined; + }, + }); + + this.bindEvents(); + sessions.set(sessionId, { ...this, store: new Store(sessionId, this.socket.ev) }); + } + + public static async create(options: SessionOptions) { + options = { readIncomingMessages: false, SSE: false, ...options }; + const { sessionId, readIncomingMessages, socketConfig } = options; + const configID = `${SESSION_CONFIG_ID}-${sessionId}`; + const data = JSON.stringify({ + readIncomingMessages: readIncomingMessages, + ...socketConfig, + }); + + const [sessionState] = await Promise.all([ + useSession(sessionId), + prisma.session.upsert({ + create: { + id: configID, + sessionId, + data, + }, + update: { data }, + where: { sessionId_id: { id: configID, sessionId } }, + }), + ]); + return new Session(sessionState, options); + } + + public static list() { + return Array.from(sessions.entries()).map(([id, session]) => ({ + id, + status: session.status(), + })); + } + + public static get(sessionId: string) { + return sessions.get(sessionId) ?? null; + } + + public static async delete(sessionId: string) { + await Session.get(sessionId)?.destroy(); + } + + public static exists(sessionId: string) { + return sessions.has(sessionId); + } - const destroy = async (logout = true) => { + public QR() { + return this.lastGeneratedQR; + } + + public status() { + const state = ['CONNECTING', 'CONNECTED', 'DISCONNECTING', 'DISCONNECTED']; + let status = state[(this.socket.ws as WebSocket).readyState]; + status = this.socket.user ? 'AUTHENTICATED' : status; + return status; + } + + public async jidExists(jid: string, type: 'group' | 'number' = 'number') { + try { + if (type === 'number') { + const [result] = await this.socket.onWhatsApp(jid); + return !!result?.exists; + } + + const groupMetadata = await this.socket.groupMetadata(jid); + return !!groupMetadata.id; + } catch (e) { + return Promise.reject(e); + } + } + + public async destroy(logout = true) { + const { sessionId } = this.options; try { await Promise.all([ - logout && socket.logout(), + logout && this.socket.logout(), prisma.chat.deleteMany({ where: { sessionId } }), prisma.contact.deleteMany({ where: { sessionId } }), prisma.message.deleteMany({ where: { sessionId } }), - prisma.groupMetadata.deleteMany({ where: { sessionId } }), prisma.session.deleteMany({ where: { sessionId } }), ]); } catch (e) { @@ -81,176 +178,110 @@ export async function createSession(options: createSessionOptions) { } finally { sessions.delete(sessionId); } - }; + } - const handleConnectionClose = () => { - const code = (connectionState.lastDisconnect?.error as Boom)?.output?.statusCode; - const restartRequired = code === DisconnectReason.restartRequired; - const doNotReconnect = !shouldReconnect(sessionId); + private bindEvents() { + const { sessionId, readIncomingMessages, webhook } = this.options; + this.socket.ev.on('creds.update', this.sessionState.saveCreds); + this.socket.ev.on('connection.update', (update) => { + this.connectionState = update; + const { connection } = update; + + if (connection === 'open') { + retries.delete(sessionId); + QRGenerations.delete(sessionId); + } else if (connection === 'close') this.handleConnectionClose(); + this.handleConnectionUpdate(); + }); - if (code === DisconnectReason.loggedOut || doNotReconnect) { - if (res) { - !SSE && !res.headersSent && res.status(500).json({ error: 'Unable to create session' }); - res.end(); - } - destroy(doNotReconnect); - return; - } + if (readIncomingMessages) { + this.socket.ev.on('messages.upsert', async (m) => { + const message = m.messages[0]; + if (message.key.fromMe || m.type !== 'notify') return; - if (!restartRequired) { - logger.info({ attempts: retries.get(sessionId) ?? 1, sessionId }, 'Reconnecting...'); + await delay(1000); + await this.socket.readMessages([message.key]); + }); } - setTimeout(() => createSession(options), restartRequired ? 0 : RECONNECT_INTERVAL); - }; - const handleNormalConnectionUpdate = async () => { - if (connectionState.qr?.length) { - if (res && !res.headersSent) { + if (webhook) { + this.socket.ev.process(async (events) => { + const data = webhook.events === 'all' ? events : pick(events, webhook.events); try { - const qr = await toDataURL(connectionState.qr); - res.status(200).json({ qr }); - return; + await Promise.any( + webhook.url.map((url) => + axios.post(url, JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + }, + timeout: 5000, + }) + ) + ); } catch (e) { - logger.error(e, 'An error occured during QR generation'); - res.status(500).json({ error: 'Unable to generate QR' }); + logger.error(e, 'An error occured during webhook request'); } - } - destroy(); + }); } - }; + } + + private async handleConnectionUpdate() { + const { sessionId, res, SSE } = this.options; + const { qr } = this.connectionState; + let generatedQR: string | null = null; + const currentQRGenerations = QRGenerations.get(sessionId) ?? 1; - const handleSSEConnectionUpdate = async () => { - let qr: string | undefined = undefined; - if (connectionState.qr?.length) { + if (qr) { try { - qr = await toDataURL(connectionState.qr); + generatedQR = await toDataURL(qr); + this.lastGeneratedQR = generatedQR; + QRGenerations.set(sessionId, currentQRGenerations + 1); } catch (e) { logger.error(e, 'An error occured during QR generation'); } } - const currentGenerations = SSEQRGenerations.get(sessionId) ?? 0; - if (!res || res.writableEnded || (qr && currentGenerations >= SSE_MAX_QR_GENERATION)) { - res && !res.writableEnded && res.end(); - destroy(); - return; - } - - const data = { ...connectionState, qr }; - if (qr) SSEQRGenerations.set(sessionId, currentGenerations + 1); - res.write(`data: ${JSON.stringify(data)}\n\n`); - }; - - const handleConnectionUpdate = SSE ? handleSSEConnectionUpdate : handleNormalConnectionUpdate; - const { state, saveCreds } = await useSession(sessionId); - const socket = makeWASocket({ - printQRInTerminal: true, - browser: Browsers.ubuntu('Chrome'), - generateHighQualityLinkPreview: true, - ...socketConfig, - auth: { - creds: state.creds, - keys: makeCacheableSignalKeyStore(state.keys, logger), - }, - logger, - shouldIgnoreJid: (jid) => isJidBroadcast(jid), - getMessage: async (key) => { - const data = await prisma.message.findFirst({ - where: { remoteJid: key.remoteJid!, id: key.id!, sessionId }, - }); - return (data?.message || undefined) as proto.IMessage | undefined; - }, - }); - - const store = new Store(sessionId, socket.ev); - sessions.set(sessionId, { ...socket, destroy, store }); - - socket.ev.on('creds.update', saveCreds); - socket.ev.on('connection.update', (update) => { - connectionState = update; - const { connection } = update; - - if (connection === 'open') { - retries.delete(sessionId); - SSEQRGenerations.delete(sessionId); + const limitReached = currentQRGenerations >= MAX_QR_GENERATION; + if (limitReached) this.destroy(); + + if (!res || res.writableEnded) return; + if (SSE) { + res.write( + `data: ${JSON.stringify( + limitReached + ? { error: 'QR max generation attempts reached' } + : { ...this.connectionState, qr: generatedQR } + )}\n\n` + ); + if (limitReached) res.end(); + } else { + if (limitReached) res.status(500).json({ error: 'QR max generation attempts reached' }).end(); + else if (!limitReached && qr && generatedQR) res.status(200).json({ qr: generatedQR }); + else if (!limitReached && qr && !generatedQR) + res.status(500).json({ error: 'Unable to generate QR' }); } - if (connection === 'close') handleConnectionClose(); - handleConnectionUpdate(); - }); - - if (readIncomingMessages) { - socket.ev.on('messages.upsert', async (m) => { - const message = m.messages[0]; - if (message.key.fromMe || m.type !== 'notify') return; - - await delay(1000); - await socket.readMessages([message.key]); - }); } - // Debug events - // socket.ev.on('messaging-history.set', (data) => dump('messaging-history.set', data)); - // socket.ev.on('chats.upsert', (data) => dump('chats.upsert', data)); - // socket.ev.on('contacts.update', (data) => dump('contacts.update', data)); - // socket.ev.on('groups.upsert', (data) => dump('groups.upsert', data)); - - await prisma.session.upsert({ - create: { - id: configID, - sessionId, - data: JSON.stringify({ readIncomingMessages, ...socketConfig }), - }, - update: {}, - where: { sessionId_id: { id: configID, sessionId } }, - }); -} - -export function getSessionStatus(session: Session) { - const state = ['CONNECTING', 'CONNECTED', 'DISCONNECTING', 'DISCONNECTED']; - let status = state[(session.ws as WebSocket).readyState]; - status = session.user ? 'AUTHENTICATED' : status; - return status; -} - -export function listSessions() { - return Array.from(sessions.entries()).map(([id, session]) => ({ - id, - status: getSessionStatus(session), - })); -} - -export function getSession(sessionId: string) { - return sessions.get(sessionId); -} - -export async function deleteSession(sessionId: string) { - sessions.get(sessionId)?.destroy(); -} - -export function sessionExists(sessionId: string) { - return sessions.has(sessionId); -} + private handleConnectionClose() { + const { sessionId, res, SSE } = this.options; + const code = (this.connectionState.lastDisconnect?.error as Boom)?.output?.statusCode; + const restartRequired = code === DisconnectReason.restartRequired; + const doNotReconnect = !shouldReconnect(sessionId); -export async function jidExists( - session: Session, - jid: string, - type: 'group' | 'number' = 'number' -) { - try { - if (type === 'number') { - const [result] = await session.onWhatsApp(jid); - return !!result?.exists; + if (code === DisconnectReason.loggedOut || doNotReconnect) { + if (res && res.writableEnded) { + !SSE && res.status(500).json({ error: 'Unable to create session' }); + res.end(); + } + return this.destroy(doNotReconnect); } - const groupMeta = await session.groupMetadata(jid); - return !!groupMeta.id; - } catch (e) { - return Promise.reject(e); + if (!restartRequired) { + logger.info( + { attempts: retries.get(sessionId) ?? 1, sessionId: sessionId }, + 'Reconnecting...' + ); + } + setTimeout(() => Session.create(this.options), restartRequired ? 0 : RECONNECT_INTERVAL); } } - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -// export async function dump(fileName: string, data: any) { -// const path = join(__dirname, '..', 'debug', `${fileName}.json`); -// await writeFile(path, JSON.stringify(data, null, 2)); -// } diff --git a/yarn.lock b/yarn.lock index 99a56454..41b440d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -121,10 +121,9 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@ookamiiixd/baileys-store@^1.0.0-beta.0": - version "1.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@ookamiiixd/baileys-store/-/baileys-store-1.0.0-beta.0.tgz#1a2a5b2e0edd75c00d68a60c72bdf856dbc881e1" - integrity sha512-djOZUZLRtjEcoy8qt/wjuT7WNLh54CfZ0v1/f1g298kRvnDKLxSKuu1I/6hdTntPMyCoir95xQA/HTBbyCXheA== +"@ookamiiixd/baileys-store@file:debug/ookamiiixd-baileys-store-v1.0.0-beta.1.tgz": + version "1.0.0-beta.1" + resolved "file:debug/ookamiiixd-baileys-store-v1.0.0-beta.1.tgz#87efc8a129a1674c42137296d8d5741f81cfbafe" dependencies: tiny-invariant "^1.3.1" @@ -203,6 +202,11 @@ resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" @@ -429,7 +433,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^8.1.1: +acorn-walk@^8.1.1, acorn-walk@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -439,6 +443,18 @@ acorn@^8.4.1, acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== +acorn@^8.7.0: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +agent-base@6, agent-base@^6.0.0, agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -481,6 +497,18 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +ast-types@^0.13.2: + version "0.13.4" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" + integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== + dependencies: + tslib "^2.0.1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + atomic-sleep@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" @@ -493,6 +521,15 @@ axios@^0.24.0: dependencies: follow-redirects "^1.14.4" +axios@^1.3.3: + version "1.3.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024" + integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -670,6 +707,13 @@ color@^4.2.3: color-convert "^2.0.1" color-string "^1.9.0" +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -697,6 +741,11 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + cors@^2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" @@ -747,6 +796,11 @@ curve25519-js@^0.0.4: resolved "https://registry.yarnpkg.com/curve25519-js/-/curve25519-js-0.0.4.tgz#e6ad967e8cd284590d657bbfc90d8b50e49ba060" integrity sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w== +data-uri-to-buffer@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" + integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -754,7 +808,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -778,11 +832,26 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deep-is@^0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +degenerator@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-3.0.2.tgz#6a61fcc42a702d6e50ff6023fe17bff435f68235" + integrity sha512-c0mef3SNQo56t6urUU6tdQAs+ThoD0o9B9MJ8HEt7NQcGEILCRFqQb7ZbP9JAv+QF1Ky5plydhMR/IrqWDm+TQ== + dependencies: + ast-types "^0.13.2" + escodegen "^1.8.1" + esprima "^4.0.0" + vm2 "^3.9.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -909,6 +978,18 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escodegen@^1.8.1: + version "1.14.3" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + eslint-config-prettier@^8.5.0: version "8.5.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" @@ -1001,6 +1082,11 @@ espree@^9.4.0: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.3.0" +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + esquery@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" @@ -1015,7 +1101,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1: +estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -1116,7 +1202,7 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@^2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== @@ -1149,6 +1235,11 @@ file-type@^16.5.4: strtok3 "^6.2.4" token-types "^4.1.1" +file-uri-to-path@2: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz#7b415aeba227d575851e0a5b0c640d7656403fba" + integrity sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg== + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -1198,11 +1289,20 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.14.4: +follow-redirects@^1.14.4, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -1218,11 +1318,28 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +ftp@^0.3.10: + version "0.3.10" + resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" + integrity sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ== + dependencies: + readable-stream "1.1.x" + xregexp "2.0.0" + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -1247,6 +1364,18 @@ get-intrinsic@^1.0.2: has "^1.0.3" has-symbols "^1.0.3" +get-uri@3: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-3.0.2.tgz#f0ef1356faabc70e1f9404fa3b66b2ba9bfc725c" + integrity sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg== + dependencies: + "@tootallnate/once" "1" + data-uri-to-buffer "3" + debug "4" + file-uri-to-path "2" + fs-extra "^8.1.0" + ftp "^0.3.10" + github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" @@ -1297,6 +1426,11 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + grapheme-splitter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" @@ -1340,6 +1474,23 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +http-proxy-agent@^4.0.0, http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +https-proxy-agent@5, https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -1378,7 +1529,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1388,6 +1539,16 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +ip@^1.1.5: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" + integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg== + +ip@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" + integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -1425,6 +1586,11 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -1452,6 +1618,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -1460,6 +1633,14 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + "libsignal@git+https://github.com/adiwajshing/libsignal-node": version "2.0.1" resolved "git+https://github.com/adiwajshing/libsignal-node#11dbd962ea108187c79a7c46fe4d6f790e23da97" @@ -1506,6 +1687,13 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -1556,7 +1744,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -1638,6 +1826,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +netmask@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" + integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== + node-abi@^3.3.0: version "3.30.0" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.30.0.tgz#d84687ad5d24ca81cdfa912a36f2c5c19b137359" @@ -1705,6 +1898,18 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -1750,6 +1955,30 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pac-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz#b718f76475a6a5415c2efbe256c1c971c84f635e" + integrity sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + get-uri "3" + http-proxy-agent "^4.0.1" + https-proxy-agent "5" + pac-resolver "^5.0.0" + raw-body "^2.2.0" + socks-proxy-agent "5" + +pac-resolver@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-5.0.1.tgz#c91efa3a9af9f669104fa2f51102839d01cde8e7" + integrity sha512-cy7u00ko2KVgBAjuhevqpPeHIkCIqPe1v24cydhWjmeuzaBfmUWFCZJ1iAh5TuVzVZoUzXIW7K8sMYOZ84uZ9Q== + dependencies: + degenerator "^3.0.2" + ip "^1.1.5" + netmask "^2.0.2" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -1900,6 +2129,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + prettier-plugin-organize-imports@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.1.tgz#7e0e0a18457e0166e740daaff1aed1c08069fcb9" @@ -1978,6 +2212,25 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-5.0.0.tgz#d31405c10d6e8431fde96cba7a0c027ce01d633b" + integrity sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g== + dependencies: + agent-base "^6.0.0" + debug "4" + http-proxy-agent "^4.0.0" + https-proxy-agent "^5.0.0" + lru-cache "^5.1.1" + pac-proxy-agent "^5.0.0" + proxy-from-env "^1.0.0" + socks-proxy-agent "^5.0.0" + +proxy-from-env@^1.0.0, proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -2048,6 +2301,16 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@^2.2.0: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -2058,6 +2321,16 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -2255,6 +2528,28 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@5, socks-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz#032fb583048a29ebffec2e6a73fca0761f48177e" + integrity sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ== + dependencies: + agent-base "^6.0.2" + debug "4" + socks "^2.3.3" + +socks@^2.3.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" + integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== + dependencies: + ip "^2.0.0" + smart-buffer "^4.2.0" + sonic-boom@^2.2.1: version "2.8.0" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-2.8.0.tgz#c1def62a77425090e6ad7516aad8eb402e047611" @@ -2269,6 +2564,11 @@ sonic-boom@^3.1.0: dependencies: atomic-sleep "^1.0.0" +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + split2@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" @@ -2300,6 +2600,11 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -2426,6 +2731,11 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + tslib@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" @@ -2452,6 +2762,13 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -2470,6 +2787,11 @@ typescript@^4.9.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -2515,6 +2837,14 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +vm2@^3.9.8: + version "3.9.14" + resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.14.tgz#964042b474cf1e6e4f475a39144773cdb9deb734" + integrity sha512-HgvPHYHeQy8+QhzlFryvSteA4uQLBCOub02mgqdR+0bN/akRZ48TGB1v0aCv7ksyc0HXx16AZtMHKS38alc6TA== + dependencies: + acorn "^8.7.0" + acorn-walk "^8.2.0" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -2540,7 +2870,7 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -word-wrap@^1.2.3: +word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== @@ -2564,11 +2894,21 @@ ws@^8.0.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== +xregexp@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" + integrity sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA== + y18n@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" From fa286613ad7562ab6470d79f036a38c0ece1c166 Mon Sep 17 00:00:00 2001 From: Royhan <60024250+ookamiiixd@users.noreply.github.com> Date: Sat, 11 Mar 2023 03:08:46 +0700 Subject: [PATCH 3/4] refactor to improve readibility --- package.json | 7 +++- prisma/schema.prisma | 95 +++++++++++++++++--------------------------- 2 files changed, 41 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index 9c73ea19..ff1aeedb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "baileys-api", "description": "Simple RESTful WhatsApp API", - "version": "1.0.0-beta.0", + "version": "1.0.0-beta.1", "private": true, "main": "dist/index.js", "repository": "github:ookamiiixd/baileys-api", @@ -15,19 +15,22 @@ "start": "node .", "build": "tsc", "lint": "eslint .", + "typecheck": "tsc --noEmit", "format": "prettier . --write" }, "dependencies": { "@adiwajshing/baileys": "^5.0.0", "@hapi/boom": "^10.0.0", - "@ookamiiixd/baileys-store": "^1.0.0-beta.0", + "@ookamiiixd/baileys-store": "file:debug/ookamiiixd-baileys-store-v1.0.0-beta.1.tgz", "@prisma/client": "^4.7.1", + "axios": "^1.3.3", "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.2", "express-validator": "^6.14.2", "link-preview-js": "^3.0.0", "pino": "^8.7.0", + "proxy-agent": "^5.0.0", "qrcode": "^1.5.1", "qrcode-terminal": "^0.12.0", "sharp": "^0.30.5" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 94fd6e25..cbcd3cb8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,38 +11,38 @@ datasource db { } model Chat { - pkId Int @id @default(autoincrement()) - sessionId String @db.VarChar(128) + pkId BigInt @id @default(autoincrement()) + sessionId String archived Boolean? contactPrimaryIdentityKey Bytes? conversationTimestamp BigInt? createdAt BigInt? - createdBy String? @db.VarChar(128) - description String? @db.VarChar(255) + createdBy String? + description String? disappearingMode Json? - displayName String? @db.VarChar(128) + displayName String? endOfHistoryTransfer Boolean? endOfHistoryTransferType Int? ephemeralExpiration Int? ephemeralSettingTimestamp BigInt? - id String @db.VarChar(128) + id String isDefaultSubgroup Boolean? isParentGroup Boolean? lastMsgTimestamp BigInt? - lidJid String? @db.VarChar(128) + lidJid String? markedAsUnread Boolean? mediaVisibility Int? messages Json? muteEndTime BigInt? - name String? @db.VarChar(128) - newJid String? @db.VarChar(128) + name String? + newJid String? notSpam Boolean? - oldJid String? @db.VarChar(128) - pHash String? @db.VarChar(128) - parentGroupId String? @db.VarChar(128) + oldJid String? + pHash String? + parentGroupId String? participant Json? pinned Int? - pnJid String? @db.VarChar(128) + pnJid String? pnhDuplicateLidThread Boolean? readOnly Boolean? shareOwnPn Boolean? @@ -57,53 +57,30 @@ model Chat { wallpaper Json? lastMessageRecvTimestamp Int? - @@unique([sessionId, id], map: "unique_id_per_session_id") + @@unique([sessionId, id], map: "unique_id_per_session_id_chat") @@index([sessionId]) } model Contact { - pkId Int @id @default(autoincrement()) - sessionId String @db.VarChar(128) - id String @db.VarChar(128) - name String? @db.VarChar(128) - notify String? @db.VarChar(128) - verifiedName String? @db.VarChar(128) - imgUrl String? @db.VarChar(255) - status String? @db.VarChar(128) + pkId BigInt @id @default(autoincrement()) + sessionId String + id String + name String? + notify String? + verifiedName String? + imgUrl String? + status String? - @@unique([sessionId, id], map: "unique_id_per_session_id") - @@index([sessionId]) -} - -model GroupMetadata { - pkId Int @id @default(autoincrement()) - sessionId String @db.VarChar(128) - id String @db.VarChar(128) - owner String? @db.VarChar(128) - subject String @db.VarChar(128) - subjectOwner String? @db.VarChar(128) - subjectTime Int? - creation Int? - desc String? @db.VarChar(255) - descOwner String? @db.VarChar(128) - descId String? @db.VarChar(128) - restrict Boolean? - announce Boolean? - size Int? - participants Json - ephemeralDuration Int? - inviteCode String? @db.VarChar(255) - - @@unique([sessionId, id], map: "unique_id_per_session_id") + @@unique([sessionId, id], map: "unique_id_per_session_id_contact") @@index([sessionId]) } model Message { - pkId Int @id @default(autoincrement()) - sessionId String @db.VarChar(128) - remoteJid String @db.VarChar(128) - id String @db.VarChar(128) - agentId String? @db.VarChar(128) + pkId BigInt @id @default(autoincrement()) + sessionId String + remoteJid String + id String + agentId String? bizPrivacyStatus Int? broadcast Boolean? clearMedia Boolean? @@ -127,13 +104,13 @@ model Message { messageStubType Int? messageTimestamp BigInt? multicast Boolean? - originalSelfAuthorUserJidString String? @db.VarChar(128) - participant String? @db.VarChar(128) + originalSelfAuthorUserJidString String? + participant String? paymentInfo Json? photoChange Json? pollAdditionalMetadata Json? pollUpdates Json? - pushName String? @db.VarChar(128) + pushName String? quotedPaymentInfo Json? quotedStickerData Json? reactions Json? @@ -145,18 +122,18 @@ model Message { urlNumber Boolean? urlText Boolean? userReceipt Json? - verifiedBizName String? @db.VarChar(128) + verifiedBizName String? @@unique([sessionId, remoteJid, id], map: "unique_message_key_per_session_id") @@index([sessionId]) } model Session { - pkId Int @id @default(autoincrement()) - sessionId String @db.VarChar(128) - id String @db.VarChar(255) + pkId BigInt @id @default(autoincrement()) + sessionId String + id String data String @db.Text - @@unique([sessionId, id], map: "unique_id_per_session_id") + @@unique([sessionId, id], map: "unique_id_per_session_id_session") @@index([sessionId]) } From be744100932438333bf75d03e03f4760ffacabf6 Mon Sep 17 00:00:00 2001 From: Royhan <60024250+ookamiiixd@users.noreply.github.com> Date: Sun, 12 Mar 2023 01:30:47 +0700 Subject: [PATCH 4/4] improve and add more routes --- package.json | 2 +- src/controllers/contact.ts | 2 +- src/controllers/group.ts | 77 ++++++++++++++++++++++++++++++++++++++ src/controllers/message.ts | 2 +- src/controllers/session.ts | 26 ++++++++++++- src/routes/chats.ts | 8 ++-- src/routes/contacts.ts | 6 +-- src/routes/groups.ts | 36 ++++++++++++++++-- src/routes/messages.ts | 13 +++++-- src/routes/sessions.ts | 31 ++++++++++++++- src/wa.ts | 56 +++++++++++++++------------ yarn.lock | 2 +- 12 files changed, 216 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index ff1aeedb..bb4ce228 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "dependencies": { "@adiwajshing/baileys": "^5.0.0", "@hapi/boom": "^10.0.0", - "@ookamiiixd/baileys-store": "file:debug/ookamiiixd-baileys-store-v1.0.0-beta.1.tgz", "@prisma/client": "^4.7.1", + "@ookamiiixd/baileys-store": "file:debug/ookamiiixd-baileys-store-v1.0.0-beta.1.tgz", "axios": "^1.3.3", "cors": "^2.8.5", "dotenv": "^16.0.3", diff --git a/src/controllers/contact.ts b/src/controllers/contact.ts index 89e280a9..5cfaae8d 100644 --- a/src/controllers/contact.ts +++ b/src/controllers/contact.ts @@ -46,7 +46,7 @@ export const listBlocked: RequestHandler = async (req, res) => { export const updateBlock: RequestHandler = async (req, res) => { try { const session = Session.get(req.params.sessionId)!; - const { jid, action = 'block' } = req.body; + const { jid, action } = req.body; const exists = await session.jidExists(jid); if (!exists) return res.status(400).json({ error: 'Jid does not exists' }); diff --git a/src/controllers/group.ts b/src/controllers/group.ts index c4cea477..bea2790f 100644 --- a/src/controllers/group.ts +++ b/src/controllers/group.ts @@ -44,4 +44,81 @@ export const find: RequestHandler = async (req, res) => { } }; +export const create: RequestHandler = async (req, res) => { + try { + const { sessionId } = req.params; + const { name, participants } = req.body; + const session = Session.get(sessionId)!; + + await session.socket.groupCreate(name, participants); + res.status(200).json({ message: 'Group created' }); + } catch (e) { + const message = 'An error occured during group create'; + logger.error(e, message); + res.status(500).json({ error: message }); + } +}; + +export const leave: RequestHandler = async (req, res) => { + try { + const { sessionId, jid } = req.params; + const session = Session.get(sessionId)!; + + await session.socket.groupLeave(jid); + res.status(200).json({ message: 'Group left' }); + } catch (e) { + const message = 'An error occured during group leave'; + logger.error(e, message); + res.status(500).json({ error: message }); + } +}; + +export const update: RequestHandler = async (req, res) => { + try { + const { sessionId, jid } = req.params; + const { name, description, mode, profilePicture } = req.body; + const session = Session.get(sessionId)!; + + if (name) await session.socket.groupUpdateSubject(jid, name); + if (description) await session.socket.groupUpdateDescription(jid, description); + if (mode) await session.socket.groupSettingUpdate(jid, mode); + if (profilePicture) await session.socket.updateProfilePicture(jid, { url: profilePicture }); + res.status(200).json({ message: 'Group updated' }); + } catch (e) { + const message = 'An error occured during group update'; + logger.error(e, message); + res.status(500).json({ error: message }); + } +}; + +export const updateParticipants: RequestHandler = async (req, res) => { + try { + const { sessionId, jid } = req.params; + const { participants, action } = req.body; + const session = Session.get(sessionId)!; + + await session.socket.groupParticipantsUpdate(jid, participants, action); + res.status(200).json({ message: 'Group participants updated' }); + } catch (e) { + const message = 'An error occured during group participants update'; + logger.error(e, message); + res.status(500).json({ error: message }); + } +}; + +export const inviteCode: RequestHandler = async (req, res) => { + try { + const { sessionId, jid } = req.params; + const { revoke } = req.query; + const session = Session.get(sessionId)!; + + const code = await session.socket[revoke ? 'groupRevokeInvite' : 'groupInviteCode'](jid); + res.status(200).json({ code }); + } catch (e) { + const message = 'An error occured during group invite code fetch'; + logger.error(e, message); + res.status(500).json({ error: message }); + } +}; + export const photo = makePhotoURLHandler('group'); diff --git a/src/controllers/message.ts b/src/controllers/message.ts index ef216013..76c63907 100644 --- a/src/controllers/message.ts +++ b/src/controllers/message.ts @@ -66,7 +66,7 @@ export const sendBulk: RequestHandler = async (req, res) => { continue; } - if (index > 0) await delayMs(delay); + if (index > 0) await delayMs(Number(delay)); const result = await session.socket.sendMessage(jid, message, options); results.push({ index, result }); } catch (e) { diff --git a/src/controllers/session.ts b/src/controllers/session.ts index e5e4d71f..7773fd2d 100644 --- a/src/controllers/session.ts +++ b/src/controllers/session.ts @@ -1,5 +1,8 @@ +import { DisconnectReason } from '@adiwajshing/baileys'; +import { Boom } from '@hapi/boom'; import type { RequestHandler } from 'express'; -import { Session } from '../wa'; +import { logger, prisma } from '../shared'; +import { Session, SESSION_CONFIG_ID } from '../wa'; export const list: RequestHandler = (req, res) => { res.status(200).json(Session.list()); @@ -25,6 +28,27 @@ export const add: RequestHandler = async (req, res) => { Session.create({ sessionId, res, readIncomingMessages, proxy, webhook, socketConfig }); }; +export const update: RequestHandler = async (req, res) => { + try { + const { sessionId } = req.params; + const { readIncomingMessages, proxy, webhook, ...socketConfig } = req.body; + const session = Session.get(sessionId)!; + + await prisma.session.update({ + data: { data: JSON.stringify({ readIncomingMessages, proxy, webhook, ...socketConfig }) }, + where: { sessionId_id: { id: `${SESSION_CONFIG_ID}-${sessionId}`, sessionId } }, + }); + session.socket.end( + new Boom('Restarting session', { statusCode: DisconnectReason.restartRequired }) + ); + res.status(200).json({ message: 'Session updated' }); + } catch (e) { + const message = 'An error occured during session update'; + logger.error(e, message); + res.status(500).json({ error: message }); + } +}; + export const addSSE: RequestHandler = async (req, res) => { const { sessionId } = req.params; res.writeHead(200, { diff --git a/src/routes/chats.ts b/src/routes/chats.ts index e41c0a47..b86fe7f6 100644 --- a/src/routes/chats.ts +++ b/src/routes/chats.ts @@ -6,15 +6,15 @@ import requestValidator from '../middlewares/request-validator'; const router = Router({ mergeParams: true }); router.get( '/', - query('cursor').isNumeric().optional(), - query('limit').isNumeric().optional(), + query('cursor').isInt().optional(), + query('limit').isInt().optional(), requestValidator, controller.list ); router.get( '/:jid', - query('cursor').isNumeric().optional(), - query('limit').isNumeric().optional(), + query('cursor').isInt().optional(), + query('limit').isInt().optional(), requestValidator, controller.find ); diff --git a/src/routes/contacts.ts b/src/routes/contacts.ts index cf01dd3a..42407796 100644 --- a/src/routes/contacts.ts +++ b/src/routes/contacts.ts @@ -7,8 +7,8 @@ import sessionValidator from '../middlewares/session-validator'; const router = Router({ mergeParams: true }); router.get( '/', - query('cursor').isNumeric().optional(), - query('limit').isNumeric().optional(), + query('cursor').isInt().optional(), + query('limit').isInt().optional(), requestValidator, controller.list ); @@ -16,7 +16,7 @@ router.get('/blocklist', sessionValidator, controller.listBlocked); router.post( '/blocklist/update', body('jid').isString().notEmpty(), - body('action').isString().isIn(['block', 'unblock']).optional(), + body('action').isIn(['block', 'unblock']), requestValidator, sessionValidator, controller.updateBlock diff --git a/src/routes/groups.ts b/src/routes/groups.ts index 8c0f037d..bebfdb7c 100644 --- a/src/routes/groups.ts +++ b/src/routes/groups.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { query } from 'express-validator'; +import { body, query } from 'express-validator'; import * as controller from '../controllers/group'; import requestValidator from '../middlewares/request-validator'; import sessionValidator from '../middlewares/session-validator'; @@ -7,12 +7,42 @@ import sessionValidator from '../middlewares/session-validator'; const router = Router({ mergeParams: true }); router.get( '/', - query('cursor').isNumeric().optional(), - query('limit').isNumeric().optional(), + query('cursor').isInt().optional(), + query('limit').isInt().optional(), requestValidator, controller.list ); router.get('/:jid', sessionValidator, controller.find); router.get('/:jid/photo', sessionValidator, controller.photo); +router.post( + '/', + body('name').isString().notEmpty(), + body('participants').isArray({ min: 1 }), + body('participants.*').isString().notEmpty(), + requestValidator, + sessionValidator, + controller.create +); +router.post( + '/:jid', + body('name').isString().notEmpty().optional(), + body('description').isString().notEmpty().optional(), + body('mode').isIn(['announcement', 'not_announcement', 'unlocked', 'locked']).optional(), + body('profilePicture').isString().notEmpty().optional(), + requestValidator, + sessionValidator, + controller.update +); +router.delete('/:jid', sessionValidator, controller.leave); +router.post( + '/:jid/participants', + body('participants').isArray({ min: 1 }), + body('participants.*').isString().notEmpty(), + body('action').isIn(['add', 'demote', 'promote', 'remove']), + requestValidator, + sessionValidator, + controller.updateParticipants +); +router.get('/:jid/invite-code', sessionValidator, controller.inviteCode); export default router; diff --git a/src/routes/messages.ts b/src/routes/messages.ts index 29ffc526..3a28814f 100644 --- a/src/routes/messages.ts +++ b/src/routes/messages.ts @@ -7,15 +7,15 @@ import sessionValidator from '../middlewares/session-validator'; const router = Router({ mergeParams: true }); router.get( '/', - query('cursor').isNumeric().optional(), - query('limit').isNumeric().optional(), + query('cursor').isInt().optional(), + query('limit').isInt().optional(), requestValidator, controller.list ); router.post( '/send', body('jid').isString().notEmpty(), - body('type').isString().isIn(['group', 'number']).optional(), + body('type').isIn(['group', 'number']).optional(), body('message').isObject().notEmpty(), body('options').isObject().optional(), requestValidator, @@ -24,7 +24,12 @@ router.post( ); router.post( '/send/bulk', - body().isArray().notEmpty(), + body().isArray({ min: 1 }), + body('*.jid').isString().notEmpty(), + body('*.type').isIn(['group', 'number']).optional(), + body('*.message').isObject().notEmpty(), + body('*.options').isObject().optional(), + body('*.delay').isInt().optional(), requestValidator, sessionValidator, controller.sendBulk diff --git a/src/routes/sessions.ts b/src/routes/sessions.ts index ec074624..99c5aa95 100644 --- a/src/routes/sessions.ts +++ b/src/routes/sessions.ts @@ -1,16 +1,43 @@ import { Router } from 'express'; -import { body } from 'express-validator'; +import { body, oneOf } from 'express-validator'; import * as controller from '../controllers/session'; import requestValidator from '../middlewares/request-validator'; import sessionValidator from '../middlewares/session-validator'; +const sessionRules = [ + body('readIncomingMessages').isBoolean().optional(), + body('proxy').isString().notEmpty().optional(), + body('webhook').isObject().optional(), + oneOf([ + body('webhook.url').if(body('webhook').exists()).isString().notEmpty(), + [ + body('webhook.url').if(body('webhook').exists()).isArray({ min: 1 }), + body('webhook.url.*').isString().notEmpty(), + ], + ]), + oneOf([ + body('webhook.events').if(body('webhook').exists()).equals('all'), + [ + body('webhook.events').if(body('webhook').exists()).isArray({ min: 1 }), + body('webhook.events.*').isString().notEmpty(), + ], + ]), +]; + const router = Router(); router.get('/', controller.list); router.get('/:sessionId', sessionValidator, controller.find); router.get('/:sessionId/status', sessionValidator, controller.status); router.get('/:sessionId/qr', sessionValidator, controller.qr); -router.post('/add', body('sessionId').isString().notEmpty(), requestValidator, controller.add); +router.post( + '/add', + body('sessionId').isString().notEmpty(), + ...sessionRules, + requestValidator, + controller.add +); router.get('/:sessionId/add-sse', controller.addSSE); +router.patch('/:sessionId', ...sessionRules, requestValidator, sessionValidator, controller.update); router.delete('/:sessionId', sessionValidator, controller.del); export default router; diff --git a/src/wa.ts b/src/wa.ts index 937300f2..0b976ca3 100644 --- a/src/wa.ts +++ b/src/wa.ts @@ -15,17 +15,17 @@ import type { WebSocket } from 'ws'; import { logger, prisma } from './shared'; import { delay, pick } from './utils'; -const sessions = new Map(); +const sessions = new Map(); const retries = new Map(); const QRGenerations = new Map(); const RECONNECT_INTERVAL = Number(process.env.RECONNECT_INTERVAL || 0); const MAX_RECONNECT_RETRIES = Number(process.env.MAX_RECONNECT_RETRIES || 5); const MAX_QR_GENERATION = Number(process.env.MAX_QR_GENERATION || 5); -const SESSION_CONFIG_ID = 'session-config'; +export const SESSION_CONFIG_ID = 'session-config'; function shouldReconnect(sessionId: string) { - let attempts = retries.get(sessionId) ?? 1; + let attempts = retries.get(sessionId) ?? 0; if (attempts < MAX_RECONNECT_RETRIES) { attempts += 1; @@ -43,8 +43,9 @@ export async function init() { }); for (const { sessionId, data } of sessions) { - const { readIncomingMessages, proxy, ...socketConfig } = JSON.parse(data); - Session.create({ sessionId, readIncomingMessages, proxy, socketConfig }); + const { readIncomingMessages, proxy, webhook, ...socketConfig } = JSON.parse(data); + console.log(JSON.parse(data)); + Session.create({ sessionId, readIncomingMessages, proxy, webhook, socketConfig }); } } @@ -55,7 +56,7 @@ type SessionOptions = { readIncomingMessages?: boolean; proxy?: string; webhook?: { - url: string[]; + url: string | string[]; events: 'all' | (keyof BaileysEventMap)[]; }; socketConfig?: SocketConfig; @@ -65,6 +66,7 @@ export class Session { private connectionState: Partial = { connection: 'close' }; private lastGeneratedQR: string | null = null; public readonly socket: ReturnType; + public readonly store: Store; constructor( private readonly sessionState: Awaited>, @@ -92,15 +94,17 @@ export class Session { }); this.bindEvents(); - sessions.set(sessionId, { ...this, store: new Store(sessionId, this.socket.ev) }); + this.store = new Store(sessionId, this.socket.ev); + sessions.set(sessionId, this); } public static async create(options: SessionOptions) { - options = { readIncomingMessages: false, SSE: false, ...options }; - const { sessionId, readIncomingMessages, socketConfig } = options; + const { sessionId, readIncomingMessages = false, proxy, webhook, socketConfig } = options; const configID = `${SESSION_CONFIG_ID}-${sessionId}`; const data = JSON.stringify({ - readIncomingMessages: readIncomingMessages, + readIncomingMessages, + proxy, + webhook, ...socketConfig, }); @@ -165,9 +169,13 @@ export class Session { public async destroy(logout = true) { const { sessionId } = this.options; + const ws = this.socket.ws; try { await Promise.all([ - logout && this.socket.logout(), + logout && + ws.readyState !== ws.CLOSING && + ws.readyState !== ws.CLOSED && + this.socket.logout(), prisma.chat.deleteMany({ where: { sessionId } }), prisma.contact.deleteMany({ where: { sessionId } }), prisma.message.deleteMany({ where: { sessionId } }), @@ -177,6 +185,8 @@ export class Session { logger.error(e, 'An error occured during session destroy'); } finally { sessions.delete(sessionId); + retries.delete(sessionId); + QRGenerations.delete(sessionId); } } @@ -188,6 +198,7 @@ export class Session { const { connection } = update; if (connection === 'open') { + this.lastGeneratedQR = null; retries.delete(sessionId); QRGenerations.delete(sessionId); } else if (connection === 'close') this.handleConnectionClose(); @@ -205,15 +216,15 @@ export class Session { } if (webhook) { - this.socket.ev.process(async (events) => { - const data = webhook.events === 'all' ? events : pick(events, webhook.events); + const { url, events } = webhook; + this.socket.ev.process(async (socketEvents) => { + const data = events === 'all' ? socketEvents : pick(socketEvents, events); + if (Object.keys(data).length <= 0) return; + try { await Promise.any( - webhook.url.map((url) => - axios.post(url, JSON.stringify(data), { - headers: { - 'Content-Type': 'application/json', - }, + (typeof url === 'string' ? [url] : url).map((url) => + axios.post(url, data, { timeout: 5000, }) ) @@ -229,7 +240,7 @@ export class Session { const { sessionId, res, SSE } = this.options; const { qr } = this.connectionState; let generatedQR: string | null = null; - const currentQRGenerations = QRGenerations.get(sessionId) ?? 1; + const currentQRGenerations = QRGenerations.get(sessionId) ?? -1; if (qr) { try { @@ -269,7 +280,7 @@ export class Session { const doNotReconnect = !shouldReconnect(sessionId); if (code === DisconnectReason.loggedOut || doNotReconnect) { - if (res && res.writableEnded) { + if (res && !res.writableEnded) { !SSE && res.status(500).json({ error: 'Unable to create session' }); res.end(); } @@ -277,10 +288,7 @@ export class Session { } if (!restartRequired) { - logger.info( - { attempts: retries.get(sessionId) ?? 1, sessionId: sessionId }, - 'Reconnecting...' - ); + logger.info({ attempts: retries.get(sessionId) ?? 1, sessionId }, 'Reconnecting...'); } setTimeout(() => Session.create(this.options), restartRequired ? 0 : RECONNECT_INTERVAL); } diff --git a/yarn.lock b/yarn.lock index 41b440d5..a9f2481d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -123,7 +123,7 @@ "@ookamiiixd/baileys-store@file:debug/ookamiiixd-baileys-store-v1.0.0-beta.1.tgz": version "1.0.0-beta.1" - resolved "file:debug/ookamiiixd-baileys-store-v1.0.0-beta.1.tgz#87efc8a129a1674c42137296d8d5741f81cfbafe" + resolved "file:debug/ookamiiixd-baileys-store-v1.0.0-beta.1.tgz#ba2891ace176dee14a519af980f95b4f8360680a" dependencies: tiny-invariant "^1.3.1"