@builderbot/provider-baileys 1.4.2-alpha.2 → 1.4.2-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bailey.d.ts +19 -2
- package/dist/bailey.d.ts.map +1 -1
- package/dist/index.cjs +1012 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +1006 -10
- package/dist/lidCache.d.ts +569 -0
- package/dist/lidCache.d.ts.map +1 -0
- package/dist/type.d.ts +13 -0
- package/dist/type.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -4,10 +4,10 @@ import require$$0$3 from 'constants';
|
|
|
4
4
|
import require$$0$4 from 'stream';
|
|
5
5
|
import require$$1 from 'util';
|
|
6
6
|
import require$$5 from 'assert';
|
|
7
|
-
import require$$1$1, { join, basename, resolve } from 'path';
|
|
7
|
+
import require$$1$1, { join, dirname, basename, resolve } from 'path';
|
|
8
8
|
import require$$0$6 from 'zlib';
|
|
9
9
|
import { Console } from 'console';
|
|
10
|
-
import {
|
|
10
|
+
import { unlink, mkdir, writeFile, stat as stat$2, access, readFile, readdir } from 'fs/promises';
|
|
11
11
|
import require$$1$2 from 'events';
|
|
12
12
|
import require$$2$2, { tmpdir } from 'os';
|
|
13
13
|
import require$$0$a from 'node:os';
|
|
@@ -33676,6 +33676,963 @@ function requireDist () {
|
|
|
33676
33676
|
|
|
33677
33677
|
var distExports = requireDist();
|
|
33678
33678
|
|
|
33679
|
+
/**
|
|
33680
|
+
* @fileoverview LID (Local Identifier) Cache for WhatsApp Baileys Provider
|
|
33681
|
+
*
|
|
33682
|
+
* This module provides a caching layer for mapping WhatsApp Local Identifiers (LIDs)
|
|
33683
|
+
* to Phone Numbers (PNs). LIDs are privacy-preserving identifiers used by WhatsApp
|
|
33684
|
+
* that don't reveal the user's actual phone number.
|
|
33685
|
+
*
|
|
33686
|
+
* ## Architecture
|
|
33687
|
+
*
|
|
33688
|
+
* The cache uses a hybrid memory+file approach:
|
|
33689
|
+
* - **Hot path**: In-memory lookups via NodeCache (O(1), ~1μs)
|
|
33690
|
+
* - **Persistence**: JSON file for cross-restart durability
|
|
33691
|
+
* - **Strategy**: Write-through to memory, async flush to disk every 30s
|
|
33692
|
+
*
|
|
33693
|
+
* ## Key Features
|
|
33694
|
+
*
|
|
33695
|
+
* - **Zero-config**: Works out of the box with sensible defaults
|
|
33696
|
+
* - **Device suffix normalization**: `123:45@lid` and `123:99@lid` resolve to same entry
|
|
33697
|
+
* - **Phone number normalization**: Accepts `+123 456-7890`, `1234567890@c.us`, etc.
|
|
33698
|
+
* - **Security**: File permissions 0o600, PII masking in logs
|
|
33699
|
+
* - **Resilience**: Corrupted files auto-rebuild, flush failures logged
|
|
33700
|
+
*
|
|
33701
|
+
* ## Usage
|
|
33702
|
+
*
|
|
33703
|
+
* ```typescript
|
|
33704
|
+
* // Default: Hybrid (memory + file)
|
|
33705
|
+
* const cache = new HybridLidCache('my-bot', 86400 * 7) // 7 day TTL
|
|
33706
|
+
* await cache.ready()
|
|
33707
|
+
*
|
|
33708
|
+
* await cache.set('123456789:45@lid', '+34 691 015 468')
|
|
33709
|
+
* const pn = await cache.get('123456789:99@lid') // Returns '34691015468@s.whatsapp.net'
|
|
33710
|
+
*
|
|
33711
|
+
* await cache.close() // Persists to disk
|
|
33712
|
+
* ```
|
|
33713
|
+
*
|
|
33714
|
+
* @module lidCache
|
|
33715
|
+
* @author BuilderBot Team
|
|
33716
|
+
* @since 1.4.2
|
|
33717
|
+
*/
|
|
33718
|
+
// =============================================================================
|
|
33719
|
+
// CONSTANTS
|
|
33720
|
+
// =============================================================================
|
|
33721
|
+
/** File format version for migrations */
|
|
33722
|
+
const CACHE_FILE_VERSION = 1;
|
|
33723
|
+
/** Default TTL: 7 days (seconds) */
|
|
33724
|
+
const DEFAULT_TTL_SECONDS = 86400 * 7;
|
|
33725
|
+
/** Auto-flush interval: 30 seconds (milliseconds) */
|
|
33726
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 30000;
|
|
33727
|
+
/** Compact file when exceeds 10 MB */
|
|
33728
|
+
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
|
|
33729
|
+
/** Compact when more than 10k entries */
|
|
33730
|
+
const COMPACT_AT_ENTRIES = 10000;
|
|
33731
|
+
/** Disable persistence after 10 consecutive flush failures */
|
|
33732
|
+
const MAX_FLUSH_FAILURES = 10;
|
|
33733
|
+
/** Unix file permissions: owner read/write only (0o600) */
|
|
33734
|
+
const FILE_PERMISSIONS = 0o600;
|
|
33735
|
+
/**
|
|
33736
|
+
* Helper to brand a string as LidJid (runtime check).
|
|
33737
|
+
* Returns null if the string is not a valid LID.
|
|
33738
|
+
*/
|
|
33739
|
+
function asLidJid(value) {
|
|
33740
|
+
return isValidLid(value) ? normalizeLid(value) : null;
|
|
33741
|
+
}
|
|
33742
|
+
/**
|
|
33743
|
+
* Helper to brand a string as PnJid (runtime check).
|
|
33744
|
+
* Returns null if the string is not a valid phone number JID.
|
|
33745
|
+
*/
|
|
33746
|
+
function asPnJid(value) {
|
|
33747
|
+
if (!isValidPn(value))
|
|
33748
|
+
return null;
|
|
33749
|
+
const normalized = normalizePn(value);
|
|
33750
|
+
return normalized ? normalized : null;
|
|
33751
|
+
}
|
|
33752
|
+
// =============================================================================
|
|
33753
|
+
// TYPE GUARDS
|
|
33754
|
+
// =============================================================================
|
|
33755
|
+
/**
|
|
33756
|
+
* Validates if a value is a valid LID string.
|
|
33757
|
+
*
|
|
33758
|
+
* A valid LID:
|
|
33759
|
+
* - Is a string
|
|
33760
|
+
* - Contains `@lid` suffix
|
|
33761
|
+
* - Has minimum length of 5 (e.g., `1@lid`)
|
|
33762
|
+
*
|
|
33763
|
+
* @param value - Value to validate
|
|
33764
|
+
* @returns Type predicate: true if value is a valid LID string
|
|
33765
|
+
*
|
|
33766
|
+
* @example
|
|
33767
|
+
* ```typescript
|
|
33768
|
+
* isValidLid('123456789@lid') // true
|
|
33769
|
+
* isValidLid('123456789:45@lid') // true
|
|
33770
|
+
* isValidLid('123@c.us') // false
|
|
33771
|
+
* isValidLid('') // false
|
|
33772
|
+
* isValidLid(null) // false
|
|
33773
|
+
* ```
|
|
33774
|
+
*/
|
|
33775
|
+
function isValidLid(value) {
|
|
33776
|
+
return typeof value === 'string' && value.length >= 5 && value.includes('@lid');
|
|
33777
|
+
}
|
|
33778
|
+
/**
|
|
33779
|
+
* Validates if a value is a valid phone number string.
|
|
33780
|
+
*
|
|
33781
|
+
* Accepts:
|
|
33782
|
+
* - Bare digits: `1234567890`
|
|
33783
|
+
* - International: `+1234567890`
|
|
33784
|
+
* - Formatted: `+1 (555) 123-4567`
|
|
33785
|
+
* - WhatsApp JIDs: `123@s.whatsapp.net`, `123@c.us`
|
|
33786
|
+
*
|
|
33787
|
+
* @param value - Value to validate
|
|
33788
|
+
* @returns true if value contains at least one digit or is already a JID
|
|
33789
|
+
*/
|
|
33790
|
+
function isValidPn(value) {
|
|
33791
|
+
if (typeof value !== 'string')
|
|
33792
|
+
return false;
|
|
33793
|
+
if (value.length === 0)
|
|
33794
|
+
return false;
|
|
33795
|
+
// Accept if already has WhatsApp JID format
|
|
33796
|
+
if (value.includes('@s.whatsapp.net') || value.includes('@c.us'))
|
|
33797
|
+
return true;
|
|
33798
|
+
// Accept if contains any digit (will be cleaned during normalization)
|
|
33799
|
+
const hasDigits = /\d/.test(value);
|
|
33800
|
+
return hasDigits;
|
|
33801
|
+
}
|
|
33802
|
+
/**
|
|
33803
|
+
* Validates if a value matches the CacheEntry interface.
|
|
33804
|
+
*
|
|
33805
|
+
* @internal
|
|
33806
|
+
*/
|
|
33807
|
+
function isCacheEntry(value) {
|
|
33808
|
+
if (typeof value !== 'object' || value === null)
|
|
33809
|
+
return false;
|
|
33810
|
+
const entry = value;
|
|
33811
|
+
return typeof entry.pn === 'string' && typeof entry.ts === 'number' && !isNaN(entry.ts);
|
|
33812
|
+
}
|
|
33813
|
+
/**
|
|
33814
|
+
* Validates if a value matches the CacheFileData interface.
|
|
33815
|
+
*
|
|
33816
|
+
* @internal
|
|
33817
|
+
*/
|
|
33818
|
+
function isCacheFileData(value) {
|
|
33819
|
+
if (typeof value !== 'object' || value === null)
|
|
33820
|
+
return false;
|
|
33821
|
+
const data = value;
|
|
33822
|
+
return typeof data.version === 'number' && typeof data.entries === 'object' && data.entries !== null;
|
|
33823
|
+
}
|
|
33824
|
+
// =============================================================================
|
|
33825
|
+
// UTILITY FUNCTIONS
|
|
33826
|
+
// =============================================================================
|
|
33827
|
+
/**
|
|
33828
|
+
* Normalizes a LID by removing the device suffix.
|
|
33829
|
+
*
|
|
33830
|
+
* WhatsApp sends LIDs with device identifiers (e.g., `:45`, `:99`) that vary
|
|
33831
|
+
* based on the user's device. The same contact will have different suffixes
|
|
33832
|
+
* on phone vs web vs tablet. This function strips the suffix for consistent
|
|
33833
|
+
* caching.
|
|
33834
|
+
*
|
|
33835
|
+
* @param lid - LID to normalize (e.g., '123456789:45@lid')
|
|
33836
|
+
* @returns Normalized LID (e.g., '123456789@lid'), or original if invalid
|
|
33837
|
+
*
|
|
33838
|
+
* @example
|
|
33839
|
+
* ```typescript
|
|
33840
|
+
* normalizeLid('123456789:45@lid') // '123456789@lid'
|
|
33841
|
+
* normalizeLid('123456789:99@lid') // '123456789@lid'
|
|
33842
|
+
* normalizeLid('123456789@lid') // '123456789@lid' (no change)
|
|
33843
|
+
* normalizeLid('123456789@c.us') // '123456789@c.us' (no change, not a LID)
|
|
33844
|
+
* normalizeLid('invalid') // 'invalid' (no change, invalid)
|
|
33845
|
+
* ```
|
|
33846
|
+
*/
|
|
33847
|
+
function normalizeLid(lid) {
|
|
33848
|
+
if (!isValidLid(lid))
|
|
33849
|
+
return lid;
|
|
33850
|
+
// Remove :digits before @lid suffix only
|
|
33851
|
+
return lid.replace(/:\d+(?=@lid$)/, '');
|
|
33852
|
+
}
|
|
33853
|
+
/**
|
|
33854
|
+
* Sanitizes a session name for safe filesystem usage.
|
|
33855
|
+
*
|
|
33856
|
+
* Prevents path traversal and invalid filename characters.
|
|
33857
|
+
*
|
|
33858
|
+
* @internal
|
|
33859
|
+
* @param name - Raw session name
|
|
33860
|
+
* @returns Sanitized name safe for use in filenames
|
|
33861
|
+
*/
|
|
33862
|
+
function sanitizeSessionName(name) {
|
|
33863
|
+
// Replace dangerous characters for filenames
|
|
33864
|
+
return name.replace(/[\\/:"*?<>|]/g, '_').replace(/\.{2,}/g, '_');
|
|
33865
|
+
}
|
|
33866
|
+
/**
|
|
33867
|
+
* Normalizes a phone number to consistent `@s.whatsapp.net` format.
|
|
33868
|
+
*
|
|
33869
|
+
* Handles multiple input formats commonly encountered from WhatsApp:
|
|
33870
|
+
* - Bare digits: `1234567890` → `1234567890@s.whatsapp.net`
|
|
33871
|
+
* - International: `+1234567890` → `1234567890@s.whatsapp.net`
|
|
33872
|
+
* - Spaced: `123 456 7890` → `1234567890@s.whatsapp.net`
|
|
33873
|
+
* - Formatted: `+1 (555) 123-4567` → `15551234567@s.whatsapp.net`
|
|
33874
|
+
* - Legacy: `123@c.us` → `123@s.whatsapp.net`
|
|
33875
|
+
* - Already normalized: `123@s.whatsapp.net` → `123@s.whatsapp.net` (no change)
|
|
33876
|
+
*
|
|
33877
|
+
* @param pn - Phone number in any format
|
|
33878
|
+
* @returns Normalized phone number, or original if cannot be normalized
|
|
33879
|
+
*
|
|
33880
|
+
* @example
|
|
33881
|
+
* ```typescript
|
|
33882
|
+
* normalizePn('34691015468') // '34691015468@s.whatsapp.net'
|
|
33883
|
+
* normalizePn('+34691015468') // '34691015468@s.whatsapp.net'
|
|
33884
|
+
* normalizePn('34 691 015 468') // '34691015468@s.whatsapp.net'
|
|
33885
|
+
* normalizePn('+1 (555) 123-4567') // '15551234567@s.whatsapp.net'
|
|
33886
|
+
* normalizePn('34691015468@c.us') // '34691015468@s.whatsapp.net'
|
|
33887
|
+
* normalizePn('34691015468@s.whatsapp.net') // '34691015468@s.whatsapp.net'
|
|
33888
|
+
* normalizePn('not-a-number') // 'not-a-number' (unchanged)
|
|
33889
|
+
* ```
|
|
33890
|
+
*/
|
|
33891
|
+
function normalizePn(pn) {
|
|
33892
|
+
if (!isValidPn(pn))
|
|
33893
|
+
return pn;
|
|
33894
|
+
// Already in WhatsApp JID format
|
|
33895
|
+
if (pn.includes('@s.whatsapp.net'))
|
|
33896
|
+
return pn;
|
|
33897
|
+
// Legacy format conversion
|
|
33898
|
+
if (pn.includes('@c.us')) {
|
|
33899
|
+
const digits = pn.replace('@c.us', '');
|
|
33900
|
+
if (/^\d+$/.test(digits))
|
|
33901
|
+
return `${digits}@s.whatsapp.net`;
|
|
33902
|
+
}
|
|
33903
|
+
// Clean common formatting: +, spaces, dashes, dots, parentheses
|
|
33904
|
+
const cleaned = pn.replace(/^\+/, '').replace(/[\s\-\.\(\)]/g, '');
|
|
33905
|
+
// Validate we now have only digits
|
|
33906
|
+
if (/^\d+$/.test(cleaned)) {
|
|
33907
|
+
return `${cleaned}@s.whatsapp.net`;
|
|
33908
|
+
}
|
|
33909
|
+
// Cannot normalize, return original
|
|
33910
|
+
return pn;
|
|
33911
|
+
}
|
|
33912
|
+
/**
|
|
33913
|
+
* Masks a LID for safe logging (PII protection).
|
|
33914
|
+
*
|
|
33915
|
+
* Replaces middle characters with `***` to prevent logging full identifiers
|
|
33916
|
+
* while keeping enough for debugging.
|
|
33917
|
+
*
|
|
33918
|
+
* @internal
|
|
33919
|
+
* @param lid - LID to mask
|
|
33920
|
+
* @returns Masked LID (e.g., '123456789@lid' → '123***@lid')
|
|
33921
|
+
*/
|
|
33922
|
+
function maskLid(lid) {
|
|
33923
|
+
if (lid.length < 8)
|
|
33924
|
+
return '***@lid';
|
|
33925
|
+
return lid.slice(0, 3) + '***' + lid.slice(lid.indexOf('@'));
|
|
33926
|
+
}
|
|
33927
|
+
// =============================================================================
|
|
33928
|
+
// HYBRID CACHE (Memory + File)
|
|
33929
|
+
// =============================================================================
|
|
33930
|
+
/**
|
|
33931
|
+
* Hybrid LID cache implementation with memory hot-path and file persistence.
|
|
33932
|
+
*
|
|
33933
|
+
* This is the recommended production implementation, providing:
|
|
33934
|
+
* - **Speed**: O(1) in-memory lookups via NodeCache (~1μs)
|
|
33935
|
+
* - **Durability**: Automatic persistence to JSON file every 30s
|
|
33936
|
+
* - **Resilience**: Survives process restarts, handles corrupted files gracefully
|
|
33937
|
+
* - **Security**: File permissions 0o600 (owner read/write only)
|
|
33938
|
+
*
|
|
33939
|
+
* ## File Location
|
|
33940
|
+
*
|
|
33941
|
+
* Files are stored at: `{cwd}/{sessionName}_sessions/lid-cache.json`
|
|
33942
|
+
*
|
|
33943
|
+
* The session name is sanitized to prevent path traversal attacks.
|
|
33944
|
+
*
|
|
33945
|
+
* ## Configuration
|
|
33946
|
+
*
|
|
33947
|
+
* | Option | Default | Description |
|
|
33948
|
+
* |--------|---------|-------------|
|
|
33949
|
+
* | `sessionName` | required | Unique name for this bot instance |
|
|
33950
|
+
* | `ttlSeconds` | 604800 (7 days) | Time-to-live for cache entries |
|
|
33951
|
+
* | `basePath` | `process.cwd()` | Directory for session files |
|
|
33952
|
+
* | `logger` | `console` | Logger for operational events |
|
|
33953
|
+
*
|
|
33954
|
+
* @example
|
|
33955
|
+
* ```typescript
|
|
33956
|
+
* // Basic usage
|
|
33957
|
+
* const cache = new HybridLidCache('my-bot')
|
|
33958
|
+
* await cache.ready()
|
|
33959
|
+
*
|
|
33960
|
+
* // Custom TTL and logger
|
|
33961
|
+
* const cache = new HybridLidCache('my-bot', 86400 * 30, undefined, winstonLogger)
|
|
33962
|
+
*
|
|
33963
|
+
* // Custom base path
|
|
33964
|
+
* const cache = new HybridLidCache('my-bot', 86400, '/var/lib/bot')
|
|
33965
|
+
* ```
|
|
33966
|
+
*/
|
|
33967
|
+
class HybridLidCache {
|
|
33968
|
+
/**
|
|
33969
|
+
* Creates a new HybridLidCache instance.
|
|
33970
|
+
*
|
|
33971
|
+
* @param sessionName - Unique identifier for this cache instance (used in filename)
|
|
33972
|
+
* @param ttlSeconds - Time-to-live for cache entries (default: 7 days)
|
|
33973
|
+
* @param basePath - Base directory for session files (default: process.cwd())
|
|
33974
|
+
* @param logger - Logger instance for operational events (default: console)
|
|
33975
|
+
*
|
|
33976
|
+
* @throws Error if sessionName is empty or not a string
|
|
33977
|
+
* @throws Error if ttlSeconds is less than 60
|
|
33978
|
+
*
|
|
33979
|
+
* @remarks
|
|
33980
|
+
* The constructor starts async file loading and sets up periodic auto-flush.
|
|
33981
|
+
* Use {@link ready()} to wait for initial load completion before operations
|
|
33982
|
+
* that require data from previous runs.
|
|
33983
|
+
*/
|
|
33984
|
+
constructor(sessionName, ttlSeconds = DEFAULT_TTL_SECONDS, basePath, logger) {
|
|
33985
|
+
/** True if memory has unwritten changes */
|
|
33986
|
+
this.dirty = false;
|
|
33987
|
+
/** True if flushToDisk() is currently running (prevents concurrent flushes) */
|
|
33988
|
+
this.flushing = false;
|
|
33989
|
+
/** Consecutive flush failure count (disables persistence after threshold) */
|
|
33990
|
+
this.consecutiveFlushFailures = 0;
|
|
33991
|
+
/** True after close() has been called */
|
|
33992
|
+
this.isClosed = false;
|
|
33993
|
+
// Validate required arguments
|
|
33994
|
+
if (!sessionName || typeof sessionName !== 'string') {
|
|
33995
|
+
throw new Error('sessionName is required and must be a string');
|
|
33996
|
+
}
|
|
33997
|
+
if (ttlSeconds < 60) {
|
|
33998
|
+
throw new Error('ttlSeconds must be at least 60 (1 minute)');
|
|
33999
|
+
}
|
|
34000
|
+
this.ttlSeconds = ttlSeconds;
|
|
34001
|
+
this.logger = logger || console;
|
|
34002
|
+
const sanitizedSession = sanitizeSessionName(sessionName);
|
|
34003
|
+
const cwd = basePath || process.cwd();
|
|
34004
|
+
this.filePath = join(cwd, `${sanitizedSession}_sessions`, 'lid-cache.json');
|
|
34005
|
+
// Initialize in-memory cache with TTL
|
|
34006
|
+
this.memory = new NodeCache({
|
|
34007
|
+
stdTTL: ttlSeconds,
|
|
34008
|
+
useClones: false, // Performance: don't clone objects
|
|
34009
|
+
deleteOnExpire: true, // Auto-cleanup expired entries
|
|
34010
|
+
});
|
|
34011
|
+
// Start async file load (tracked to prevent race conditions)
|
|
34012
|
+
this.loadPromise = this.loadFromDisk();
|
|
34013
|
+
// Setup periodic auto-flush (every 30s)
|
|
34014
|
+
this.flushInterval = setInterval(() => {
|
|
34015
|
+
if (!this.isClosed) {
|
|
34016
|
+
this.flushToDisk().catch((err) => {
|
|
34017
|
+
this.logger.error('[LID Cache] Periodic flush failed:', err);
|
|
34018
|
+
});
|
|
34019
|
+
}
|
|
34020
|
+
}, DEFAULT_FLUSH_INTERVAL_MS);
|
|
34021
|
+
// Unref interval so it doesn't block process exit (important for tests)
|
|
34022
|
+
if (this.flushInterval.unref) {
|
|
34023
|
+
this.flushInterval.unref();
|
|
34024
|
+
}
|
|
34025
|
+
}
|
|
34026
|
+
/**
|
|
34027
|
+
* Waits for the initial file load to complete.
|
|
34028
|
+
*
|
|
34029
|
+
* Use this method before operations that depend on data from previous runs:
|
|
34030
|
+
* - Before checking if a LID exists from a previous session
|
|
34031
|
+
* - In tests to ensure deterministic state
|
|
34032
|
+
* - During cache warming procedures
|
|
34033
|
+
*
|
|
34034
|
+
* @returns Promise that resolves when initial load completes
|
|
34035
|
+
*
|
|
34036
|
+
* @example
|
|
34037
|
+
* ```typescript
|
|
34038
|
+
* const cache = new HybridLidCache('my-bot')
|
|
34039
|
+
* await cache.ready() // Wait for any existing data to load
|
|
34040
|
+
* const pn = await cache.get('existing@lid') // Now safe to access
|
|
34041
|
+
* ```
|
|
34042
|
+
*/
|
|
34043
|
+
async ready() {
|
|
34044
|
+
await this.loadPromise;
|
|
34045
|
+
}
|
|
34046
|
+
/**
|
|
34047
|
+
* Retrieves the phone number for a given LID.
|
|
34048
|
+
*
|
|
34049
|
+
* @param lid - WhatsApp Local Identifier (e.g., '123456789:45@lid')
|
|
34050
|
+
* @returns Phone number in format '1234567890@s.whatsapp.net', or null if:
|
|
34051
|
+
* - Cache is closed
|
|
34052
|
+
* - LID is invalid (doesn't match `*@lid` pattern)
|
|
34053
|
+
* - LID not found in cache
|
|
34054
|
+
* - Entry has expired
|
|
34055
|
+
*/
|
|
34056
|
+
async get(lid) {
|
|
34057
|
+
if (this.isClosed)
|
|
34058
|
+
return null;
|
|
34059
|
+
if (!isValidLid(lid))
|
|
34060
|
+
return null;
|
|
34061
|
+
const normalized = normalizeLid(lid);
|
|
34062
|
+
const value = this.memory.get(normalized);
|
|
34063
|
+
if (!value)
|
|
34064
|
+
return null;
|
|
34065
|
+
// Refresh LRU timestamp (extends implicit TTL)
|
|
34066
|
+
this.memory.set(normalized, value);
|
|
34067
|
+
return value;
|
|
34068
|
+
}
|
|
34069
|
+
/**
|
|
34070
|
+
* Stores a LID → phone number mapping.
|
|
34071
|
+
*
|
|
34072
|
+
* Both the LID and phone number are normalized before storage:
|
|
34073
|
+
* - LID: Device suffix removed (`123:45@lid` → `123@lid`)
|
|
34074
|
+
* - PN: Formatted to `123@s.whatsapp.net`
|
|
34075
|
+
*
|
|
34076
|
+
* @param lid - WhatsApp Local Identifier
|
|
34077
|
+
* @param pn - Phone number in any accepted format
|
|
34078
|
+
* @returns Promise that resolves immediately (memory write is synchronous)
|
|
34079
|
+
*
|
|
34080
|
+
* @remarks
|
|
34081
|
+
* - Invalid inputs are silently rejected (no throw)
|
|
34082
|
+
* - The `dirty` flag is set, triggering async flush to disk within 30s
|
|
34083
|
+
* - If the cache is closed, this is a no-op
|
|
34084
|
+
*/
|
|
34085
|
+
async set(lid, pn) {
|
|
34086
|
+
if (this.isClosed)
|
|
34087
|
+
return;
|
|
34088
|
+
if (!isValidLid(lid)) {
|
|
34089
|
+
this.logger.debug?.('[LID Cache] Rejected invalid LID:', maskLid(String(lid)));
|
|
34090
|
+
return;
|
|
34091
|
+
}
|
|
34092
|
+
if (!isValidPn(pn)) {
|
|
34093
|
+
this.logger.debug?.('[LID Cache] Rejected invalid PN:', pn);
|
|
34094
|
+
return;
|
|
34095
|
+
}
|
|
34096
|
+
const normalizedLid = normalizeLid(lid);
|
|
34097
|
+
const normalizedPn = normalizePn(pn);
|
|
34098
|
+
this.memory.set(normalizedLid, normalizedPn);
|
|
34099
|
+
this.dirty = true;
|
|
34100
|
+
}
|
|
34101
|
+
/**
|
|
34102
|
+
* Checks if a LID exists in the cache.
|
|
34103
|
+
*
|
|
34104
|
+
* @param lid - WhatsApp Local Identifier
|
|
34105
|
+
* @returns true if:
|
|
34106
|
+
* - LID is valid
|
|
34107
|
+
* - Cache is open
|
|
34108
|
+
* - Entry exists and hasn't expired
|
|
34109
|
+
*
|
|
34110
|
+
* @example
|
|
34111
|
+
* ```typescript
|
|
34112
|
+
* if (await cache.has('123@lid')) {
|
|
34113
|
+
* const pn = await cache.get('123@lid')
|
|
34114
|
+
* // ...
|
|
34115
|
+
* }
|
|
34116
|
+
* ```
|
|
34117
|
+
*/
|
|
34118
|
+
async has(lid) {
|
|
34119
|
+
if (this.isClosed)
|
|
34120
|
+
return false;
|
|
34121
|
+
if (!isValidLid(lid))
|
|
34122
|
+
return false;
|
|
34123
|
+
const normalized = normalizeLid(lid);
|
|
34124
|
+
return this.memory.has(normalized);
|
|
34125
|
+
}
|
|
34126
|
+
/**
|
|
34127
|
+
* Clears all entries from the cache.
|
|
34128
|
+
*
|
|
34129
|
+
* @remarks
|
|
34130
|
+
* - Immediately clears memory
|
|
34131
|
+
* - Triggers synchronous flush to disk (empty file)
|
|
34132
|
+
* - Logs the operation at info level
|
|
34133
|
+
*/
|
|
34134
|
+
async clear() {
|
|
34135
|
+
if (this.isClosed)
|
|
34136
|
+
return;
|
|
34137
|
+
this.memory.flushAll();
|
|
34138
|
+
this.dirty = true;
|
|
34139
|
+
this.logger.info?.('[LID Cache] Cache cleared');
|
|
34140
|
+
await this.flushToDisk();
|
|
34141
|
+
}
|
|
34142
|
+
/**
|
|
34143
|
+
* Closes the cache, releasing resources and triggering final persistence.
|
|
34144
|
+
*
|
|
34145
|
+
* This method:
|
|
34146
|
+
* 1. Stops the auto-flush interval
|
|
34147
|
+
* 2. Waits for any in-progress file load
|
|
34148
|
+
* 3. Performs a final flush to disk
|
|
34149
|
+
* 4. Closes the NodeCache instance
|
|
34150
|
+
* 5. Marks the cache as closed
|
|
34151
|
+
*
|
|
34152
|
+
* @returns Promise that resolves when cleanup is complete
|
|
34153
|
+
*
|
|
34154
|
+
* @remarks
|
|
34155
|
+
* - After close(), all operations return null/void
|
|
34156
|
+
* - Safe to call multiple times (subsequent calls are no-ops)
|
|
34157
|
+
* - Flush errors are logged but don't throw
|
|
34158
|
+
*/
|
|
34159
|
+
async close() {
|
|
34160
|
+
if (this.isClosed)
|
|
34161
|
+
return;
|
|
34162
|
+
if (this.flushInterval) {
|
|
34163
|
+
clearInterval(this.flushInterval);
|
|
34164
|
+
this.flushInterval = undefined;
|
|
34165
|
+
}
|
|
34166
|
+
// Wait for initial load to complete if still in progress
|
|
34167
|
+
try {
|
|
34168
|
+
await this.loadPromise;
|
|
34169
|
+
}
|
|
34170
|
+
catch {
|
|
34171
|
+
// Ignore load errors during close
|
|
34172
|
+
}
|
|
34173
|
+
// Final flush attempt
|
|
34174
|
+
try {
|
|
34175
|
+
await this.flushToDisk();
|
|
34176
|
+
}
|
|
34177
|
+
catch (err) {
|
|
34178
|
+
this.logger.error('[LID Cache] Final flush failed on close:', err);
|
|
34179
|
+
}
|
|
34180
|
+
this.isClosed = true;
|
|
34181
|
+
this.memory.close();
|
|
34182
|
+
this.logger.info?.('[LID Cache] Closed');
|
|
34183
|
+
}
|
|
34184
|
+
/**
|
|
34185
|
+
* Forces compaction of the cache file by rewriting it with only valid entries.
|
|
34186
|
+
*
|
|
34187
|
+
* This removes any expired entries that might still be in the file
|
|
34188
|
+
* (since NodeCache auto-expiry only removes from memory, not disk).
|
|
34189
|
+
*
|
|
34190
|
+
* @returns Promise that resolves when compaction completes
|
|
34191
|
+
*
|
|
34192
|
+
* @remarks
|
|
34193
|
+
* - If the cache is empty, the file is deleted instead
|
|
34194
|
+
* - Automatically called when file exceeds 10MB or 10k entries
|
|
34195
|
+
*/
|
|
34196
|
+
async compact() {
|
|
34197
|
+
if (this.isClosed)
|
|
34198
|
+
return;
|
|
34199
|
+
const keys = this.memory.keys();
|
|
34200
|
+
if (keys.length === 0) {
|
|
34201
|
+
// Empty cache - delete file
|
|
34202
|
+
try {
|
|
34203
|
+
await unlink(this.filePath);
|
|
34204
|
+
this.dirty = false;
|
|
34205
|
+
this.logger.info?.('[LID Cache] Empty cache file removed');
|
|
34206
|
+
}
|
|
34207
|
+
catch {
|
|
34208
|
+
// File may not exist
|
|
34209
|
+
}
|
|
34210
|
+
return;
|
|
34211
|
+
}
|
|
34212
|
+
// Force full rewrite
|
|
34213
|
+
this.dirty = true;
|
|
34214
|
+
await this.flushToDisk();
|
|
34215
|
+
this.logger.info?.('[LID Cache] Compacted', { entries: keys.length });
|
|
34216
|
+
}
|
|
34217
|
+
/**
|
|
34218
|
+
* Persists the current cache state to disk.
|
|
34219
|
+
*
|
|
34220
|
+
* @internal
|
|
34221
|
+
* @remarks
|
|
34222
|
+
* - Concurrent calls are deduplicated (only one flush runs at a time)
|
|
34223
|
+
* - No-op if nothing has changed since last flush (`dirty` flag check)
|
|
34224
|
+
* - File is written with 0o600 permissions (owner read/write only)
|
|
34225
|
+
* - After 10 consecutive failures, persistence is disabled
|
|
34226
|
+
*/
|
|
34227
|
+
async flushToDisk() {
|
|
34228
|
+
// Prevent concurrent flushes
|
|
34229
|
+
if (this.flushing)
|
|
34230
|
+
return;
|
|
34231
|
+
if (!this.dirty)
|
|
34232
|
+
return;
|
|
34233
|
+
this.flushing = true;
|
|
34234
|
+
try {
|
|
34235
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
34236
|
+
const keys = this.memory.keys();
|
|
34237
|
+
// Compact if entry count exceeds threshold
|
|
34238
|
+
if (keys.length > COMPACT_AT_ENTRIES) {
|
|
34239
|
+
await this.compact();
|
|
34240
|
+
}
|
|
34241
|
+
const entries = {};
|
|
34242
|
+
const now = Date.now();
|
|
34243
|
+
for (const key of keys) {
|
|
34244
|
+
const value = this.memory.get(key);
|
|
34245
|
+
if (!value)
|
|
34246
|
+
continue;
|
|
34247
|
+
entries[key] = {
|
|
34248
|
+
pn: value,
|
|
34249
|
+
ts: now,
|
|
34250
|
+
};
|
|
34251
|
+
}
|
|
34252
|
+
const data = {
|
|
34253
|
+
version: CACHE_FILE_VERSION,
|
|
34254
|
+
entries,
|
|
34255
|
+
};
|
|
34256
|
+
// Write with secure permissions
|
|
34257
|
+
await writeFile(this.filePath, JSON.stringify(data, null, 2), {
|
|
34258
|
+
encoding: 'utf-8',
|
|
34259
|
+
mode: FILE_PERMISSIONS,
|
|
34260
|
+
});
|
|
34261
|
+
this.dirty = false;
|
|
34262
|
+
this.consecutiveFlushFailures = 0; // Reset on success
|
|
34263
|
+
// Check file size and compact if needed
|
|
34264
|
+
await this.checkAndCompactIfNeeded();
|
|
34265
|
+
}
|
|
34266
|
+
catch (err) {
|
|
34267
|
+
this.consecutiveFlushFailures++;
|
|
34268
|
+
if (this.consecutiveFlushFailures >= MAX_FLUSH_FAILURES) {
|
|
34269
|
+
this.logger.error(`[LID Cache] Flush failed ${MAX_FLUSH_FAILURES} times, disabling persistence. ` +
|
|
34270
|
+
'Cache will work in-memory only until restart.', { error: err, filePath: this.filePath });
|
|
34271
|
+
this.dirty = false; // Stop trying
|
|
34272
|
+
}
|
|
34273
|
+
else {
|
|
34274
|
+
this.logger.warn(`[LID Cache] Flush failed (${this.consecutiveFlushFailures}/${MAX_FLUSH_FAILURES}):`, err);
|
|
34275
|
+
}
|
|
34276
|
+
throw err; // Re-throw for caller awareness
|
|
34277
|
+
}
|
|
34278
|
+
finally {
|
|
34279
|
+
this.flushing = false;
|
|
34280
|
+
}
|
|
34281
|
+
}
|
|
34282
|
+
/**
|
|
34283
|
+
* Checks file size and triggers compaction if exceeds threshold.
|
|
34284
|
+
*
|
|
34285
|
+
* @internal
|
|
34286
|
+
*/
|
|
34287
|
+
async checkAndCompactIfNeeded() {
|
|
34288
|
+
try {
|
|
34289
|
+
const stats = await stat$2(this.filePath);
|
|
34290
|
+
if (stats.size > MAX_FILE_SIZE_BYTES) {
|
|
34291
|
+
this.logger.warn(`[LID Cache] File size ${stats.size} bytes exceeds threshold, compacting...`);
|
|
34292
|
+
await this.compact();
|
|
34293
|
+
}
|
|
34294
|
+
}
|
|
34295
|
+
catch {
|
|
34296
|
+
// File may not exist
|
|
34297
|
+
}
|
|
34298
|
+
}
|
|
34299
|
+
/**
|
|
34300
|
+
* Loads cache data from disk on startup.
|
|
34301
|
+
*
|
|
34302
|
+
* @internal
|
|
34303
|
+
* @remarks
|
|
34304
|
+
* - Silently handles missing file (first run)
|
|
34305
|
+
* - Automatically removes and recovers from corrupted files
|
|
34306
|
+
* - Validates TTL on each entry (entries older than TTL are skipped)
|
|
34307
|
+
*/
|
|
34308
|
+
async loadFromDisk() {
|
|
34309
|
+
try {
|
|
34310
|
+
await access(this.filePath);
|
|
34311
|
+
}
|
|
34312
|
+
catch {
|
|
34313
|
+
// File doesn't exist - clean start
|
|
34314
|
+
return;
|
|
34315
|
+
}
|
|
34316
|
+
let data;
|
|
34317
|
+
try {
|
|
34318
|
+
const raw = await readFile(this.filePath, 'utf-8');
|
|
34319
|
+
data = JSON.parse(raw);
|
|
34320
|
+
}
|
|
34321
|
+
catch (err) {
|
|
34322
|
+
// Corrupted file - remove and start fresh
|
|
34323
|
+
try {
|
|
34324
|
+
await unlink(this.filePath);
|
|
34325
|
+
}
|
|
34326
|
+
catch {
|
|
34327
|
+
// ignore unlink errors
|
|
34328
|
+
}
|
|
34329
|
+
this.logger.error('[LID Cache] Corrupted cache file removed, starting fresh:', err);
|
|
34330
|
+
return;
|
|
34331
|
+
}
|
|
34332
|
+
// Validate file structure
|
|
34333
|
+
if (!isCacheFileData(data)) {
|
|
34334
|
+
this.logger.warn('[LID Cache] Invalid cache file format, starting fresh');
|
|
34335
|
+
return;
|
|
34336
|
+
}
|
|
34337
|
+
// Load valid, non-expired entries
|
|
34338
|
+
let loadedCount = 0;
|
|
34339
|
+
let expiredCount = 0;
|
|
34340
|
+
const now = Date.now();
|
|
34341
|
+
const ttlMs = this.ttlSeconds * 1000;
|
|
34342
|
+
for (const [key, entry] of Object.entries(data.entries)) {
|
|
34343
|
+
if (!isCacheEntry(entry))
|
|
34344
|
+
continue;
|
|
34345
|
+
// Check TTL from stored timestamp
|
|
34346
|
+
const age = now - entry.ts;
|
|
34347
|
+
if (age >= ttlMs) {
|
|
34348
|
+
expiredCount++;
|
|
34349
|
+
continue;
|
|
34350
|
+
}
|
|
34351
|
+
this.memory.set(key, entry.pn);
|
|
34352
|
+
loadedCount++;
|
|
34353
|
+
}
|
|
34354
|
+
if (loadedCount > 0 || expiredCount > 0) {
|
|
34355
|
+
this.logger.info('[LID Cache] Loaded entries from disk', {
|
|
34356
|
+
valid: loadedCount,
|
|
34357
|
+
expired: expiredCount,
|
|
34358
|
+
file: this.filePath,
|
|
34359
|
+
});
|
|
34360
|
+
}
|
|
34361
|
+
}
|
|
34362
|
+
/**
|
|
34363
|
+
* Returns cache statistics for monitoring.
|
|
34364
|
+
*
|
|
34365
|
+
* @returns Object with:
|
|
34366
|
+
* - `keys`: Number of entries currently in memory
|
|
34367
|
+
* - `hits`: Cache hit count (from NodeCache stats)
|
|
34368
|
+
* - `misses`: Cache miss count (from NodeCache stats)
|
|
34369
|
+
*
|
|
34370
|
+
* @example
|
|
34371
|
+
* ```typescript
|
|
34372
|
+
* const stats = cache.getStats()
|
|
34373
|
+
* console.log(`Cache: ${stats.keys} entries, ${stats.hits} hits, ${stats.misses} misses`)
|
|
34374
|
+
* // → Cache: 1523 entries, 4500 hits, 123 misses
|
|
34375
|
+
* ```
|
|
34376
|
+
*/
|
|
34377
|
+
getStats() {
|
|
34378
|
+
return {
|
|
34379
|
+
keys: this.memory.keys().length,
|
|
34380
|
+
hits: this.memory.getStats()?.hits || 0,
|
|
34381
|
+
misses: this.memory.getStats()?.misses || 0,
|
|
34382
|
+
};
|
|
34383
|
+
}
|
|
34384
|
+
}
|
|
34385
|
+
// =============================================================================
|
|
34386
|
+
// MEMORY-ONLY CACHE (Testing)
|
|
34387
|
+
// =============================================================================
|
|
34388
|
+
/**
|
|
34389
|
+
* Memory-only LID cache implementation for testing.
|
|
34390
|
+
*
|
|
34391
|
+
* This implementation provides the same {@link LidCache} interface but without
|
|
34392
|
+
* file persistence. Useful for:
|
|
34393
|
+
* - Unit tests (no file I/O, no cleanup needed)
|
|
34394
|
+
* - Ephemeral caches that don't need durability
|
|
34395
|
+
* - Reducing disk wear in high-frequency test scenarios
|
|
34396
|
+
*
|
|
34397
|
+
* ## Differences from HybridLidCache
|
|
34398
|
+
*
|
|
34399
|
+
* | Feature | MemoryLidCache | HybridLidCache |
|
|
34400
|
+
* |---------|---------------|----------------|
|
|
34401
|
+
* | Persistence | ❌ None | ✅ JSON file |
|
|
34402
|
+
* | `ready()` | Optional | Recommended |
|
|
34403
|
+
* | `close()` | No-op | Flushes to disk |
|
|
34404
|
+
* | `compact()` | No-op | Rewrites file |
|
|
34405
|
+
* | Cross-restart | Data lost | Data preserved |
|
|
34406
|
+
*
|
|
34407
|
+
* @example
|
|
34408
|
+
* ```typescript
|
|
34409
|
+
* // Testing scenario
|
|
34410
|
+
* const cache = new MemoryLidCache(3600) // 1 hour TTL
|
|
34411
|
+
* await cache.set('123@lid', '456@s.whatsapp.net')
|
|
34412
|
+
* expect(await cache.get('123@lid')).toBe('456@s.whatsapp.net')
|
|
34413
|
+
* // No cleanup needed - data is ephemeral
|
|
34414
|
+
* ```
|
|
34415
|
+
*/
|
|
34416
|
+
class MemoryLidCache {
|
|
34417
|
+
/**
|
|
34418
|
+
* Creates a new MemoryLidCache instance.
|
|
34419
|
+
*
|
|
34420
|
+
* @param ttlSeconds - Time-to-live for cache entries (default: 7 days)
|
|
34421
|
+
*/
|
|
34422
|
+
constructor(ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
34423
|
+
this.memory = new NodeCache({
|
|
34424
|
+
stdTTL: ttlSeconds,
|
|
34425
|
+
useClones: false,
|
|
34426
|
+
});
|
|
34427
|
+
}
|
|
34428
|
+
/**
|
|
34429
|
+
* Retrieves the phone number for a given LID.
|
|
34430
|
+
*
|
|
34431
|
+
* @param lid - WhatsApp Local Identifier
|
|
34432
|
+
* @returns Phone number or null if not found/invalid
|
|
34433
|
+
*/
|
|
34434
|
+
async get(lid) {
|
|
34435
|
+
if (!isValidLid(lid))
|
|
34436
|
+
return null;
|
|
34437
|
+
const normalized = normalizeLid(lid);
|
|
34438
|
+
return this.memory.get(normalized) || null;
|
|
34439
|
+
}
|
|
34440
|
+
/**
|
|
34441
|
+
* Stores a LID → phone number mapping.
|
|
34442
|
+
*
|
|
34443
|
+
* @param lid - WhatsApp Local Identifier
|
|
34444
|
+
* @param pn - Phone number in any format
|
|
34445
|
+
*/
|
|
34446
|
+
async set(lid, pn) {
|
|
34447
|
+
if (!isValidLid(lid))
|
|
34448
|
+
return;
|
|
34449
|
+
if (!isValidPn(pn))
|
|
34450
|
+
return;
|
|
34451
|
+
const normalized = normalizeLid(lid);
|
|
34452
|
+
this.memory.set(normalized, pn);
|
|
34453
|
+
}
|
|
34454
|
+
/**
|
|
34455
|
+
* Checks if a LID exists in the cache.
|
|
34456
|
+
*
|
|
34457
|
+
* @param lid - WhatsApp Local Identifier
|
|
34458
|
+
* @returns true if entry exists and hasn't expired
|
|
34459
|
+
*/
|
|
34460
|
+
async has(lid) {
|
|
34461
|
+
if (!isValidLid(lid))
|
|
34462
|
+
return false;
|
|
34463
|
+
const normalized = normalizeLid(lid);
|
|
34464
|
+
return this.memory.has(normalized);
|
|
34465
|
+
}
|
|
34466
|
+
/**
|
|
34467
|
+
* Clears all entries from the cache.
|
|
34468
|
+
*/
|
|
34469
|
+
async clear() {
|
|
34470
|
+
this.memory.flushAll();
|
|
34471
|
+
}
|
|
34472
|
+
/**
|
|
34473
|
+
* Closes the cache. For MemoryLidCache, this is a no-op.
|
|
34474
|
+
*
|
|
34475
|
+
* @remarks The NodeCache instance is not closed to allow continued testing.
|
|
34476
|
+
* If you need to free memory, use `clear()` before `close()`.
|
|
34477
|
+
*/
|
|
34478
|
+
async close() {
|
|
34479
|
+
// No-op for memory cache - data is already ephemeral
|
|
34480
|
+
}
|
|
34481
|
+
}
|
|
34482
|
+
/**
|
|
34483
|
+
* Factory function to create a LidCache instance based on configuration.
|
|
34484
|
+
*
|
|
34485
|
+
* This factory centralizes cache creation logic, making it reusable across
|
|
34486
|
+
* the codebase and easier to test/maintain.
|
|
34487
|
+
*
|
|
34488
|
+
* @param options - Factory configuration options
|
|
34489
|
+
* @returns Configured LidCache instance
|
|
34490
|
+
*
|
|
34491
|
+
* @example
|
|
34492
|
+
* ```typescript
|
|
34493
|
+
* // Default: HybridLidCache (file + memory)
|
|
34494
|
+
* const cache = createLidCache({ sessionName: 'my-bot' })
|
|
34495
|
+
*
|
|
34496
|
+
* // Memory-only (testing)
|
|
34497
|
+
* const cache = createLidCache({ strategy: 'memory', ttlSeconds: 3600 })
|
|
34498
|
+
*
|
|
34499
|
+
* // Custom implementation
|
|
34500
|
+
* const cache = createLidCache({ strategy: new RedisLidCache() })
|
|
34501
|
+
* ```
|
|
34502
|
+
*/
|
|
34503
|
+
function createLidCache(options = {}) {
|
|
34504
|
+
const { strategy = 'file', sessionName = 'default', ttlSeconds = DEFAULT_TTL_SECONDS, basePath, logger } = options;
|
|
34505
|
+
// If custom instance passed, use it directly
|
|
34506
|
+
if (strategy && typeof strategy === 'object') {
|
|
34507
|
+
return strategy;
|
|
34508
|
+
}
|
|
34509
|
+
// Memory-only strategy
|
|
34510
|
+
if (strategy === 'memory') {
|
|
34511
|
+
return new MemoryLidCache(ttlSeconds);
|
|
34512
|
+
}
|
|
34513
|
+
// Default: Hybrid (file + memory)
|
|
34514
|
+
return new HybridLidCache(sessionName, ttlSeconds, basePath, logger);
|
|
34515
|
+
}
|
|
34516
|
+
/**
|
|
34517
|
+
* Type guard to check if a value is a valid MessageContext.
|
|
34518
|
+
*
|
|
34519
|
+
* @param value - Value to check
|
|
34520
|
+
* @returns true if the value matches MessageContext structure
|
|
34521
|
+
*/
|
|
34522
|
+
function isMessageContext(value) {
|
|
34523
|
+
if (typeof value !== 'object' || value === null)
|
|
34524
|
+
return false;
|
|
34525
|
+
const ctx = value;
|
|
34526
|
+
// If key exists, it must be an object (MessageContextKey)
|
|
34527
|
+
if ('key' in ctx && ctx.key !== undefined) {
|
|
34528
|
+
if (typeof ctx.key !== 'object' || ctx.key === null)
|
|
34529
|
+
return false;
|
|
34530
|
+
}
|
|
34531
|
+
return true;
|
|
34532
|
+
}
|
|
34533
|
+
/**
|
|
34534
|
+
* Extracts and caches LID → PN mapping from an incoming message.
|
|
34535
|
+
*
|
|
34536
|
+
* This utility function inspects the message context to find LID/PN pairs
|
|
34537
|
+
* and stores them in the provided cache for future lookups.
|
|
34538
|
+
*
|
|
34539
|
+
* @param cache - LidCache instance to store the mapping
|
|
34540
|
+
* @param messageCtx - Baileys message context (WAMessage)
|
|
34541
|
+
* @returns Promise that resolves when caching is complete (or silently fails)
|
|
34542
|
+
*
|
|
34543
|
+
* @example
|
|
34544
|
+
* ```typescript
|
|
34545
|
+
* // In message handler:
|
|
34546
|
+
* for (const message of messages) {
|
|
34547
|
+
* await extractAndCacheLidFromMessage(lidCache, message)
|
|
34548
|
+
* }
|
|
34549
|
+
* ```
|
|
34550
|
+
*/
|
|
34551
|
+
async function extractAndCacheLidFromMessage(cache, messageCtx) {
|
|
34552
|
+
try {
|
|
34553
|
+
const key = messageCtx?.key;
|
|
34554
|
+
if (!key)
|
|
34555
|
+
return;
|
|
34556
|
+
const isGroup = key.remoteJid?.includes('@g.us');
|
|
34557
|
+
if (isGroup) {
|
|
34558
|
+
// Groups: participant has the LID, participantAlt has the PN
|
|
34559
|
+
if (key.participant?.includes('@lid') && key.participantAlt) {
|
|
34560
|
+
await cache.set(key.participant, key.participantAlt);
|
|
34561
|
+
}
|
|
34562
|
+
}
|
|
34563
|
+
else {
|
|
34564
|
+
// DMs: remoteJid has the LID
|
|
34565
|
+
if (key.remoteJid?.includes('@lid')) {
|
|
34566
|
+
// Priority: remoteJidAlt > senderPn > participantPn
|
|
34567
|
+
const pn = key.remoteJidAlt || key.senderPn || key.participantPn;
|
|
34568
|
+
if (pn) {
|
|
34569
|
+
await cache.set(key.remoteJid, pn);
|
|
34570
|
+
}
|
|
34571
|
+
}
|
|
34572
|
+
}
|
|
34573
|
+
}
|
|
34574
|
+
catch {
|
|
34575
|
+
// Silent failure - don't block message processing
|
|
34576
|
+
}
|
|
34577
|
+
}
|
|
34578
|
+
/**
|
|
34579
|
+
* Resolves a LID to a phone number using cache-first strategy.
|
|
34580
|
+
*
|
|
34581
|
+
* This function implements the resolution chain:
|
|
34582
|
+
* 1. Check cache first (O(1), ~1μs)
|
|
34583
|
+
* 2. If miss, call fallback resolver (e.g., Baileys lidMapping)
|
|
34584
|
+
* 3. If resolved, store in cache for future lookups
|
|
34585
|
+
*
|
|
34586
|
+
* @param cache - LidCache instance for storing/retrieving mappings
|
|
34587
|
+
* @param fallbackResolver - Async function to resolve LID when not in cache
|
|
34588
|
+
* @param logger - Logger for operational events (optional)
|
|
34589
|
+
* @param lid - WhatsApp Local Identifier to resolve
|
|
34590
|
+
* @returns Phone number in format '1234567890@s.whatsapp.net', or null
|
|
34591
|
+
*
|
|
34592
|
+
* @example
|
|
34593
|
+
* ```typescript
|
|
34594
|
+
* const pn = await resolveLidToPn(
|
|
34595
|
+
* lidCache,
|
|
34596
|
+
* (lid) => baileysSignalRepo.lidMapping.getPNForLID(lid),
|
|
34597
|
+
* console,
|
|
34598
|
+
* '123456789@lid' as LidJid
|
|
34599
|
+
* )
|
|
34600
|
+
* ```
|
|
34601
|
+
*/
|
|
34602
|
+
async function resolveLidToPn(cache, fallbackResolver, logger, lid) {
|
|
34603
|
+
try {
|
|
34604
|
+
// Validate/normalize the LID
|
|
34605
|
+
const normalizedLid = asLidJid(typeof lid === 'string' ? lid : lid);
|
|
34606
|
+
if (!normalizedLid) {
|
|
34607
|
+
logger?.error?.('[LID Cache] Invalid LID format:', lid);
|
|
34608
|
+
return null;
|
|
34609
|
+
}
|
|
34610
|
+
// 1. Check cache first (fast, O(1))
|
|
34611
|
+
const cached = await cache.get(normalizedLid);
|
|
34612
|
+
if (cached) {
|
|
34613
|
+
logger?.log?.(`[LID Cache] Hit: ${normalizedLid} -> ${cached}`);
|
|
34614
|
+
return cached;
|
|
34615
|
+
}
|
|
34616
|
+
// 2. Fallback to provided resolver
|
|
34617
|
+
const resolved = await fallbackResolver(normalizedLid);
|
|
34618
|
+
if (resolved) {
|
|
34619
|
+
// Validate the resolved value is a valid PN
|
|
34620
|
+
const normalizedPn = asPnJid(resolved);
|
|
34621
|
+
if (normalizedPn) {
|
|
34622
|
+
// Store in cache for next time
|
|
34623
|
+
await cache.set(normalizedLid, normalizedPn);
|
|
34624
|
+
logger?.log?.(`[LID Cache] Resolved: ${normalizedLid} -> ${normalizedPn}`);
|
|
34625
|
+
return normalizedPn;
|
|
34626
|
+
}
|
|
34627
|
+
}
|
|
34628
|
+
return null;
|
|
34629
|
+
}
|
|
34630
|
+
catch (e) {
|
|
34631
|
+
logger?.error?.('[LID Cache] Error resolving LID:', e);
|
|
34632
|
+
return null;
|
|
34633
|
+
}
|
|
34634
|
+
}
|
|
34635
|
+
|
|
33679
34636
|
const keepFiles = ['creds.json', 'baileys_store.json', 'app-state-sync', 'session'];
|
|
33680
34637
|
/**
|
|
33681
34638
|
* @alpha
|
|
@@ -33948,6 +34905,8 @@ class BaileysProvider extends ProviderClass {
|
|
|
33948
34905
|
if (messageCtx?.key?.id && messageCtx?.message) {
|
|
33949
34906
|
this.messageCache?.set(`msg:${messageCtx.key.id}`, messageCtx.message);
|
|
33950
34907
|
}
|
|
34908
|
+
// Aprender mapeo LID→PN desde mensaje entrante (async, no bloqueante)
|
|
34909
|
+
this.cacheLidFromMessage(messageCtx).catch(() => { });
|
|
33951
34910
|
if (messageCtx?.messageStubParameters?.length &&
|
|
33952
34911
|
messageCtx.messageStubParameters[0].includes('absent'))
|
|
33953
34912
|
continue;
|
|
@@ -34197,17 +35156,18 @@ class BaileysProvider extends ProviderClass {
|
|
|
34197
35156
|
}
|
|
34198
35157
|
};
|
|
34199
35158
|
/**
|
|
34200
|
-
* Obtener número de teléfono (PN) para un LID (Local Identifier)
|
|
35159
|
+
* Obtener número de teléfono (PN) para un LID (Local Identifier).
|
|
35160
|
+
* Delegates to the standalone utility with Baileys lidMapping as fallback.
|
|
35161
|
+
*
|
|
34201
35162
|
* @param lid - JID con formato '16424005304394@lid'
|
|
35163
|
+
* @returns Phone number en formato '1234567890@s.whatsapp.net', o null si no se resuelve
|
|
34202
35164
|
*/
|
|
34203
35165
|
this.getPNForLID = async (lid) => {
|
|
34204
|
-
|
|
34205
|
-
|
|
34206
|
-
|
|
34207
|
-
catch (e) {
|
|
34208
|
-
this.logger.log(`[${new Date().toISOString()}] Error getting PN for LID:`, e);
|
|
35166
|
+
// Normalize to branded type if valid
|
|
35167
|
+
const lidJid = typeof lid === 'string' ? asLidJid(lid) : lid;
|
|
35168
|
+
if (!lidJid)
|
|
34209
35169
|
return null;
|
|
34210
|
-
|
|
35170
|
+
return resolveLidToPn(this.lidCache, (id) => this.lidMapping?.getPNForLID?.(id) ?? Promise.resolve(null), this.logger, lidJid);
|
|
34211
35171
|
};
|
|
34212
35172
|
/**
|
|
34213
35173
|
* Normaliza un número entrante a un JID válido para envío.
|
|
@@ -34492,6 +35452,8 @@ class BaileysProvider extends ProviderClass {
|
|
|
34492
35452
|
errorOnMissing: false,
|
|
34493
35453
|
});
|
|
34494
35454
|
this.globalVendorArgs = { ...this.globalVendorArgs, ...args };
|
|
35455
|
+
// Initialize LID cache (hybrid file+memory or memory-only based on config)
|
|
35456
|
+
this.lidCache = this.initializeLidCache();
|
|
34495
35457
|
this.setupCleanupHandlers();
|
|
34496
35458
|
this.setupPeriodicCleanup();
|
|
34497
35459
|
}
|
|
@@ -34559,6 +35521,12 @@ class BaileysProvider extends ProviderClass {
|
|
|
34559
35521
|
this.messageCache.close();
|
|
34560
35522
|
this.messageCache = undefined;
|
|
34561
35523
|
}
|
|
35524
|
+
// Cerrar LID cache (flush final a disco)
|
|
35525
|
+
if (this.lidCache?.close) {
|
|
35526
|
+
this.lidCache.close().catch((err) => {
|
|
35527
|
+
this.logger.error(`[${new Date().toISOString()}] Error closing LID cache:`, err);
|
|
35528
|
+
});
|
|
35529
|
+
}
|
|
34562
35530
|
this.mapSet.clear();
|
|
34563
35531
|
this.idsDuplicates.length = 0;
|
|
34564
35532
|
if (this.logStream && typeof this.logStream.end === 'function') {
|
|
@@ -34584,6 +35552,34 @@ class BaileysProvider extends ProviderClass {
|
|
|
34584
35552
|
.get('/', this.indexHome);
|
|
34585
35553
|
}
|
|
34586
35554
|
afterHttpServerInit() { }
|
|
35555
|
+
// =============================================================================
|
|
35556
|
+
// LID CACHE INTEGRATION
|
|
35557
|
+
// =============================================================================
|
|
35558
|
+
/**
|
|
35559
|
+
* Inicializa el caché LID/PN usando el factory.
|
|
35560
|
+
* @returns Instancia de LidCache configurada según globalVendorArgs
|
|
35561
|
+
*/
|
|
35562
|
+
initializeLidCache() {
|
|
35563
|
+
return createLidCache({
|
|
35564
|
+
strategy: this.globalVendorArgs.lidCache,
|
|
35565
|
+
sessionName: this.globalVendorArgs.name,
|
|
35566
|
+
ttlSeconds: this.globalVendorArgs.lidCacheTtl,
|
|
35567
|
+
logger: this.logger,
|
|
35568
|
+
});
|
|
35569
|
+
}
|
|
35570
|
+
/**
|
|
35571
|
+
* Delegates to the standalone utility for caching LID→PN from messages.
|
|
35572
|
+
* This wrapper maintains the method signature for internal use while
|
|
35573
|
+
* leveraging the exported function for reusability.
|
|
35574
|
+
*/
|
|
35575
|
+
async cacheLidFromMessage(messageCtx) {
|
|
35576
|
+
// Type guard: ensure the message context has the expected structure
|
|
35577
|
+
if (!isMessageContext(messageCtx)) {
|
|
35578
|
+
this.logger.debug?.('Invalid message context for LID caching');
|
|
35579
|
+
return;
|
|
35580
|
+
}
|
|
35581
|
+
return extractAndCacheLidFromMessage(this.lidCache, messageCtx);
|
|
35582
|
+
}
|
|
34587
35583
|
/**
|
|
34588
35584
|
* Accede al lidMapping del signalRepository de Baileys.
|
|
34589
35585
|
*/
|
|
@@ -34642,4 +35638,4 @@ class BaileysProvider extends ProviderClass {
|
|
|
34642
35638
|
}
|
|
34643
35639
|
}
|
|
34644
35640
|
|
|
34645
|
-
export { BaileysProvider, baileyCleanNumber };
|
|
35641
|
+
export { BaileysProvider, HybridLidCache, MemoryLidCache, asLidJid, asPnJid, baileyCleanNumber, createLidCache, extractAndCacheLidFromMessage, isMessageContext, normalizeLid, resolveLidToPn };
|