diff --git a/docker-compose.yaml b/docker-compose.yaml index 23fe3830..4fe7b0bd 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -30,3 +30,15 @@ services: - WHATSAPP_SWAGGER_PASSWORD=123 - WHATSAPP_API_HOSTNAME=localhost restart: always + + mongodb: + image: mongo + container_name: mongodb + ports: + - '27017:27017/tcp' + volumes: + - mongodb_data:/data/db + restart: always + +volumes: + mongodb_data: {} diff --git a/docs/site/content/en/docs/how-to/config/index.md b/docs/site/content/en/docs/how-to/config/index.md index 189c1794..b5ed46ac 100644 --- a/docs/site/content/en/docs/how-to/config/index.md +++ b/docs/site/content/en/docs/how-to/config/index.md @@ -38,6 +38,8 @@ The following environment variables can be used to configure the WAHA. response. - `WHATSAPP_START_SESSION=session1,session2`: This variable can be used to start sessions with the specified names right after launching the API. Separate session names with a comma. +- Also read more about [Sessions ->]({{< relref "/docs/how-to/sessions" >}}) +- Also read more about [Session Storages on the dedicated page ->]({{< relref "/docs/how-to/storages#sessions" >}}) ### Swagger - `WHATSAPP_SWAGGER_CONFIG_ADVANCED=true` - enables advanced configuration options for Swagger documentation - you can customize host, port and base URL for the requests. @@ -66,14 +68,18 @@ Read more about it on [**Session page** ->]({{< relref "/docs/how-to/sessions#co Keep in mind that session's proxy configuration takes precedence over proxy configuration set by environment variables! -### Security ![](/images/versions/plus.png) +### Security +Security is available in [WAHA Plus ![](/images/versions/plus.png)]({{< relref "/docs/how-to/plus-version" >}}) only. + - `WHATSAPP_API_KEY=mysecret`: If you set this variable, you must include the `X-Api-Key: mysecret` header in all requests to the API. This will protect the API with a secret code. - `WHATSAPP_SWAGGER_USERNAME=admin` and `WHATSAPP_SWAGGER_PASSWORD=admin`: These variables can be used to protect the Swagger panel with `admin / admin` credentials. This does not affect API access. +Read more about security settings for Swagger and API on [**Security page** ->]({{< relref "/docs/how-to/security" >}}). -### Files ![](/images/versions/plus.png) +### Files +Files configuration is available in [WAHA Plus ![](/images/versions/plus.png)]({{< relref "/docs/how-to/plus-version" >}}) only. The following environment variables can be used to configure the file storage options for the WAHA: @@ -85,8 +91,11 @@ The following environment variables can be used to configure the file storage op - Under the hood, it sets `WHATSAPP_FILES_MIMETYPES=mimetype/ignore-all-media` to ignore all media files. - `WHATSAPP_FILES_LIFETIME`: This variable can be used to set the time (in seconds) after which files will be removed to free up space. The default value is `180`. + - Set this variable to `0` to disable the file lifetime. - `WHATSAPP_FILES_FOLDER`: This variable can be used to set the folder where files from chats (images, voice messages) will be stored. The default value is `/tmp/whatsapp-files`. + - The folder must be mounted to the host machine to keep the files between container restarts. [ Read more about how to persist files ->]({{< relref "/docs/how-to/storages#media" >}}) +- Also read more about [Media Storages on the dedicated page ->]({{< relref "/docs/how-to/storages#media" >}}) 💡 Even if WAHA doesn't process the message media because of `WHATSAPP_FILES_MIMETYPES` or `WHATSAPP_DOWNLOAD_MEDIA` you'll get a webhook event with `hasMedia: True` field, but with no `media.url`. diff --git a/docs/site/content/en/docs/how-to/sessions/index.md b/docs/site/content/en/docs/how-to/sessions/index.md index 7f07e6be..3a098d6a 100644 --- a/docs/site/content/en/docs/how-to/sessions/index.md +++ b/docs/site/content/en/docs/how-to/sessions/index.md @@ -298,28 +298,8 @@ configure autostart options so when the docker container restarts - it restores ### Session persistent -#### File storage -If you want to save your session and do not scan QR code everytime when you launch WAHA - connect a local file storage -to the container. WAHA stores authentication information in the directory and reuses it after restart. - -[Attach volume](https://docs.docker.com/storage/volumes/) part to the command: - -```bash --v `pwd`/.sessions:/app/.sessions -``` - -The full command would be: - -```bash -docker run -d -v `pwd`/.sessions:/app/.sessions -p 3000:3000/tcp --name whatsapp-http-api devlikeapro/whatsapp-http-api-plus -``` - -#### Remote storage ![](/images/versions/soon.png) - -If you're interested in using some "remote" storage (like Redis or other Databases) to save sessions - please create an -issue on GitHub. - -For instances, it may be useful if you run WAHA in a cluster of servers and do not have shared file storage +If you want to save your session and do not scan QR code everytime when you launch WAHA - +[connect the session storage to the container ->]({{< relref "/docs/how-to/storages#sessions" >}}) ### Autostart If you don't want to call `POST /api/sessions/start` for every session each time when the container restart - diff --git a/docs/site/content/en/docs/how-to/storages/index.md b/docs/site/content/en/docs/how-to/storages/index.md new file mode 100644 index 00000000..9b257744 --- /dev/null +++ b/docs/site/content/en/docs/how-to/storages/index.md @@ -0,0 +1,139 @@ +--- +title: "Storages" +description: "Storages" +lead: "" +date: 2020-10-06T08:48:45+00:00 +lastmod: 2020-10-06T08:48:45+00:00 +draft: false +images: [ ] +weight: 700 +--- +There are several storages that are used by the WAHA, below you can find the list of them and the way to configure them. +1. [Sessions](#sessions) - used to store the session data, such as authentication information, configuration, and other data that is required to keep the session alive and connected to WhatsApp. +2. [Media](#media) - used to store the media files, such as images, videos, and other files that are received from the WhatsApp instance. + +Storages are available in [WAHA Plus ![](/images/versions/plus.png)]({{< relref "/docs/how-to/plus-version" >}}) only. + +## Sessions +The "Sessions" storage is used to store the session data, such as authentication information, configuration, +and other data that is required to keep the session alive and connected to WhatsApp. + +If you want to save your session and do not scan QR code everytime when you launch WAHA - +connect the session storage to the container. + +For the session storage, you can use the following options: +1. [Local (files)](#local-files) - the default option, stores the session data in the local storage using files. +2. [MongoDB](#mongodb) - stores the session data in the MongoDB database. + +The following table shows the compatibility of the session storage with the engines: + +| | WEBJS | NOWEB | VENOM | +|---------------|:-----:|:-----:|:-----:| +| Local (files) | ✔️ | ✔️ | ✔️ | +| MongoDB | ✔️ | ✔️ | ➖ | + +## Sessions - Local (files) +By default, the WAHA uses the **local storage (files)** to store the session data. + +{{< alert icon="💡" text="It's well tested solution even for production" />}} + +In order to use the local storage and save the session data between the container restarts, +you need to mount the volume to the `/app/.sessions` directory using the `-v` option in `docker run` command: + +```bash +-v /path/to/on/host/.sessions:/app/.sessions +``` + +The full command to run the WAHA with the local storage and save the session data +in the current directory and `.sessions` directory: +```bash +docker run -v `pwd`/.sessions:/app/.sessions -p 3000:3000/tcp devlikeapro/whatsapp-http-api-plus +``` +This is the only action you need to do to use the local storage - all session data will be available between the container restarts. + +### How it works +In the host machine, the session data will be stored in the current directory in the `.sessions` directory. + +Under the hood, the WAHA stores the session data in the following directory structure: +``` +.sessions/{engine}/{sessionName}/... +``` +when you "logout" the session using [POST /api/sessions/logout]({{< relref "/docs/how-to/sessions#logout">}}) or providing `logout: True` in [POST /api/sessions/stop]({{< relref "/docs/how-to/sessions#stop">}}) +it removes the directory with the session data. + + +## Sessions - MongoDB +If you want to use the MongoDB to store the session data, you need to: +1. Start the MongoDB server (using docker or any other way). You can either start your own MongoDB server or use the one of cloud providers, like [MongoDB Atlas](https://www.mongodb.com/atlas/database). +2. Set `WHATSAPP_SESSIONS_MONGO_URL=mongodb://user:password@host:port/` environment variable to connect to the MongoDB server. + + +**We recommend using your own MongoDB server as close as possible to WAHA server** for the best performance and security reasons. + +### Example +First, you need to start MongoDB server: +```bash +docker run -d -p 27017:27017 --name mongodb mongo +``` + +Then, you need to run the WAHA with the `WHATSAPP_SESSIONS_MONGO_URL` environment variable: +```bash +docker run -e WHATSAPP_SESSIONS_MONGO_URL=mongodb://localhost:27017/ -p 3000:3000/tcp devlikeapro/whatsapp-http-api-plus +``` + +This is the only action you need to do to use the MongoDB storage - +all session authentication data will be stored in the MongoDB database. + +### How it works +When you start a session, it stores the session data in the MongoDB database in two databases: +1. `waha_{engine}` - it saves the session configuration in `sessions` collection with `name: {sessionName}` field. +2. `waha_{engine}_{sessionName}` - it saves the WhatsApp authentication data and other session data. Each engine saves different data in this database. + +When you "logout" the session using [POST /api/sessions/logout]({{< relref "/docs/how-to/sessions#logout">}}) or providing `logout: True` in [POST /api/sessions/stop]({{< relref "/docs/how-to/sessions#stop">}}) it removes those databases or elements from them. + +For dealing and troubleshooting with the MongoDB, we recommend using [MongoDB Compass](https://www.mongodb.com/products/tools/compass). + +![](waha-mongodb.png) + +### MongoDB Atlas +If you use the [MongoDB Atlas](https://www.mongodb.com/atlas/database) you must grant +`Atlas Admin` role in **Security => Database Access** before you connect to the database. + +For production please consider running the MongoDB server close to the WAHA server for the best performance and security reasons. + +## Media +When your WhatsApp instance receives media files, it stores them in the media storage. + +You can use the following options to store the media files: +1. [Local](#local) - the default option, stores the media files in the local storage using files. +2. [S3](#s3) - stores the media files in the S3 storage. + +Read more about [available configuration options ->]({{}}). + +The following table shows the compatibility of the storage with the engines: + +| | WEBJS | NOWEB | VENOM | +|-------|:-----:|:-----:|:-----:| +| Local | ✔️ | ✔️ | ✔️ | +| S3 | 🕒 | 🕒 | ➖ | + +## Media - Local +By default, the WAHA uses the **local file storage** to store the media files and those files has a short lifetime (180 seconds). +So it's your app responsibility to download and store them in a safe persistent place during this time. + +### Save media files between the container restarts +If you want to use the local storage and **save the media files between the container restarts for a long time** - you need to: +1. Specify a dedicated folder to store the media files using `WHATSAPP_FILES_FOLDER=/app/.media` environment variable +2. Disable automatic media files cleanup using `WHATSAPP_FILES_LIFETIME=0` environment variable +3. Connect the volume to the specified folder using the `-v /path/to/files/on/host:/app/.media` option in `docker run` command to the folder specified in `WHATSAPP_FILES_FOLDER` environment variable. + +Read more about [available configuration options ->]({{}}). + +Here's all the steps in one command: +```bash +docker run -v /path/to/on/host/.media:/app/.media -e WHATSAPP_FILES_FOLDER=/app/.media -e WHATSAPP_FILES_LIFETIME=0 -p 3000:3000/tcp devlikeapro/whatsapp-http-api-plus +``` + +## Media - S3 +If you're interested in using the S3 storage or any other cloud storage (like [self-hosted S3 - Minio](https://min.io/)), +please create an issue or vote for the S3 issue in [the GitHub repository](https://github.com/devlikeapro/whatsapp-http-api/issues?q=is%3Aissue+is%3Aopen+S3). diff --git a/docs/site/content/en/docs/how-to/storages/waha-mongodb.png b/docs/site/content/en/docs/how-to/storages/waha-mongodb.png new file mode 100644 index 00000000..04a6c1d8 Binary files /dev/null and b/docs/site/content/en/docs/how-to/storages/waha-mongodb.png differ diff --git a/docs/site/content/en/docs/overview/changelog.md b/docs/site/content/en/docs/overview/changelog.md index 8f467e6e..f5467efc 100644 --- a/docs/site/content/en/docs/overview/changelog.md +++ b/docs/site/content/en/docs/overview/changelog.md @@ -19,6 +19,16 @@ If you wish to get and read Release Notes - read them on one of our platforms, * You even can **subscribe to get new updates** there! +## 2024.2 +- Add support for [MongoDB as storage for Session data]({{< relref "/docs/how-to/storages" >}}) +- Support persistent file storage for media files - [now you can save media files between container restarts]({{< relref "/docs/how-to/storages#media" >}}) +- If you set `WHATSAPP_FILES_LIFETIME=0` environment variable - media files will be never deleted. + +## 2024.1 +- Implement [Patron Portal](https://portal.devlike.pro/) where you can get your personal API key and manage your perks. + - Read more on [Patreon ->](https://www.patreon.com/posts/waha-patron-97637416) + - Read more on [Boosty ->](https://boosty.to/wa-http-api/posts/8319079f-dac1-4179-b954-fcc559097c76) + ## 2024.2 - Listen for browser disconnected and page close events in **WEBJS** engine [#262](https://github.com/devlikeapro/whatsapp-http-api/issues/262) diff --git a/package.json b/package.json index e81997a4..28f2cb78 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "link-preview-js": "^3.0.4", "lodash": "^4.17.21", "mime-types": "^2.1.27", + "mongodb": "^6.3.0", "passport": "^0.6.0", "passport-headerapikey": "^1.2.2", "qrcode": "^1.5.1", @@ -53,7 +54,7 @@ "rxjs": "^7.1.0", "swagger-ui-express": "^4.1.4", "venom-bot": "5.0.1", - "whatsapp-web.js": "https://github.com/pedroslopez/whatsapp-web.js" + "whatsapp-web.js": "https://github.com/devlikeapro/whatsapp-web.js#main-fork" }, "devDependencies": { "@nestjs/cli": "^9.0.0", diff --git a/src/api/sessions.controller.ts b/src/api/sessions.controller.ts index 5b420eac..3cfcd320 100644 --- a/src/api/sessions.controller.ts +++ b/src/api/sessions.controller.ts @@ -29,7 +29,7 @@ class SessionsController { @Post('/start/') async start(@Body() request: SessionStartRequest): Promise { const result = await this.manager.start(request); - await this.manager.sessionStorage.configRepository.save( + await this.manager.sessionConfigRepository.save( request.name, request.config || null, ); diff --git a/src/config.service.ts b/src/config.service.ts index b47ae18a..4e414910 100644 --- a/src/config.service.ts +++ b/src/config.service.ts @@ -108,6 +108,10 @@ export class WhatsappConfigService { return value ? value.split(',') : []; } + getSessionMongoUrl(): string | undefined { + return this.configService.get('WHATSAPP_SESSIONS_MONGO_URL', undefined); + } + getDefaultEngineName(): WAHAEngine { const value = getEngineName(); if (value in WAHAEngine) { diff --git a/src/core/abc/DataStore.ts b/src/core/abc/DataStore.ts new file mode 100644 index 00000000..23e5a723 --- /dev/null +++ b/src/core/abc/DataStore.ts @@ -0,0 +1 @@ +export abstract class DataStore {} diff --git a/src/core/abc/manager.abc.ts b/src/core/abc/manager.abc.ts index 239ec0f7..a1369d7e 100644 --- a/src/core/abc/manager.abc.ts +++ b/src/core/abc/manager.abc.ts @@ -8,12 +8,15 @@ import { SessionStartRequest, SessionStopRequest, } from '../../structures/sessions.dto'; +import { ISessionAuthRepository } from '../storage/ISessionAuthRepository'; +import { ISessionConfigRepository } from '../storage/ISessionConfigRepository'; import { WhatsappSession } from './session.abc'; -import { LocalSessionStorage } from './storage.abc'; import { WebhookConductor } from './webhooks.abc'; export abstract class SessionManager implements OnApplicationShutdown { - public sessionStorage: LocalSessionStorage; + public store: any; + public sessionAuthRepository: ISessionAuthRepository; + public sessionConfigRepository: ISessionConfigRepository; protected abstract getEngine(engine: WAHAEngine): typeof WhatsappSession; diff --git a/src/core/abc/session.abc.ts b/src/core/abc/session.abc.ts index cd14f998..9c6e2fcf 100644 --- a/src/core/abc/session.abc.ts +++ b/src/core/abc/session.abc.ts @@ -3,7 +3,6 @@ import { EventEmitter } from 'events'; import * as fs from 'fs'; import { MessageId } from 'whatsapp-web.js'; -import { OTPRequest, RequestCodeRequest } from '../../structures/auth.dto'; import { ChatRequest, CheckNumberStatusQuery, @@ -47,8 +46,8 @@ import { import { WASessionStatusBody } from '../../structures/webhooks.dto'; import { NotImplementedByEngineError } from '../exceptions'; import { QR } from '../QR'; +import { DataStore } from './DataStore'; import { MediaManager } from './media.abc'; -import { LocalSessionStorage } from './storage.abc'; const CHROME_PATH = '/usr/bin/google-chrome-stable'; const CHROMIUM_PATH = '/usr/bin/chromium'; @@ -77,7 +76,7 @@ export interface SessionParams { name: string; mediaManager: MediaManager; log: ConsoleLogger; - sessionStorage: LocalSessionStorage; + sessionStore: DataStore; proxyConfig?: ProxyConfig; sessionConfig?: SessionConfig; } @@ -89,7 +88,7 @@ export abstract class WhatsappSession { public name: string; protected mediaManager: MediaManager; protected log: ConsoleLogger; - protected sessionStorage: LocalSessionStorage; + protected sessionStore: DataStore; protected proxyConfig?: ProxyConfig; public sessionConfig?: SessionConfig; @@ -98,7 +97,7 @@ export abstract class WhatsappSession { public constructor({ name, log, - sessionStorage, + sessionStore, proxyConfig, mediaManager, sessionConfig, @@ -107,7 +106,7 @@ export abstract class WhatsappSession { this.name = name; this.proxyConfig = proxyConfig; this.log = log; - this.sessionStorage = sessionStorage; + this.sessionStore = sessionStore; this.mediaManager = mediaManager; this.sessionConfig = sessionConfig; } diff --git a/src/core/abc/storage.abc.ts b/src/core/abc/storage.abc.ts deleted file mode 100644 index 5c4c005a..00000000 --- a/src/core/abc/storage.abc.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { SessionConfig } from '../../structures/sessions.dto'; - -abstract class SessionConfigRepository { - abstract save( - sessionName: string, - config: SessionConfig | null, - ): Promise; - - abstract get(sessionName: string): Promise; -} - -abstract class SessionStorage { - protected constructor(public engine: string) { - this.engine = engine; - } - - abstract init(sessionName?: string): Promise; - - abstract clean(sessionName: string): Promise; - - abstract getAll(): Promise; - - // - // Repositories - // - abstract get configRepository(): SessionConfigRepository; -} - -abstract class LocalSessionStorage extends SessionStorage { - public sessionsFolder: string; - - abstract getFolderPath(sessionName: string): string; -} - -export { LocalSessionStorage, SessionConfigRepository }; diff --git a/src/core/engines/noweb/NowebAuthFactoryCore.ts b/src/core/engines/noweb/NowebAuthFactoryCore.ts new file mode 100644 index 00000000..df7c1c87 --- /dev/null +++ b/src/core/engines/noweb/NowebAuthFactoryCore.ts @@ -0,0 +1,19 @@ +import { useMultiFileAuthState } from '@adiwajshing/baileys'; +import { Store } from 'whatsapp-web.js'; + +import { DataStore } from '../../abc/DataStore'; +import { LocalStore } from '../../storage/LocalStore'; + +export class NowebAuthFactoryCore { + buildAuth(store: DataStore, name: string) { + if (store instanceof LocalStore) return this.buildLocalAuth(store, name); + throw new Error(`Unsupported store type '${store.constructor.name}'`); + } + + protected async buildLocalAuth(store: LocalStore, name: string) { + await store.init(name); + const authFolder = store.getSessionDirectory(name); + const { state, saveCreds } = await useMultiFileAuthState(authFolder); + return { state, saveCreds }; + } +} diff --git a/src/core/session.noweb.core.ts b/src/core/engines/noweb/session.noweb.core.ts similarity index 95% rename from src/core/session.noweb.core.ts rename to src/core/engines/noweb/session.noweb.core.ts index f35334dc..c799cedc 100644 --- a/src/core/session.noweb.core.ts +++ b/src/core/engines/noweb/session.noweb.core.ts @@ -5,6 +5,7 @@ import makeWASocket, { getKeyAuthor, isJidGroup, jidNormalizedUser, + makeCacheableSignalKeyStore, makeInMemoryStore, PresenceData, proto, @@ -19,7 +20,7 @@ import { Agent } from 'https'; import * as lodash from 'lodash'; import { PairingCodeResponse } from 'src/structures/auth.dto'; -import { flipObject, splitAt } from '../helpers'; +import { flipObject, splitAt } from '../../../helpers'; import { ChatRequest, CheckNumberStatusQuery, @@ -35,8 +36,8 @@ import { MessageVoiceRequest, SendSeenRequest, WANumberExistResult, -} from '../structures/chatting.dto'; -import { ContactQuery, ContactRequest } from '../structures/contacts.dto'; +} from '../../../structures/chatting.dto'; +import { ContactQuery, ContactRequest } from '../../../structures/contacts.dto'; import { ACK_UNKNOWN, SECOND, @@ -45,35 +46,36 @@ import { WAHAPresenceStatus, WAHASessionStatus, WAMessageAck, -} from '../structures/enums.dto'; +} from '../../../structures/enums.dto'; import { CreateGroupRequest, ParticipantsRequest, -} from '../structures/groups.dto'; +} from '../../../structures/groups.dto'; import { WAHAChatPresences, WAHAPresenceData, -} from '../structures/presence.dto'; -import { WAMessage } from '../structures/responses.dto'; -import { MeInfo } from '../structures/sessions.dto'; -import { BROADCAST_ID, TextStatus } from '../structures/status.dto'; +} from '../../../structures/presence.dto'; +import { WAMessage } from '../../../structures/responses.dto'; +import { MeInfo } from '../../../structures/sessions.dto'; +import { BROADCAST_ID, TextStatus } from '../../../structures/status.dto'; import { PollVote, PollVotePayload, WAMessageAckBody, -} from '../structures/webhooks.dto'; -import { IEngineMediaProcessor } from './abc/media.abc'; +} from '../../../structures/webhooks.dto'; +import { IEngineMediaProcessor } from '../../abc/media.abc'; import { ensureSuffix, WAHAInternalEvent, WhatsappSession, -} from './abc/session.abc'; +} from '../../abc/session.abc'; import { AvailableInPlusVersion, NotImplementedByEngineError, -} from './exceptions'; -import { createAgentProxy } from './helpers.proxy'; -import { QR } from './QR'; +} from '../../exceptions'; +import { createAgentProxy } from '../../helpers.proxy'; +import { QR } from '../../QR'; +import { NowebAuthFactoryCore } from './NowebAuthFactoryCore'; // eslint-disable-next-line @typescript-eslint/no-var-requires const QRCode = require('qrcode'); @@ -101,6 +103,8 @@ const ToEnginePresenceStatus = flipObject(PresenceStatuses); export class WhatsappSessionNoWebCore extends WhatsappSession { engine = WAHAEngine.NOWEB; + authFactory = new NowebAuthFactoryCore(); + get listenConnectionEventsFromTheStart() { return true; } @@ -123,7 +127,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { return { agent: agent, fetchAgent: agent, - auth: state, + auth: { + creds: state.creds, + /** caching makes the store faster to send/recv messages */ + keys: makeCacheableSignalKeyStore(state.keys, logger), + }, printQRInTerminal: true, browser: Browsers.macOS('Chrome'), logger: logger, @@ -134,9 +142,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { } async makeSocket() { - const authFolder = this.sessionStorage.getFolderPath(this.name); - await fs.mkdir(authFolder, { recursive: true }); - const { state, saveCreds } = await useMultiFileAuthState(authFolder); + const { state, saveCreds } = await this.authFactory.buildAuth( + this.sessionStore, + this.name, + ); const agent = this.makeAgent(); const socketConfig = this.getSocketConfig(agent, state); const sock: any = makeWASocket(socketConfig); diff --git a/src/core/session.venom.core.ts b/src/core/engines/venom/session.venom.core.ts similarity index 95% rename from src/core/session.venom.core.ts rename to src/core/engines/venom/session.venom.core.ts index fc52ab2d..4ab10414 100644 --- a/src/core/session.venom.core.ts +++ b/src/core/engines/venom/session.venom.core.ts @@ -15,17 +15,17 @@ import { MessageReplyRequest, MessageTextRequest, WANumberExistResult, -} from '../structures/chatting.dto'; +} from '../../../structures/chatting.dto'; import { WAHAEngine, WAHAEvents, WAHASessionStatus, -} from '../structures/enums.dto'; -import { WAMessage } from '../structures/responses.dto'; -import { IEngineMediaProcessor } from './abc/media.abc'; -import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc'; -import { NotImplementedByEngineError } from './exceptions'; -import { QR } from './QR'; +} from '../../../structures/enums.dto'; +import { WAMessage } from '../../../structures/responses.dto'; +import { IEngineMediaProcessor } from '../../abc/media.abc'; +import { WAHAInternalEvent, WhatsappSession } from '../../abc/session.abc'; +import { NotImplementedByEngineError } from '../../exceptions'; +import { QR } from '../../QR'; export class WhatsappSessionVenomCore extends WhatsappSession { engine = WAHAEngine.VENOM; diff --git a/src/core/session.webjs.core.ts b/src/core/engines/webjs/session.webjs.core.ts similarity index 96% rename from src/core/session.webjs.core.ts rename to src/core/engines/webjs/session.webjs.core.ts index 5bf6494e..c7a0ed76 100644 --- a/src/core/session.webjs.core.ts +++ b/src/core/engines/webjs/session.webjs.core.ts @@ -24,8 +24,8 @@ import { MessageVoiceRequest, SendSeenRequest, WANumberExistResult, -} from '../structures/chatting.dto'; -import { ContactQuery, ContactRequest } from '../structures/contacts.dto'; +} from '../../../structures/chatting.dto'; +import { ContactQuery, ContactRequest } from '../../../structures/contacts.dto'; import { ACK_UNKNOWN, WAHAEngine, @@ -33,22 +33,22 @@ import { WAHAPresenceStatus, WAHASessionStatus, WAMessageAck, -} from '../structures/enums.dto'; +} from '../../../structures/enums.dto'; import { CreateGroupRequest, ParticipantsRequest, SettingsSecurityChangeInfo, -} from '../structures/groups.dto'; -import { WAMessage } from '../structures/responses.dto'; -import { MeInfo } from '../structures/sessions.dto'; -import { WAMessageRevokedBody } from '../structures/webhooks.dto'; -import { IEngineMediaProcessor } from './abc/media.abc'; -import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc'; +} from '../../../structures/groups.dto'; +import { WAMessage } from '../../../structures/responses.dto'; +import { MeInfo } from '../../../structures/sessions.dto'; +import { WAMessageRevokedBody } from '../../../structures/webhooks.dto'; +import { IEngineMediaProcessor } from '../../abc/media.abc'; +import { WAHAInternalEvent, WhatsappSession } from '../../abc/session.abc'; import { AvailableInPlusVersion, NotImplementedByEngineError, -} from './exceptions'; -import { QR } from './QR'; +} from '../../exceptions'; +import { QR } from '../../QR'; // eslint-disable-next-line @typescript-eslint/no-var-requires const QRCode = require('qrcode'); @@ -67,7 +67,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { this.qr = new QR(); } - protected buildClient() { + protected async buildClient() { const clientOptions: ClientOptions = { puppeteer: { headless: true, @@ -98,7 +98,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { async start() { this.status = WAHASessionStatus.STARTING; - this.whatsapp = this.buildClient(); + this.whatsapp = await this.buildClient(); this.whatsapp .initialize() .then(() => { diff --git a/src/core/manager.core.ts b/src/core/manager.core.ts index ea37bf86..47b2e8a3 100644 --- a/src/core/manager.core.ts +++ b/src/core/manager.core.ts @@ -19,13 +19,15 @@ import { import { WebhookConfig } from '../structures/webhooks.config.dto'; import { SessionManager } from './abc/manager.abc'; import { SessionParams, WhatsappSession } from './abc/session.abc'; +import { WhatsappSessionNoWebCore } from './engines/noweb/session.noweb.core'; +import { WhatsappSessionVenomCore } from './engines/venom/session.venom.core'; +import { WhatsappSessionWebJSCore } from './engines/webjs/session.webjs.core'; import { DOCS_URL } from './exceptions'; import { getProxyConfig } from './helpers.proxy'; import { CoreMediaManager, MediaStorageCore } from './media.core'; -import { WhatsappSessionNoWebCore } from './session.noweb.core'; -import { WhatsappSessionVenomCore } from './session.venom.core'; -import { WhatsappSessionWebJSCore } from './session.webjs.core'; -import { SessionStorageCore } from './storage.core'; +import { LocalSessionAuthRepository } from './storage/LocalSessionAuthRepository'; +import { LocalSessionConfigRepository } from './storage/LocalSessionConfigRepository'; +import { LocalStoreCore } from './storage/LocalStoreCore'; import { WebhookConductorCore } from './webhooks.core'; export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException { @@ -59,8 +61,9 @@ export class SessionManagerCore extends SessionManager { this.session = undefined; const engineName = this.config.getDefaultEngineName(); this.EngineClass = this.getEngine(engineName); - this.sessionStorage = new SessionStorageCore(engineName.toLowerCase()); - + this.store = new LocalStoreCore(engineName.toLowerCase()); + this.sessionAuthRepository = new LocalSessionAuthRepository(this.store); + this.sessionConfigRepository = new LocalSessionConfigRepository(this.store); this.startPredefinedSessions(); } @@ -116,11 +119,11 @@ export class SessionManagerCore extends SessionManager { name, mediaManager, log, - sessionStorage: this.sessionStorage, + sessionStore: this.store, proxyConfig: proxyConfig, sessionConfig: request.config, }; - await this.sessionStorage.init(name); + await this.sessionAuthRepository.init(name); // @ts-ignore const session = new this.EngineClass(sessionConfig); this.session = session; @@ -181,7 +184,7 @@ export class SessionManagerCore extends SessionManager { } async logout(request: SessionLogoutRequest) { - await this.sessionStorage.clean(request.name); + await this.sessionAuthRepository.clean(request.name); } getSession(name: string): WhatsappSession { diff --git a/src/core/storage.core.ts b/src/core/storage.core.ts deleted file mode 100644 index d08eac24..00000000 --- a/src/core/storage.core.ts +++ /dev/null @@ -1,97 +0,0 @@ -import * as crypto from 'crypto'; -import * as fs from 'fs/promises'; -import * as os from 'os'; -import * as path from 'path'; - -import { SessionConfig } from '../structures/sessions.dto'; -import { - LocalSessionStorage, - SessionConfigRepository, -} from './abc/storage.abc'; - -class LocalSessionConfigRepository extends SessionConfigRepository { - FILENAME = '.waha.session.config.json'; - - constructor(private storage: LocalSessionStorage) { - super(); - } - - async get(sessionName: string): Promise { - const filepath = this.getFilePath(sessionName); - // Check file exists - try { - await fs.access(filepath, fs.constants.F_OK); - } catch (error) { - return null; - } - - // Try to load config - let content; - try { - content = await fs.readFile(filepath, 'utf-8'); - } catch (error) { - return null; - } - - return JSON.parse(content); - } - - async save(sessionName: string, config: SessionConfig | null) { - // Create folder if not exist - const folder = this.storage.getFolderPath(sessionName); - await fs.mkdir(folder, { recursive: true }); - // Save config - const filepath = this.getFilePath(sessionName); - const content = JSON.stringify(config || null); - await fs.writeFile(filepath, content); - } - - private getFilePath(sessionName): string { - const folder = this.storage.getFolderPath(sessionName); - return path.join(folder, this.FILENAME); - } -} - -export class SessionStorageCore extends LocalSessionStorage { - public sessionsFolder: string; - - constructor(engine: string) { - super(engine); - this.sessionsFolder = path.join(os.tmpdir(), 'waha-sessions'); - } - - get engineFolder() { - return path.join(this.sessionsFolder, this.engine); - } - - async init(sessionName?: string) { - const folder = sessionName - ? this.getFolderPath(sessionName) - : this.engineFolder; - await fs.mkdir(folder, { recursive: true }); - } - - getFolderPath(name: string): string { - const suffix = crypto.createHash('md5').update(name).digest('hex'); - return path.join(this.engineFolder, `${name}-${suffix}`); - } - - async clean(sessionName: string) { - const folder = this.getFolderPath(sessionName); - await fs.rm(folder, { recursive: true }); - } - - async getAll(): Promise { - await this.init(); - const content = await fs.readdir(this.engineFolder, { - withFileTypes: true, - }); - return content - .filter((dirent) => dirent.isDirectory()) - .map((dirent) => dirent.name); - } - - get configRepository(): LocalSessionConfigRepository { - return new LocalSessionConfigRepository(this); - } -} diff --git a/src/core/storage/ISessionAuthRepository.ts b/src/core/storage/ISessionAuthRepository.ts new file mode 100644 index 00000000..f9b2a4e3 --- /dev/null +++ b/src/core/storage/ISessionAuthRepository.ts @@ -0,0 +1,7 @@ +export abstract class ISessionAuthRepository { + abstract init(sessionName?: string): Promise; + + abstract clean(sessionName: string): Promise; + + abstract getAll(): Promise; +} diff --git a/src/core/storage/ISessionConfigRepository.ts b/src/core/storage/ISessionConfigRepository.ts new file mode 100644 index 00000000..f5cb3d19 --- /dev/null +++ b/src/core/storage/ISessionConfigRepository.ts @@ -0,0 +1,12 @@ +import { SessionConfig } from '../../structures/sessions.dto'; + +export abstract class ISessionConfigRepository { + abstract save( + sessionName: string, + config: SessionConfig | null, + ): Promise; + + abstract get(sessionName: string): Promise; + + abstract delete(sessionName: string): Promise; +} diff --git a/src/core/storage/LocalSessionAuthRepository.ts b/src/core/storage/LocalSessionAuthRepository.ts new file mode 100644 index 00000000..2a7ef56c --- /dev/null +++ b/src/core/storage/LocalSessionAuthRepository.ts @@ -0,0 +1,32 @@ +import * as fs from 'fs/promises'; + +import { ISessionAuthRepository } from './ISessionAuthRepository'; +import { LocalStore } from './LocalStore'; + +export class LocalSessionAuthRepository extends ISessionAuthRepository { + private store: LocalStore; + + constructor(store: LocalStore) { + super(); + this.store = store; + } + + async init(sessionName?: string) { + await this.store.init(sessionName); + } + + async clean(sessionName: string) { + const folder = this.store.getSessionDirectory(sessionName); + await fs.rm(folder, { recursive: true }); + } + + async getAll(): Promise { + await this.init(); + const content = await fs.readdir(this.store.getEngineDirectory(), { + withFileTypes: true, + }); + return content + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + } +} diff --git a/src/core/storage/LocalSessionConfigRepository.ts b/src/core/storage/LocalSessionConfigRepository.ts new file mode 100644 index 00000000..3c2c30fa --- /dev/null +++ b/src/core/storage/LocalSessionConfigRepository.ts @@ -0,0 +1,56 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; + +import { SessionConfig } from '../../structures/sessions.dto'; +import { ISessionConfigRepository } from './ISessionConfigRepository'; +import { LocalStore } from './LocalStore'; + +export class LocalSessionConfigRepository extends ISessionConfigRepository { + FILENAME = '.waha.session.config.json'; + private store: LocalStore; + + constructor(store: LocalStore) { + super(); + this.store = store; + } + + async get(sessionName: string): Promise { + const filepath = this.getFilePath(sessionName); + // Check file exists + try { + await fs.access(filepath, fs.constants.F_OK); + } catch (error) { + return null; + } + + // Try to load config + let content; + try { + content = await fs.readFile(filepath, 'utf-8'); + } catch (error) { + return null; + } + + return JSON.parse(content); + } + + async save(sessionName: string, config: SessionConfig | null) { + // Create folder if not exist + const folder = this.store.getSessionDirectory(sessionName); + await fs.mkdir(folder, { recursive: true }); + // Save config + const filepath = this.getFilePath(sessionName); + const content = JSON.stringify(config || null); + await fs.writeFile(filepath, content); + } + + private getFilePath(sessionName): string { + const folder = this.store.getSessionDirectory(sessionName); + return path.join(folder, this.FILENAME); + } + + async delete(sessionName: string): Promise { + const filepath = this.getFilePath(sessionName); + await fs.unlink(filepath); + } +} diff --git a/src/core/storage/LocalStore.ts b/src/core/storage/LocalStore.ts new file mode 100644 index 00000000..eed83f19 --- /dev/null +++ b/src/core/storage/LocalStore.ts @@ -0,0 +1,20 @@ +import { DataStore } from '../abc/DataStore'; + +export abstract class LocalStore extends DataStore { + abstract init(sessionName?: string): Promise; + + /** + * Get the directory where all the engines and sessions are stored + */ + abstract getBaseDirectory(): string; + + /** + * Get the directory where the engine sessions are stored + */ + abstract getEngineDirectory(): string; + + /** + * Get the directory where the session data is stored + */ + abstract getSessionDirectory(name: string): string; +} diff --git a/src/core/storage/LocalStoreCore.ts b/src/core/storage/LocalStoreCore.ts new file mode 100644 index 00000000..fd19eb98 --- /dev/null +++ b/src/core/storage/LocalStoreCore.ts @@ -0,0 +1,51 @@ +import * as crypto from 'crypto'; +import * as fs from 'fs/promises'; +import * as os from 'os'; +import * as path from 'path'; + +import { LocalStore } from './LocalStore'; + +export class LocalStoreCore implements LocalStore { + protected readonly baseDirectory: string = path.join( + os.tmpdir(), + 'waha-sessions', + ); + + private readonly engine: string; + + constructor(engine: string) { + this.engine = engine; + } + + async init(sessionName?: string) { + await fs.mkdir(this.getEngineDirectory(), { recursive: true }); + if (sessionName) { + await fs.mkdir(this.getSessionDirectory(sessionName), { + recursive: true, + }); + } + } + + /** + * Get the directory where all the engines and sessions are stored + */ + getBaseDirectory() { + return path.join(this.baseDirectory); + } + + /** + * Get the directory where the engine sessions are stored + */ + getEngineDirectory() { + return path.join(this.baseDirectory, this.engine); + } + + getSessionDirectory(name: string): string { + return this.getDirectoryPath(name); + } + + protected getDirectoryPath(name: string): string { + const suffix = crypto.createHash('md5').update(name).digest('hex'); + return path.join(this.getEngineDirectory(), `${name}-${suffix}`); + } +} diff --git a/yarn.lock b/yarn.lock index 8e7d727d..04448adb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -935,6 +935,15 @@ __metadata: languageName: node linkType: hard +"@mongodb-js/saslprep@npm:^1.1.0": + version: 1.1.4 + resolution: "@mongodb-js/saslprep@npm:1.1.4" + dependencies: + sparse-bitfield: ^3.0.3 + checksum: 208fd6f82136fd4332d0a6c667f8090b08f365dd7aa5880b8c485501caed7b058a99c231085c51ad7fa25f4a590d96c87af9a5b3fc0aea4de8c527657e33e548 + languageName: node + linkType: hard + "@nestjs/cli@npm:^9.0.0": version: 9.5.0 resolution: "@nestjs/cli@npm:9.5.0" @@ -1818,6 +1827,22 @@ __metadata: languageName: node linkType: hard +"@types/webidl-conversions@npm:*": + version: 7.0.3 + resolution: "@types/webidl-conversions@npm:7.0.3" + checksum: 535ead9de4d3d6c8e4f4fa14e9db780d2a31e8020debc062f337e1420a41c3265e223e4f4b628f97a11ecf3b96390962cd88a9ffe34f44e159dec583ff49aa34 + languageName: node + linkType: hard + +"@types/whatwg-url@npm:^11.0.2": + version: 11.0.4 + resolution: "@types/whatwg-url@npm:11.0.4" + dependencies: + "@types/webidl-conversions": "*" + checksum: 5acd60dbd49df34b9131ea75dc5e24b03e084cc4ed5a273ddd801fdaf21c896abbbb846bb3ba71ffd5c715120bac0454debe569f39b9177887fb7636d65cdae4 + languageName: node + linkType: hard + "@types/ws@npm:^8.5.4": version: 8.5.5 resolution: "@types/ws@npm:8.5.5" @@ -2355,6 +2380,13 @@ __metadata: languageName: node linkType: hard +"adm-zip@npm:^0.5.10": + version: 0.5.10 + resolution: "adm-zip@npm:0.5.10" + checksum: 07ed91cf6423bf5dca4ee63977bc7635e91b8d21829c00829d48dce4c6932e1b19e6cfcbe44f1931c956e68795ae97183fc775913883fa48ce88a1ac11fb2034 + languageName: node + linkType: hard + "agent-base@npm:6, agent-base@npm:^6.0.2": version: 6.0.2 resolution: "agent-base@npm:6.0.2" @@ -3257,6 +3289,13 @@ __metadata: languageName: node linkType: hard +"bson@npm:^6.2.0": + version: 6.2.0 + resolution: "bson@npm:6.2.0" + checksum: 950fccd2abd0ff5a1bd3637f4697631298f1538314994ab8c9e13f1c9851d0fd042b54fe8340e00151c2acee43917ea40e64b800ceeea811b00f2de3e900c77e + languageName: node + linkType: hard + "buffer-crc32@npm:^0.2.1, buffer-crc32@npm:^0.2.13, buffer-crc32@npm:~0.2.3": version: 0.2.13 resolution: "buffer-crc32@npm:0.2.13" @@ -8634,6 +8673,13 @@ __metadata: languageName: node linkType: hard +"memory-pager@npm:^1.0.2": + version: 1.5.0 + resolution: "memory-pager@npm:1.5.0" + checksum: d1a2e684583ef55c61cd3a49101da645b11ad57014dfc565e0b43baa9004b743f7e4ab81493d8fff2ab24e9950987cc3209c94bcc4fc8d7e30a475489a1f15e9 + languageName: node + linkType: hard + "merge-deep@npm:^3.0.1": version: 3.0.3 resolution: "merge-deep@npm:3.0.3" @@ -8970,6 +9016,50 @@ __metadata: languageName: node linkType: hard +"mongodb-connection-string-url@npm:^3.0.0": + version: 3.0.0 + resolution: "mongodb-connection-string-url@npm:3.0.0" + dependencies: + "@types/whatwg-url": ^11.0.2 + whatwg-url: ^13.0.0 + checksum: 9d4885377345b14e2ba2ee63cb3085364a01aeae3aca622bbd568e7761caa58fdc664528f19f125c762cff230c9349f99e98396dd2271a4260286cf241a8c477 + languageName: node + linkType: hard + +"mongodb@npm:^6.3.0": + version: 6.3.0 + resolution: "mongodb@npm:6.3.0" + dependencies: + "@mongodb-js/saslprep": ^1.1.0 + bson: ^6.2.0 + mongodb-connection-string-url: ^3.0.0 + peerDependencies: + "@aws-sdk/credential-providers": ^3.188.0 + "@mongodb-js/zstd": ^1.1.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: ">=6.0.0 <7" + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + "@aws-sdk/credential-providers": + optional: true + "@mongodb-js/zstd": + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + checksum: ebc5d9dbd1299321b6873e86eb4ea635316f97450644811db24ce2b01432b1c641def864facf2eab6f0c0c5c360c318108ea5555142f55177ca4c33991c6d7c4 + languageName: node + linkType: hard + "mpg123-decoder@npm:^0.4.8": version: 0.4.8 resolution: "mpg123-decoder@npm:0.4.8" @@ -10176,6 +10266,13 @@ __metadata: languageName: node linkType: hard +"punycode@npm:^2.3.0": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: bb0a0ceedca4c3c57a9b981b90601579058903c62be23c5e8e843d2c2d4148a3ecf029d5133486fb0e1822b098ba8bba09e89d6b21742d02fa26bda6441a6fb2 + languageName: node + linkType: hard + "puppeteer-core@npm:20.9.0": version: 20.9.0 resolution: "puppeteer-core@npm:20.9.0" @@ -11449,6 +11546,15 @@ __metadata: languageName: node linkType: hard +"sparse-bitfield@npm:^3.0.3": + version: 3.0.3 + resolution: "sparse-bitfield@npm:3.0.3" + dependencies: + memory-pager: ^1.0.2 + checksum: 174da88dbbcc783d5dbd26921931cc83830280b8055fb05333786ebe6fc015b9601b24972b3d55920dd2d9f5fb120576fbfa2469b08e5222c9cadf3f05210aab + languageName: node + linkType: hard + "spdx-correct@npm:^3.0.0": version: 3.2.0 resolution: "spdx-correct@npm:3.2.0" @@ -12196,6 +12302,15 @@ __metadata: languageName: node linkType: hard +"tr46@npm:^4.1.1": + version: 4.1.1 + resolution: "tr46@npm:4.1.1" + dependencies: + punycode: ^2.3.0 + checksum: aeeb821ac2cd792e63ec84888b4fd6598ac6ed75d861579e21a5cf9d4ee78b2c6b94e7d45036f2ca2088bc85b9b46560ad23c4482979421063b24137349dbd96 + languageName: node + linkType: hard + "tr46@npm:~0.0.3": version: 0.0.3 resolution: "tr46@npm:0.0.3" @@ -13015,6 +13130,13 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^7.0.0": + version: 7.0.0 + resolution: "webidl-conversions@npm:7.0.0" + checksum: f05588567a2a76428515333eff87200fae6c83c3948a7482ebb109562971e77ef6dc49749afa58abb993391227c5697b3ecca52018793e0cb4620a48f10bd21b + languageName: node + linkType: hard + "webpack-node-externals@npm:3.0.0": version: 3.0.0 resolution: "webpack-node-externals@npm:3.0.0" @@ -13104,6 +13226,7 @@ __metadata: link-preview-js: ^3.0.4 lodash: ^4.17.21 mime-types: ^2.1.27 + mongodb: ^6.3.0 passport: ^0.6.0 passport-headerapikey: ^1.2.2 prettier: ^1.19.1 @@ -13122,15 +13245,16 @@ __metadata: tsconfig-paths: ^4.1.0 typescript: ^4.8.4 venom-bot: 5.0.1 - whatsapp-web.js: "https://github.com/pedroslopez/whatsapp-web.js" + whatsapp-web.js: "https://github.com/devlikeapro/whatsapp-web.js#main-fork" languageName: unknown linkType: soft -"whatsapp-web.js@https://github.com/pedroslopez/whatsapp-web.js": +"whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js#main-fork": version: 1.23.1-alpha.4 - resolution: "whatsapp-web.js@https://github.com/pedroslopez/whatsapp-web.js.git#commit=ef2f725bce1bd1afd8491f9de0cd2ed06a6febc3" + resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=6ab7270e9a2c86a81efb8831b7817da10daffff2" dependencies: "@pedroslopez/moduleraid": ^5.0.2 + adm-zip: ^0.5.10 archiver: ^5.3.1 fluent-ffmpeg: 2.1.2 fs-extra: ^10.1.0 @@ -13146,7 +13270,7 @@ __metadata: optional: true unzipper: optional: true - checksum: 36062f9aa97e5d3f2ae50235a1535ae4b67550ea40f875794590cbaed6045b6b5ae56bd3edd79092419018f5509ff8dfb8f03cb894ed76315f9583d546a4f4be + checksum: 425fa37ba93bc42a32609dde9e102a11e2783750119972153593a887200afbdf555dc3b559c346274b72d92ceb00cc363cf714b6ff40c8c35103676234170837 languageName: node linkType: hard @@ -13166,6 +13290,16 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:^13.0.0": + version: 13.0.0 + resolution: "whatwg-url@npm:13.0.0" + dependencies: + tr46: ^4.1.1 + webidl-conversions: ^7.0.0 + checksum: 7f69272a1bfd5f0d994988b9e234e35d21071a9bffe0d6fd4477d295552665c566b176ff8e0251a0a79c61c5a67a7a392e248aae5887d7e22bdff0125209e26b + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0"