From 4ff1c01e382fba9e613786e50417d5cbbdbbd949 Mon Sep 17 00:00:00 2001 From: Berg Pinheiro Date: Sat, 9 May 2026 22:36:15 -0300 Subject: [PATCH] [core] Convert GOWS Lottie stickers (application/was) to animated WebP - fix #2039 closes devlikeapro/waha-plus#43 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lottie stickers arrive from WhatsApp as a ZIP archive (mimetype application/was) containing animation/animation.json. This change intercepts those stickers in the GOWS Plus session, renders each animation frame off-screen, and delivers an animated WebP to the webhook instead of the raw ZIP. Implementation: - src/plus/utils/lottie-converter.ts (new): - Extracts animation.json from the ZIP via adm-zip - Renders frames with @lottiefiles/dotlottie-web + @napi-rs/canvas (HTMLCanvasElement polyfill required; ImageData must NOT be polyfilled to avoid per-frame pixel caching in WASM memory) - Drives frames manually via dotLottie.setFrame(i) after 'load' for reliable synchronous capture - Encodes each RGBA frame to single-frame WebP with sharp (quality=80) - Assembles animated WebP via node-webpmux with full alpha transparency - Frame delay derived from animation fps; clamped to >=30ms - Fallback delay configurable via WAHA_LOTTIE_DEFAULT_DELAY_MS env var - sharp/adm-zip/node-webpmux loaded via require() — their module.exports is the callable itself with no .default, so ESM default import resolves to undefined at runtime - src/plus/engines/gows/session.gows.plus.ts: - Overrides downloadMedia with LottieAwareGOWSEngineMediaProcessor - normalizeStickerUrl: fixes GOWS URL field case mismatch (URL vs url) that caused an infinite network loop on Lottie downloads - Reports mimetype image/webp and extension .webp for Lottie stickers - package.json: add @lottiefiles/dotlottie-web, @napi-rs/canvas, node-webpmux to dependencies --- package.json | 3 + src/core/engines/gows/session.gows.core.ts | 15 +- src/core/engines/noweb/session.noweb.core.ts | 7 +- src/core/engines/webjs/session.webjs.core.ts | 4 +- src/core/engines/wpp/session.wpp.core.ts | 4 +- src/core/media/LottieMediaProcessorWrapper.ts | 69 +++++ src/core/utils/lottie-converter.ts | 284 ++++++++++++++++++ yarn.lock | 140 +++++++++ 8 files changed, 522 insertions(+), 4 deletions(-) create mode 100644 src/core/media/LottieMediaProcessorWrapper.ts create mode 100644 src/core/utils/lottie-converter.ts diff --git a/package.json b/package.json index 549c91fa..9ebe2742 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,9 @@ "@casl/ability": "^6.8.0", "@figuro/chatwoot-sdk": "^1.1.17", "@liaoliaots/nestjs-redis": "^9", + "@lottiefiles/dotlottie-web": "^0.72.1", "@modelcontextprotocol/sdk": "^1.29.0", + "@napi-rs/canvas": "^1.0.0", "@nestjs/axios": "^3.0.2", "@nestjs/bullmq": "^11.0.2", "@nestjs/common": "^11.0.0", @@ -94,6 +96,7 @@ "mustache": "^4.2.0", "nestjs-pino": "^4.1.0", "node-cache": "5.1.2", + "node-webpmux": "^3.2.1", "passport": "^0.7.0", "passport-headerapikey": "^1.2.2", "pg": "^8.13.1", diff --git a/src/core/engines/gows/session.gows.core.ts b/src/core/engines/gows/session.gows.core.ts index a0f12808..d4e50a3f 100644 --- a/src/core/engines/gows/session.gows.core.ts +++ b/src/core/engines/gows/session.gows.core.ts @@ -39,6 +39,7 @@ import { NotImplementedByEngineError, } from '@waha/core/exceptions'; import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor'; +import { LottieMediaProcessorWrapper } from '@waha/core/media/LottieMediaProcessorWrapper'; import { QR } from '@waha/core/QR'; import { ExtractMessageKeysForRead } from '@waha/core/utils/convertors'; import { parseMessageIdSerialized } from '@waha/core/utils/ids'; @@ -2224,7 +2225,10 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { } protected async downloadMedia(message) { - const processor = new GOWSEngineMediaProcessor(this); + let processor: IMediaEngineProcessor = new GOWSEngineMediaProcessor( + this, + ); + processor = new LottieMediaProcessorWrapper(processor, this.logger); const media = await this.mediaManager.processMedia( processor, message, @@ -2574,6 +2578,15 @@ export class GOWSEngineMediaProcessor implements IMediaEngineProcessor { async getMediaBuffer(message: any): Promise { const mediaDownloadTimeoutMs = 600_000; // 10 minutes + // Go serializes stickerMessage.URL as uppercase (Go struct field name), but + // whatsmeow's download unmarshals it expecting lowercase proto JSON. Add the + // lowercase alias so the download URL is found correctly. + const sticker = message?.Message?.stickerMessage; + if (sticker?.URL && !sticker?.url) { + message = JSON.parse(JSON.stringify(message)); + message.Message.stickerMessage.url = message.Message.stickerMessage.URL; + } + const data = JSON.stringify(message.Message); const tmpdir = new TmpDir( this.session.logger, diff --git a/src/core/engines/noweb/session.noweb.core.ts b/src/core/engines/noweb/session.noweb.core.ts index 491b78c9..93e267dc 100644 --- a/src/core/engines/noweb/session.noweb.core.ts +++ b/src/core/engines/noweb/session.noweb.core.ts @@ -66,6 +66,7 @@ import { toVcardV3 } from '@waha/core/vcard'; import { createAgentProxy } from '@waha/core/helpers.proxy'; import type { Agent } from 'https'; import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor'; +import { LottieMediaProcessorWrapper } from '@waha/core/media/LottieMediaProcessorWrapper'; import { QR } from '@waha/core/QR'; import { AckToStatus, StatusToAck } from '@waha/core/utils/acks'; import { pairs } from '@waha/utils/pairs'; @@ -2770,7 +2771,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { } protected async downloadMedia(message): Promise { - const processor = new NOWEBEngineMediaProcessor(this, this.loggerBuilder); + let processor: IMediaEngineProcessor = new NOWEBEngineMediaProcessor( + this, + this.loggerBuilder, + ); + processor = new LottieMediaProcessorWrapper(processor, this.logger); return this.mediaManager.processMedia(processor, message, this.name); } diff --git a/src/core/engines/webjs/session.webjs.core.ts b/src/core/engines/webjs/session.webjs.core.ts index c68bcd7e..e56e2dd3 100644 --- a/src/core/engines/webjs/session.webjs.core.ts +++ b/src/core/engines/webjs/session.webjs.core.ts @@ -33,6 +33,7 @@ import { NotImplementedByEngineError, } from '@waha/core/exceptions'; import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor'; +import { LottieMediaProcessorWrapper } from '@waha/core/media/LottieMediaProcessorWrapper'; import { QR } from '@waha/core/QR'; import { StatusToAck } from '@waha/core/utils/acks'; import { @@ -2224,7 +2225,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { } protected async downloadMedia(message: Message) { - const processor = new WEBJSEngineMediaProcessor(); + let processor = new WEBJSEngineMediaProcessor(); + processor = new LottieMediaProcessorWrapper(processor, this.logger); const media = await this.mediaManager.processMedia( processor, message, diff --git a/src/core/engines/wpp/session.wpp.core.ts b/src/core/engines/wpp/session.wpp.core.ts index 15d60e02..2603b510 100644 --- a/src/core/engines/wpp/session.wpp.core.ts +++ b/src/core/engines/wpp/session.wpp.core.ts @@ -126,6 +126,7 @@ import { NotImplementedByEngineError, } from '@waha/core/exceptions'; import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor'; +import { LottieMediaProcessorWrapper } from '@waha/core/media/LottieMediaProcessorWrapper'; import { IWPPAuthManager } from '@waha/core/engines/wpp/IWPPAuthManager'; import { QR } from '@waha/core/QR'; import { removeSingletonFiles } from '@waha/core/utils/chrome'; @@ -2118,7 +2119,8 @@ export class WhatsappSessionWPPCore extends WhatsappSession { } protected async downloadMedia(message: any): Promise { - const processor = new WPPEngineMediaProcessor(this.wpp); + let processor = new WPPEngineMediaProcessor(this.wpp); + processor = new LottieMediaProcessorWrapper(processor, this.logger); return this.mediaManager.processMedia(processor, message, this.name); } diff --git a/src/core/media/LottieMediaProcessorWrapper.ts b/src/core/media/LottieMediaProcessorWrapper.ts new file mode 100644 index 00000000..ee0e3d66 --- /dev/null +++ b/src/core/media/LottieMediaProcessorWrapper.ts @@ -0,0 +1,69 @@ +import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor'; +import { convertLottieZipToWebp } from '@waha/core/utils/lottie-converter'; + +const LOTTIE_MIMETYPE = 'application/was'; + +/** + * Wraps any engine media processor to transparently convert Lottie stickers + * (mimetype `application/was`, delivered as a ZIP archive) into animated WebP. + * The inner processor downloads the raw ZIP; this wrapper converts it before + * the result reaches the media storage layer, so callers see `image/webp`. + */ +export class LottieMediaProcessorWrapper implements IMediaEngineProcessor { + constructor( + private inner: IMediaEngineProcessor, + private logger: any, + ) {} + + hasMedia(msg: any): boolean { + return this.inner.hasMedia(msg); + } + + getChatId(msg: any): string { + return this.inner.getChatId(msg); + } + + getMessageId(msg: any): string { + return this.inner.getMessageId(msg); + } + + getMimetype(msg: any): string { + const mime = this.inner.getMimetype(msg); + return mime === LOTTIE_MIMETYPE ? 'image/webp' : mime; + } + + getFilename(msg: any): string | null { + if (this.inner.getMimetype(msg) !== LOTTIE_MIMETYPE) { + return this.inner.getFilename(msg); + } + return `${this.inner.getMessageId(msg)}.webp`; + } + + async getMediaBuffer(msg: any): Promise { + const buffer = await this.inner.getMediaBuffer(msg); + if (!buffer || !buffer.length) { + return null; + } + if (this.inner.getMimetype(msg) !== LOTTIE_MIMETYPE) { + return buffer; + } + + const id = this.inner.getMessageId(msg); + this.logger.info( + `Converting Lottie sticker '${id}' to WebP (input ${buffer.length} bytes)...`, + ); + try { + const webp = await convertLottieZipToWebp(buffer); + this.logger.info( + `Lottie sticker '${id}' converted to WebP (${webp.byteLength} bytes)`, + ); + return webp; + } catch (err) { + this.logger.warn( + { err: err }, + `Lottie conversion failed for '${id}', returning original buffer`, + ); + return buffer; + } + } +} diff --git a/src/core/utils/lottie-converter.ts b/src/core/utils/lottie-converter.ts new file mode 100644 index 00000000..eb0b4809 --- /dev/null +++ b/src/core/utils/lottie-converter.ts @@ -0,0 +1,284 @@ +import { createCanvas, Canvas } from '@napi-rs/canvas'; +import { DotLottie } from '@lottiefiles/dotlottie-web'; +// sharp, adm-zip and node-webpmux are CJS modules whose module.exports is the +// callable/constructor itself — static default imports resolve to undefined at +// runtime because there is no .default property. require() is the correct interop. +// eslint-disable-next-line @typescript-eslint/no-require-imports +const sharp = require('sharp'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const AdmZip = require('adm-zip'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const { Image: WebpImage } = require('node-webpmux'); + +// DotLottie._draw() guards canvas rendering behind: +// canvas instanceof HTMLCanvasElement || canvas instanceof OffscreenCanvas +// Both are undefined in Node.js, so the guard fails and _draw() returns without +// calling putImageData – producing a fully black canvas. Registering the +// @napi-rs/canvas Canvas constructor as HTMLCanvasElement makes the instanceof +// check pass so DotLottie acquires the 2D context and paints frames normally. +// +// ImageData must NOT be registered globally. When it is, DotLottie uses: +// new ImageData(Uint8ClampedArray_view_into_WASM_memory, w, h) +// @napi-rs/canvas copies the bytes once at construction and never re-reads the +// WASM buffer, so every frame produces the same cached pixels. Without a global +// ImageData, DotLottie falls back to: +// ctx.createImageData(w, h) + imageData.data.set(currentPixelBuffer) +// which copies fresh pixel data from the WASM buffer on every frame. +const globalAny = global as Record; +if (!globalAny['HTMLCanvasElement']) { + globalAny['HTMLCanvasElement'] = Canvas; +} + +export class LottieConvertError extends Error { + constructor(message: string, options?: { cause?: unknown }) { + super(message); + this.name = 'LottieConvertError'; + if (options?.cause !== undefined) { + (this as any).cause = options.cause; + } + } +} + +const STICKER_SIZE = 256; +const envDelayMs = parseInt( + process.env['WAHA_LOTTIE_DEFAULT_DELAY_MS'] ?? '', + 10, +); +const DEFAULT_DELAY_MS = envDelayMs > 0 ? envDelayMs : 100; + +function isZipBuffer(buffer: Buffer): boolean { + return ( + buffer.length >= 4 && + buffer[0] === 0x50 && + buffer[1] === 0x4b && + buffer[2] === 0x03 && + buffer[3] === 0x04 + ); +} + +function extractAnimationJson(zipBuffer: Buffer): string | null { + const zip = new AdmZip(zipBuffer); + const entry = + zip.getEntry('animation/animation.json') ?? + zip.getEntries().find((e) => e.entryName.endsWith('.json')); + if (!entry) { + return null; + } + return zip.readAsText(entry); +} + +export async function convertLottieZipToWebp( + zipBuffer: Buffer, +): Promise { + if (!isZipBuffer(zipBuffer)) { + throw new LottieConvertError('Lottie buffer is not a valid ZIP'); + } + + let animationJson: string | null; + try { + animationJson = extractAnimationJson(zipBuffer); + } catch (err) { + throw new LottieConvertError(`Failed to unzip Lottie file: ${err}`, { + cause: err, + }); + } + + if (!animationJson) { + throw new LottieConvertError('Lottie ZIP has no animation.json entry'); + } + + let animationData: Record; + try { + animationData = JSON.parse(animationJson); + } catch (err) { + throw new LottieConvertError( + `Lottie animation.json is not valid JSON: ${err}`, + { + cause: err, + }, + ); + } + + return renderLottieToWebp(animationData); +} + +function renderLottieToWebp( + animationData: Record, +): Promise { + return new Promise((resolve, reject) => { + let settled = false; + + function finish(result: Buffer) { + if (settled) return; + settled = true; + resolve(result); + } + + function fail(reason: unknown) { + if (settled) return; + settled = true; + reject( + reason instanceof LottieConvertError + ? reason + : new LottieConvertError(String(reason), { cause: reason }), + ); + } + + let canvas: Canvas; + let dotLottie: DotLottie; + + try { + canvas = createCanvas(STICKER_SIZE, STICKER_SIZE); + } catch (err) { + return fail(err); + } + + // DotLottie dispatches 'load' via setTimeout. Instead of relying on the + // setImmediate-based animation loop (timing-dependent, unreliable for + // frame capture), we drive frames manually with setFrame(i) after load. + // setFrame() renders synchronously: set_frame → render → putImageData. + try { + dotLottie = new DotLottie({ + loop: false, + useFrameInterpolation: false, + autoplay: false, + canvas: canvas as unknown as HTMLCanvasElement, + data: animationData, + renderConfig: { + autoResize: false, + devicePixelRatio: 1, + freezeOnOffscreen: false, + }, + }); + } catch (err) { + return fail(err); + } + + function onLoad() { + if (settled) return; + + const totalFrames = dotLottie.totalFrames; + const duration = dotLottie.duration; + let delayMs: number; + let framePixels: Buffer[]; + + try { + if (totalFrames <= 0) { + throw new Error( + `Animation has no frames (totalFrames=${totalFrames})`, + ); + } + + let rawDelayMs = DEFAULT_DELAY_MS; + if (duration > 0 && isFinite(duration)) { + const fps = totalFrames / duration; + const computed = Math.round(1000 / fps); + if (computed > 0 && isFinite(computed)) { + rawDelayMs = computed; + } + } + // WebP delay is in milliseconds. Clamp to at least 30ms to stay above + // the ~20ms threshold where some players impose their own minimum. + // When clamping, subsample frames proportionally so total duration is + // preserved — e.g. a 60fps animation must not play 1.8× too slowly + // because all 180 frames are kept at 30ms each. + delayMs = Math.max(30, rawDelayMs); + const frameStep = delayMs / rawDelayMs; + + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('canvas.getContext("2d") returned null'); + } + + // Collect raw RGBA pixels. When frameStep > 1 we subsample frames to + // keep playback speed correct despite the minimum-delay clamp. + // Pass the float position to setFrame so the renderer interpolates + // smoothly along the animation curve rather than snapping to the + // nearest integer frame (which causes uneven motion). + framePixels = []; + for (let t = 0; t < totalFrames; t += frameStep) { + // setFrame(0) is a no-op (already rendered after load), so skip it. + if (t > 0) { + dotLottie.setFrame(t); + } + framePixels.push( + Buffer.from( + ctx.getImageData(0, 0, STICKER_SIZE, STICKER_SIZE).data, + ), + ); + } + } catch (err) { + dotLottie.destroy(); + fail(err); + return; + } + + dotLottie.destroy(); + + // Encode each RGBA frame as a single-frame WebP, then assemble into an + // animated WebP via node-webpmux. This preserves full alpha transparency + // and avoids GIF's 256-colour palette limitation. + if (framePixels.length === 1) { + sharp(framePixels[0], { + raw: { width: STICKER_SIZE, height: STICKER_SIZE, channels: 4 }, + }) + .webp({ quality: 80, effort: 3 }) + .toBuffer() + .then(finish) + .catch(fail); + return; + } + + Promise.all( + framePixels.map((px) => + sharp(px, { + raw: { width: STICKER_SIZE, height: STICKER_SIZE, channels: 4 }, + }) + .webp({ quality: 80, effort: 3 }) + .toBuffer(), + ), + ) + .then((webpFrames) => + Promise.all( + webpFrames.map((buf) => + WebpImage.generateFrame({ + buffer: buf, + delay: delayMs, + x: 0, + y: 0, + blend: false, + dispose: true, + }), + ), + ), + ) + .then((frames) => + WebpImage.save(null, { + width: STICKER_SIZE, + height: STICKER_SIZE, + bgColor: [0, 0, 0, 0], + loops: 0, + frames: frames, + }), + ) + .then(finish) + .catch(fail); + } + + function onLoadError(payload: any) { + if (settled) return; + dotLottie.destroy(); + fail( + new LottieConvertError( + `DotLottie failed to load animation: ${ + payload?.error ?? 'unknown error' + }`, + { cause: payload?.error }, + ), + ); + } + + dotLottie.addEventListener('load', onLoad); + dotLottie.addEventListener('loadError', onLoadError); + }); +} diff --git a/yarn.lock b/yarn.lock index 7d8002d2..44e3c195 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2250,6 +2250,13 @@ __metadata: languageName: node linkType: hard +"@lottiefiles/dotlottie-web@npm:^0.72.1": + version: 0.72.1 + resolution: "@lottiefiles/dotlottie-web@npm:0.72.1" + checksum: 10/79bec5037fb07f38a676d32a11597277c0b15c8ad9716dbde008fe79b2e4e0374aaefcffd9bb68672a8bb35d02b57af4ed01a857238166f8ed85786ad29756b1 + languageName: node + linkType: hard + "@lukeed/csprng@npm:^1.0.0": version: 1.1.0 resolution: "@lukeed/csprng@npm:1.1.0" @@ -2367,6 +2374,129 @@ __metadata: languageName: node linkType: hard +"@napi-rs/canvas-android-arm64@npm:1.0.0": + version: 1.0.0 + resolution: "@napi-rs/canvas-android-arm64@npm:1.0.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/canvas-darwin-arm64@npm:1.0.0": + version: 1.0.0 + resolution: "@napi-rs/canvas-darwin-arm64@npm:1.0.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/canvas-darwin-x64@npm:1.0.0": + version: 1.0.0 + resolution: "@napi-rs/canvas-darwin-x64@npm:1.0.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-arm-gnueabihf@npm:1.0.0": + version: 1.0.0 + resolution: "@napi-rs/canvas-linux-arm-gnueabihf@npm:1.0.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-arm64-gnu@npm:1.0.0": + version: 1.0.0 + resolution: "@napi-rs/canvas-linux-arm64-gnu@npm:1.0.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-arm64-musl@npm:1.0.0": + version: 1.0.0 + resolution: "@napi-rs/canvas-linux-arm64-musl@npm:1.0.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-riscv64-gnu@npm:1.0.0": + version: 1.0.0 + resolution: "@napi-rs/canvas-linux-riscv64-gnu@npm:1.0.0" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-x64-gnu@npm:1.0.0": + version: 1.0.0 + resolution: "@napi-rs/canvas-linux-x64-gnu@npm:1.0.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-x64-musl@npm:1.0.0": + version: 1.0.0 + resolution: "@napi-rs/canvas-linux-x64-musl@npm:1.0.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@napi-rs/canvas-win32-arm64-msvc@npm:1.0.0": + version: 1.0.0 + resolution: "@napi-rs/canvas-win32-arm64-msvc@npm:1.0.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/canvas-win32-x64-msvc@npm:1.0.0": + version: 1.0.0 + resolution: "@napi-rs/canvas-win32-x64-msvc@npm:1.0.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@napi-rs/canvas@npm:^1.0.0": + version: 1.0.0 + resolution: "@napi-rs/canvas@npm:1.0.0" + dependencies: + "@napi-rs/canvas-android-arm64": "npm:1.0.0" + "@napi-rs/canvas-darwin-arm64": "npm:1.0.0" + "@napi-rs/canvas-darwin-x64": "npm:1.0.0" + "@napi-rs/canvas-linux-arm-gnueabihf": "npm:1.0.0" + "@napi-rs/canvas-linux-arm64-gnu": "npm:1.0.0" + "@napi-rs/canvas-linux-arm64-musl": "npm:1.0.0" + "@napi-rs/canvas-linux-riscv64-gnu": "npm:1.0.0" + "@napi-rs/canvas-linux-x64-gnu": "npm:1.0.0" + "@napi-rs/canvas-linux-x64-musl": "npm:1.0.0" + "@napi-rs/canvas-win32-arm64-msvc": "npm:1.0.0" + "@napi-rs/canvas-win32-x64-msvc": "npm:1.0.0" + dependenciesMeta: + "@napi-rs/canvas-android-arm64": + optional: true + "@napi-rs/canvas-darwin-arm64": + optional: true + "@napi-rs/canvas-darwin-x64": + optional: true + "@napi-rs/canvas-linux-arm-gnueabihf": + optional: true + "@napi-rs/canvas-linux-arm64-gnu": + optional: true + "@napi-rs/canvas-linux-arm64-musl": + optional: true + "@napi-rs/canvas-linux-riscv64-gnu": + optional: true + "@napi-rs/canvas-linux-x64-gnu": + optional: true + "@napi-rs/canvas-linux-x64-musl": + optional: true + "@napi-rs/canvas-win32-arm64-msvc": + optional: true + "@napi-rs/canvas-win32-x64-msvc": + optional: true + canvas: + built: true + skia-canvas: + built: true + checksum: 10/1b2838833988c98d86105980ccd52fb53c4059e30732c7bebdf21774d83a351bf389337c8e6eb4d55fc31359accb90e27d8d80c94cc3bb08d415ab0586227464 + languageName: node + linkType: hard + "@nestjs/axios@npm:^3.0.2": version: 3.1.3 resolution: "@nestjs/axios@npm:3.1.3" @@ -10498,6 +10628,13 @@ __metadata: languageName: node linkType: hard +"node-webpmux@npm:^3.2.1": + version: 3.2.1 + resolution: "node-webpmux@npm:3.2.1" + checksum: 10/8797ebe4846707443629800c70d95c592ab1c18f313b360dd26832b8443efa9b89966269ad6b1e3aa59a18e790406b29a55e19c10212fc3068dc476e48cee6e9 + languageName: node + linkType: hard + "nopt@npm:^5.0.0": version: 5.0.0 resolution: "nopt@npm:5.0.0" @@ -13795,7 +13932,9 @@ __metadata: "@grpc/grpc-js": "npm:^1.14.1" "@grpc/proto-loader": "npm:^0.8.0" "@liaoliaots/nestjs-redis": "npm:^9" + "@lottiefiles/dotlottie-web": "npm:^0.72.1" "@modelcontextprotocol/sdk": "npm:^1.29.0" + "@napi-rs/canvas": "npm:^1.0.0" "@nestjs/axios": "npm:^3.0.2" "@nestjs/bullmq": "npm:^11.0.2" "@nestjs/cli": "npm:^11.0.0" @@ -13864,6 +14003,7 @@ __metadata: mustache: "npm:^4.2.0" nestjs-pino: "npm:^4.1.0" node-cache: "npm:5.1.2" + node-webpmux: "npm:^3.2.1" oxlint: "npm:^1.13.0" passport: "npm:^0.7.0" passport-headerapikey: "npm:^1.2.2"