whatsapp-pi 1.0.22 → 1.0.24
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/README.md +30 -8
- package/package.json +1 -1
- package/src/models/whatsapp.types.ts +34 -0
- package/src/services/audio.service.ts +2 -2
- package/src/services/recents.service.ts +203 -0
- package/src/services/session.manager.ts +80 -15
- package/src/services/whatsapp.service.ts +64 -20
- package/src/ui/menu.handler.ts +335 -38
- package/whatsapp-pi.ts +312 -68
package/README.md
CHANGED
|
@@ -18,14 +18,22 @@ Pi is a powerful agentic AI coding assistant that operates in your terminal. Thi
|
|
|
18
18
|
- View ignored numbers (not in allow list) and add them when needed
|
|
19
19
|
- **Reliable Messaging**: Queue-based message sending with retry logic
|
|
20
20
|
- **TUI Integration**: Menu-driven interface for managing connections and contacts
|
|
21
|
+
- **Media Support**:
|
|
22
|
+
- **Vision Analysis**: Automatically forwards WhatsApp images to Pi for analysis.
|
|
23
|
+
- **Document Handling**: Downloads and stores documents (PDF, text) for agent access.
|
|
21
24
|
|
|
22
25
|
## Prerequisites
|
|
23
26
|
|
|
24
|
-
To enable audio features
|
|
27
|
+
To enable audio transcription features:
|
|
25
28
|
```bash
|
|
26
29
|
python -m pip install -U openai-whisper
|
|
27
30
|
```
|
|
28
31
|
|
|
32
|
+
To enable PDF reading capabilities (required for the agent to process documents):
|
|
33
|
+
- **Linux**: `sudo apt-get install poppler-utils`
|
|
34
|
+
- **macOS**: `brew install poppler`
|
|
35
|
+
- **Windows**: Install `poppler` (e.g., via Scoop) and add to PATH.
|
|
36
|
+
|
|
29
37
|
## Quick Start
|
|
30
38
|
|
|
31
39
|
1. Install the extension:
|
|
@@ -71,7 +79,7 @@ pi -e whatsapp-pi.ts --verbose
|
|
|
71
79
|
- `/whatsapp` - Open the WhatsApp management menu
|
|
72
80
|
|
|
73
81
|
### Main Menu Options
|
|
74
|
-
- **Connect WhatsApp** - Start WhatsApp connection
|
|
82
|
+
- **Connect / Reconnect WhatsApp** - Start WhatsApp connection using saved credentials when available; QR code appears only if pairing is required
|
|
75
83
|
- **Disconnect WhatsApp** - Stop WhatsApp connection
|
|
76
84
|
- **Logoff (Delete Session)** - Remove all credentials and session data
|
|
77
85
|
- **Allowed Numbers** - Manage contacts that can interact with Pi
|
|
@@ -79,8 +87,7 @@ pi -e whatsapp-pi.ts --verbose
|
|
|
79
87
|
|
|
80
88
|
### Allowed Numbers Management
|
|
81
89
|
- **Add Number** - Add a new contact to the allow list (format: +5511999999999)
|
|
82
|
-
- **
|
|
83
|
-
- **Clear All** - Remove all allowed numbers
|
|
90
|
+
- **Select a contact** - Open a submenu with **Send Message**, **Remove Number**, and **Back**
|
|
84
91
|
- **Back** - Return to main menu
|
|
85
92
|
|
|
86
93
|
### Blocked Numbers Management
|
|
@@ -108,6 +115,11 @@ See `specs/` directory for detailed feature documentation:
|
|
|
108
115
|
- `002-manual-whatsapp-connection/` - Connection management
|
|
109
116
|
- `003-whatsapp-messaging-refactor/` - Reliable messaging
|
|
110
117
|
- `004-blocked-numbers-management/` - Block list feature
|
|
118
|
+
- `005-verbose-mode-support/` - Logging and tracing
|
|
119
|
+
- `006-auto-connect-flag/` - Automatic connection support
|
|
120
|
+
- `007-image-recognition/` - Vision analysis integration
|
|
121
|
+
- `008-document-message-support/` - Document handling and storage
|
|
122
|
+
- `009-localize-system-messages/` - US English localization
|
|
111
123
|
|
|
112
124
|
## Development
|
|
113
125
|
|
|
@@ -116,7 +128,17 @@ Run tests:
|
|
|
116
128
|
npm test
|
|
117
129
|
```
|
|
118
130
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
## Implementation Notes
|
|
132
|
+
|
|
133
|
+
### Recent Feature Updates (2026-04)
|
|
134
|
+
|
|
135
|
+
- **Auto-Connect Support**: Use the `--whatsapp-pi-online` flag to automatically connect to WhatsApp on startup if you have a valid active session.
|
|
136
|
+
- **Vision Analysis**: Images sent via WhatsApp are automatically downloaded and forwarded to the Pi agent as base64, enabling vision-based interactions.
|
|
137
|
+
- **Document Message Support**:
|
|
138
|
+
- WhatsApp documents (PDFs, text files, etc.) are downloaded and saved to `./.pi-data/whatsapp/documents/`.
|
|
139
|
+
- The Pi agent receives a notification with the file path and metadata.
|
|
140
|
+
- **Full US-English Localization**: All user-facing menus, TUI notifications, console logs, and agent communication headers have been localized to `en-US` for a consistent experience.
|
|
141
|
+
- **Intelligent Message Filtering**:
|
|
142
|
+
- **Loop Prevention**: The bot automatically ignores any message ending with the `π` symbol, preventing infinite loops between instances.
|
|
143
|
+
- **Manual Interaction**: Users can now interact with the bot from their own WhatsApp account; the bot will process `fromMe` messages as long as they don't contain the bot's signature.
|
|
144
|
+
- **Storage Management**: All persistent data (auth state, documents, config) is centralized in the `.pi-data/` directory.
|
package/package.json
CHANGED
|
@@ -44,3 +44,37 @@ export class WhatsAppError extends Error {
|
|
|
44
44
|
export function validatePhoneNumber(number: string): boolean {
|
|
45
45
|
return /^\+[1-9]\d{1,14}$/.test(number);
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
export interface DocumentMetadata {
|
|
49
|
+
filename: string;
|
|
50
|
+
mimetype: string;
|
|
51
|
+
size: number;
|
|
52
|
+
savedPath: string;
|
|
53
|
+
timestamp: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type MessageDirection = 'incoming' | 'outgoing';
|
|
57
|
+
|
|
58
|
+
export interface RecentConversationMessage {
|
|
59
|
+
messageId: string;
|
|
60
|
+
senderNumber: string;
|
|
61
|
+
text: string;
|
|
62
|
+
direction: MessageDirection;
|
|
63
|
+
timestamp: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface RecentConversationSummary {
|
|
67
|
+
senderNumber: string;
|
|
68
|
+
senderName?: string;
|
|
69
|
+
lastMessagePreview: string;
|
|
70
|
+
lastMessageTime: number;
|
|
71
|
+
lastMessageDirection: MessageDirection;
|
|
72
|
+
messageCount: number;
|
|
73
|
+
isAllowed: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RecentsStore {
|
|
77
|
+
conversations: RecentConversationSummary[];
|
|
78
|
+
messagesBySender: Record<string, RecentConversationMessage[]>;
|
|
79
|
+
updatedAt: number;
|
|
80
|
+
}
|
|
@@ -45,10 +45,10 @@ export class AudioService {
|
|
|
45
45
|
return text.trim();
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
return '[
|
|
48
|
+
return '[Empty transcription]';
|
|
49
49
|
} catch (error) {
|
|
50
50
|
console.error('[AudioService] Transcription error:', error);
|
|
51
|
-
return `[
|
|
51
|
+
return `[Transcription error: ${error instanceof Error ? error.message : String(error)}]`;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
4
|
+
import type {
|
|
5
|
+
MessageDirection,
|
|
6
|
+
RecentConversationMessage,
|
|
7
|
+
RecentConversationSummary,
|
|
8
|
+
RecentsStore
|
|
9
|
+
} from '../models/whatsapp.types.js';
|
|
10
|
+
import { SessionManager } from './session.manager.js';
|
|
11
|
+
|
|
12
|
+
export interface RecentsMessageInput {
|
|
13
|
+
messageId: string;
|
|
14
|
+
senderNumber: string;
|
|
15
|
+
senderName?: string;
|
|
16
|
+
text: string;
|
|
17
|
+
direction: MessageDirection;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class RecentsService {
|
|
22
|
+
private readonly baseDir = join(homedir(), '.pi', 'whatsapp-pi');
|
|
23
|
+
private readonly dataDir = join(this.baseDir, 'recents');
|
|
24
|
+
private readonly storePath = join(this.dataDir, 'recents.json');
|
|
25
|
+
private store: RecentsStore = {
|
|
26
|
+
conversations: [],
|
|
27
|
+
messagesBySender: {},
|
|
28
|
+
updatedAt: Date.now()
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
constructor(private readonly sessionManager: SessionManager) {}
|
|
32
|
+
|
|
33
|
+
async ensureInitialized() {
|
|
34
|
+
await mkdir(this.dataDir, { recursive: true });
|
|
35
|
+
await this.loadStore();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async loadStore() {
|
|
39
|
+
try {
|
|
40
|
+
const content = await readFile(this.storePath, 'utf-8');
|
|
41
|
+
const parsed = JSON.parse(content) as Partial<RecentsStore>;
|
|
42
|
+
|
|
43
|
+
this.store = {
|
|
44
|
+
conversations: Array.isArray(parsed.conversations) ? parsed.conversations.slice(0, 20) : [],
|
|
45
|
+
messagesBySender: parsed.messagesBySender && typeof parsed.messagesBySender === 'object'
|
|
46
|
+
? this.normalizeMessagesMap(parsed.messagesBySender)
|
|
47
|
+
: {},
|
|
48
|
+
updatedAt: typeof parsed.updatedAt === 'number' ? parsed.updatedAt : Date.now()
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
this.rebuildConversationState();
|
|
52
|
+
} catch {
|
|
53
|
+
this.store = {
|
|
54
|
+
conversations: [],
|
|
55
|
+
messagesBySender: {},
|
|
56
|
+
updatedAt: Date.now()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private normalizeMessagesMap(messagesBySender: RecentsStore['messagesBySender']): RecentsStore['messagesBySender'] {
|
|
62
|
+
const normalized: RecentsStore['messagesBySender'] = {};
|
|
63
|
+
|
|
64
|
+
for (const [senderNumber, messages] of Object.entries(messagesBySender)) {
|
|
65
|
+
if (!Array.isArray(messages)) continue;
|
|
66
|
+
normalized[senderNumber] = messages
|
|
67
|
+
.filter((message): message is RecentConversationMessage => this.isValidMessage(message))
|
|
68
|
+
.map(message => ({ ...message, timestamp: this.normalizeTimestamp(message.timestamp) }))
|
|
69
|
+
.sort((left, right) => left.timestamp - right.timestamp)
|
|
70
|
+
.slice(-20);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return normalized;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private isValidMessage(message: unknown): message is RecentConversationMessage {
|
|
77
|
+
return Boolean(
|
|
78
|
+
message &&
|
|
79
|
+
typeof message === 'object' &&
|
|
80
|
+
typeof (message as RecentConversationMessage).messageId === 'string' &&
|
|
81
|
+
typeof (message as RecentConversationMessage).senderNumber === 'string' &&
|
|
82
|
+
typeof (message as RecentConversationMessage).text === 'string' &&
|
|
83
|
+
(message as RecentConversationMessage).text.trim().length > 0 &&
|
|
84
|
+
((message as RecentConversationMessage).direction === 'incoming' || (message as RecentConversationMessage).direction === 'outgoing') &&
|
|
85
|
+
typeof (message as RecentConversationMessage).timestamp === 'number'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private rebuildConversationState() {
|
|
90
|
+
const previousNames = new Map(
|
|
91
|
+
this.store.conversations.map(conversation => [conversation.senderNumber, conversation.senderName] as const)
|
|
92
|
+
);
|
|
93
|
+
const summaries = new Map<string, RecentConversationSummary>();
|
|
94
|
+
|
|
95
|
+
for (const [senderNumber, messages] of Object.entries(this.store.messagesBySender)) {
|
|
96
|
+
if (messages.length === 0) continue;
|
|
97
|
+
const lastMessage = messages[messages.length - 1];
|
|
98
|
+
summaries.set(senderNumber, {
|
|
99
|
+
senderNumber,
|
|
100
|
+
senderName: previousNames.get(senderNumber),
|
|
101
|
+
lastMessagePreview: this.buildPreview(lastMessage.text),
|
|
102
|
+
lastMessageTime: lastMessage.timestamp,
|
|
103
|
+
lastMessageDirection: lastMessage.direction,
|
|
104
|
+
messageCount: messages.length,
|
|
105
|
+
isAllowed: this.sessionManager.isAllowed(senderNumber)
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.store.conversations = Array.from(summaries.values())
|
|
110
|
+
.sort((left, right) => right.lastMessageTime - left.lastMessageTime)
|
|
111
|
+
.slice(0, 20);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private buildPreview(text: string): string {
|
|
115
|
+
const normalized = text.trim().replace(/\s+/g, ' ');
|
|
116
|
+
if (normalized.length <= 80) return normalized;
|
|
117
|
+
return `${normalized.slice(0, 77)}...`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async persistStore() {
|
|
121
|
+
this.store.updatedAt = Date.now();
|
|
122
|
+
await writeFile(this.storePath, JSON.stringify(this.store, null, 2));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private normalizeNumber(input: string): string {
|
|
126
|
+
const cleaned = input.replace(/@s\.whatsapp\.net$/, '');
|
|
127
|
+
if (cleaned.startsWith('+')) {
|
|
128
|
+
return cleaned;
|
|
129
|
+
}
|
|
130
|
+
if (/^\d+$/.test(cleaned)) {
|
|
131
|
+
return `+${cleaned}`;
|
|
132
|
+
}
|
|
133
|
+
return cleaned;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private normalizeTimestamp(timestamp: number): number {
|
|
137
|
+
return timestamp < 1_000_000_000_000 ? timestamp * 1000 : timestamp;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async recordMessage(input: RecentsMessageInput) {
|
|
141
|
+
const senderNumber = this.normalizeNumber(input.senderNumber);
|
|
142
|
+
if (!senderNumber) return;
|
|
143
|
+
|
|
144
|
+
const normalizedTimestamp = this.normalizeTimestamp(input.timestamp);
|
|
145
|
+
const normalizedText = input.text.trim().replace(/\s+/g, ' ');
|
|
146
|
+
if (!normalizedText) return;
|
|
147
|
+
|
|
148
|
+
const existing = this.store.messagesBySender[senderNumber] ?? [];
|
|
149
|
+
const nextMessage: RecentConversationMessage = {
|
|
150
|
+
messageId: input.messageId,
|
|
151
|
+
senderNumber,
|
|
152
|
+
text: normalizedText,
|
|
153
|
+
direction: input.direction,
|
|
154
|
+
timestamp: normalizedTimestamp
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const filtered = existing.filter(message => message.messageId !== nextMessage.messageId);
|
|
158
|
+
filtered.push(nextMessage);
|
|
159
|
+
|
|
160
|
+
this.store.messagesBySender[senderNumber] = filtered
|
|
161
|
+
.sort((left, right) => left.timestamp - right.timestamp)
|
|
162
|
+
.slice(-20);
|
|
163
|
+
|
|
164
|
+
const existingConversation = this.store.conversations.find(conversation => conversation.senderNumber === senderNumber);
|
|
165
|
+
const summary: RecentConversationSummary = {
|
|
166
|
+
senderNumber,
|
|
167
|
+
senderName: input.senderName ?? existingConversation?.senderName,
|
|
168
|
+
lastMessagePreview: this.buildPreview(input.text),
|
|
169
|
+
lastMessageTime: normalizedTimestamp,
|
|
170
|
+
lastMessageDirection: input.direction,
|
|
171
|
+
messageCount: this.store.messagesBySender[senderNumber].length,
|
|
172
|
+
isAllowed: this.sessionManager.isAllowed(senderNumber)
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
this.store.conversations = [
|
|
176
|
+
summary,
|
|
177
|
+
...this.store.conversations.filter(item => item.senderNumber !== senderNumber)
|
|
178
|
+
]
|
|
179
|
+
.sort((left, right) => right.lastMessageTime - left.lastMessageTime)
|
|
180
|
+
.slice(0, 20);
|
|
181
|
+
|
|
182
|
+
await this.persistStore();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async getRecentConversations(): Promise<RecentConversationSummary[]> {
|
|
186
|
+
this.rebuildConversationState();
|
|
187
|
+
return [...this.store.conversations];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async getConversationHistory(senderNumber: string): Promise<RecentConversationMessage[]> {
|
|
191
|
+
const normalizedNumber = this.normalizeNumber(senderNumber);
|
|
192
|
+
const messages = this.store.messagesBySender[normalizedNumber] ?? [];
|
|
193
|
+
return [...messages]
|
|
194
|
+
.map(message => ({ ...message, timestamp: this.normalizeTimestamp(message.timestamp) }))
|
|
195
|
+
.sort((left, right) => left.timestamp - right.timestamp)
|
|
196
|
+
.slice(-20);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async hasRecentConversations(): Promise<boolean> {
|
|
200
|
+
const conversations = await this.getRecentConversations();
|
|
201
|
+
return conversations.length > 0;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMultiFileAuthState } from '@whiskeysockets/baileys';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { readFile, writeFile, mkdir, rm } from 'fs/promises';
|
|
3
|
+
import { readFile, writeFile, mkdir, rm, readdir } from 'fs/promises';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { SessionStatus } from '../models/whatsapp.types.js';
|
|
6
6
|
|
|
@@ -12,21 +12,27 @@ export interface Contact {
|
|
|
12
12
|
export class SessionManager {
|
|
13
13
|
// Data is stored in the user's home directory to persist across updates
|
|
14
14
|
private readonly baseDir = join(homedir(), '.pi', 'whatsapp-pi');
|
|
15
|
-
private readonly
|
|
15
|
+
private readonly authStateDir = join(this.baseDir, 'auth');
|
|
16
16
|
private readonly configPath = join(this.baseDir, 'config.json');
|
|
17
17
|
|
|
18
18
|
private status: SessionStatus = 'logged-out';
|
|
19
19
|
private allowList: Contact[] = [];
|
|
20
20
|
private blockList: Contact[] = [];
|
|
21
21
|
private ignoredNumbers: Contact[] = [];
|
|
22
|
+
private hasAuthState = false;
|
|
22
23
|
private openaiKey: string = '';
|
|
23
24
|
private visionModel: string = 'gpt-4o';
|
|
24
25
|
|
|
26
|
+
private async ensureStorageDirectories() {
|
|
27
|
+
await mkdir(this.baseDir, { recursive: true });
|
|
28
|
+
await mkdir(this.authStateDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
public async ensureInitialized() {
|
|
26
32
|
try {
|
|
27
|
-
await
|
|
28
|
-
await mkdir(this.authDir, { recursive: true });
|
|
33
|
+
await this.ensureStorageDirectories();
|
|
29
34
|
await this.loadConfig();
|
|
35
|
+
await this.syncAuthStateFromDisk();
|
|
30
36
|
} catch (error) {}
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -54,6 +60,7 @@ export class SessionManager {
|
|
|
54
60
|
this.blockList = (config.blockList || []).map(cleanContact).filter(Boolean) as Contact[];
|
|
55
61
|
this.ignoredNumbers = (config.ignoredNumbers || []).map(cleanContact).filter(Boolean) as Contact[];
|
|
56
62
|
this.status = config.status || 'logged-out';
|
|
63
|
+
this.hasAuthState = Boolean(config.hasAuthState);
|
|
57
64
|
this.openaiKey = config.openaiKey || '';
|
|
58
65
|
this.visionModel = config.visionModel || 'gpt-4o';
|
|
59
66
|
} catch (error) {
|
|
@@ -68,6 +75,7 @@ export class SessionManager {
|
|
|
68
75
|
blockList: this.blockList,
|
|
69
76
|
ignoredNumbers: this.ignoredNumbers,
|
|
70
77
|
status: this.status,
|
|
78
|
+
hasAuthState: this.hasAuthState,
|
|
71
79
|
openaiKey: this.openaiKey,
|
|
72
80
|
visionModel: this.visionModel
|
|
73
81
|
};
|
|
@@ -81,6 +89,10 @@ export class SessionManager {
|
|
|
81
89
|
return this.allowList;
|
|
82
90
|
}
|
|
83
91
|
|
|
92
|
+
getAllowedContact(number: string): Contact | undefined {
|
|
93
|
+
return this.allowList.find(c => c.number === number);
|
|
94
|
+
}
|
|
95
|
+
|
|
84
96
|
getBlockList(): Contact[] {
|
|
85
97
|
return this.blockList;
|
|
86
98
|
}
|
|
@@ -106,12 +118,19 @@ export class SessionManager {
|
|
|
106
118
|
return;
|
|
107
119
|
}
|
|
108
120
|
|
|
109
|
-
|
|
121
|
+
const existing = this.allowList.find(c => c.number === cleanNumber);
|
|
122
|
+
if (!existing) {
|
|
110
123
|
this.allowList.push({ number: cleanNumber, name });
|
|
111
124
|
// Remove from blockList and ignoredNumbers if it was there
|
|
112
125
|
this.blockList = this.blockList.filter(c => c.number !== cleanNumber);
|
|
113
126
|
this.ignoredNumbers = this.ignoredNumbers.filter(c => c.number !== cleanNumber);
|
|
114
127
|
await this.saveConfig();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (name && !existing.name) {
|
|
132
|
+
existing.name = name;
|
|
133
|
+
await this.saveConfig();
|
|
115
134
|
}
|
|
116
135
|
}
|
|
117
136
|
|
|
@@ -120,8 +139,28 @@ export class SessionManager {
|
|
|
120
139
|
await this.saveConfig();
|
|
121
140
|
}
|
|
122
141
|
|
|
123
|
-
async
|
|
124
|
-
|
|
142
|
+
async setAllowedContactAlias(number: string, alias: string) {
|
|
143
|
+
const trimmedAlias = alias.trim();
|
|
144
|
+
if (!trimmedAlias) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const contact = this.getAllowedContact(number);
|
|
149
|
+
if (!contact) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
contact.name = trimmedAlias;
|
|
154
|
+
await this.saveConfig();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async removeAllowedContactAlias(number: string) {
|
|
158
|
+
const contact = this.getAllowedContact(number);
|
|
159
|
+
if (!contact || !contact.name) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
delete contact.name;
|
|
125
164
|
await this.saveConfig();
|
|
126
165
|
}
|
|
127
166
|
|
|
@@ -168,25 +207,51 @@ export class SessionManager {
|
|
|
168
207
|
|
|
169
208
|
public async isRegistered(): Promise<boolean> {
|
|
170
209
|
try {
|
|
171
|
-
const credsPah = join(this.
|
|
210
|
+
const credsPah = join(this.authStateDir, 'creds.json');
|
|
172
211
|
await readFile(credsPah);
|
|
212
|
+
this.hasAuthState = true;
|
|
173
213
|
return true;
|
|
174
214
|
} catch {
|
|
175
|
-
|
|
215
|
+
await this.syncAuthStateFromDisk();
|
|
216
|
+
return this.hasAuthState;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async markAuthStateAvailable() {
|
|
221
|
+
if (!this.hasAuthState) {
|
|
222
|
+
this.hasAuthState = true;
|
|
223
|
+
await this.saveConfig();
|
|
176
224
|
}
|
|
177
225
|
}
|
|
178
226
|
|
|
179
227
|
async getAuthState() {
|
|
180
|
-
|
|
228
|
+
await this.ensureStorageDirectories();
|
|
229
|
+
return await useMultiFileAuthState(this.authStateDir);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private async syncAuthStateFromDisk() {
|
|
233
|
+
try {
|
|
234
|
+
const entries = await readdir(this.authStateDir);
|
|
235
|
+
if (entries.length > 0) {
|
|
236
|
+
if (!this.hasAuthState) {
|
|
237
|
+
this.hasAuthState = true;
|
|
238
|
+
await this.saveConfig();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// Ignore missing directory / empty auth state
|
|
243
|
+
}
|
|
181
244
|
}
|
|
182
245
|
|
|
183
|
-
async
|
|
246
|
+
async deleteAuthState() {
|
|
184
247
|
try {
|
|
185
|
-
await rm(this.
|
|
248
|
+
await rm(this.authStateDir, { recursive: true, force: true });
|
|
249
|
+
await mkdir(this.authStateDir, { recursive: true });
|
|
186
250
|
this.status = 'logged-out';
|
|
251
|
+
this.hasAuthState = false;
|
|
187
252
|
await this.saveConfig();
|
|
188
253
|
} catch (error) {
|
|
189
|
-
console.error('Failed to
|
|
254
|
+
console.error('Failed to delete auth state:', error);
|
|
190
255
|
}
|
|
191
256
|
}
|
|
192
257
|
|
|
@@ -217,7 +282,7 @@ export class SessionManager {
|
|
|
217
282
|
await this.saveConfig();
|
|
218
283
|
}
|
|
219
284
|
|
|
220
|
-
|
|
221
|
-
return this.
|
|
285
|
+
getAuthStateDir(): string {
|
|
286
|
+
return this.authStateDir;
|
|
222
287
|
}
|
|
223
288
|
}
|