-
Notifications
You must be signed in to change notification settings - Fork 117
Expand file tree
/
Copy pathcommon.ts
More file actions
150 lines (134 loc) · 5.04 KB
/
common.ts
File metadata and controls
150 lines (134 loc) · 5.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
* Common utility functions
*/
/**
* Delay for specified milliseconds
* @param ms Milliseconds
*/
export const delay = (ms: number): Promise<void> => {
return new Promise<void>((resolve) => setTimeout(resolve, ms))
}
/**
* Validate message content
* @param text Text content
* @param attachments Attachment list
* @returns Validation result: hasText, hasAttachments
* @throws Error when content is empty
*/
export function validateMessageContent(
text: string | undefined,
attachments: readonly string[] | undefined
): { hasText: boolean; hasAttachments: boolean } {
const hasText = Boolean(text && text.trim().length > 0)
const hasAttachments = Boolean(attachments && attachments.length > 0)
if (!hasText && !hasAttachments) {
throw new Error('Message must contain text or attachments')
}
return { hasText, hasAttachments }
}
/**
* Normalize chatId format by extracting the core identifier.
* - Group: `any;+;534ce85d...` -> `534ce85d...`
* - Legacy group: `iMessage;+;chat534ce85d...` -> `chat534ce85d...`
* - DM: `any;-;+1234567890` -> `+1234567890`
* - Already bare: returned as-is
*/
export function normalizeChatId(chatId: string): string {
if (chatId.includes(';')) {
const parts = chatId.split(';')
return parts[parts.length - 1] ?? chatId
}
return chatId
}
/**
* Check if a chatId represents a group chat (not a DM).
* Recognizes:
* - Modern macOS: `any;+;{guid}` or any `service;+;identifier`
* - Legacy: `iMessage;+;chat{guid}`
* - Bare GUID: `chat{guid}` (no semicolons, starts with "chat")
*/
export function isGroupChatId(chatId: string): boolean {
if (chatId.includes(';+;')) return true
if (chatId.startsWith('iMessage;+;chat')) return true
if (!chatId.includes(';') && chatId.startsWith('chat') && chatId.length > 10) return true
return false
}
/**
* Extract recipient from a service-prefixed DM chatId.
* Handles:
* - 3-part modern: `any;-;+1234567890` -> `+1234567890`
* - 2-part legacy: `iMessage;+1234567890` -> `+1234567890`
*/
export function extractRecipientFromChatId(chatId: string): string | null {
if (!chatId.includes(';')) return null
if (isGroupChatId(chatId)) return null
const parts = chatId.split(';')
// 3-part DM: service;-;address (e.g., any;-;+1234567890)
if (parts.length === 3 && parts[1] === '-') return parts[2] || null
// 2-part DM: service;address (e.g., iMessage;+1234567890)
if (parts.length === 2) return parts[1] || null
return null
}
/**
* Validate chatId format.
* Accepted forms:
* 1) Group: `service;+;guid` (e.g., `any;+;534ce85d...`)
* 2) Legacy group: `iMessage;+;chat{guid}`
* 3) Bare group GUID: `chat{guid}` or raw hex GUID (length >= 8)
* 4) DM: `service;-;address` (e.g., `any;-;+1234567890`)
* 5) Legacy DM: `service;address` (e.g., `iMessage;+1234567890`)
* @throws Error when chatId is invalid
*/
export function validateChatId(chatId: string): void {
if (!chatId || typeof chatId !== 'string') {
throw new Error('chatId must be a non-empty string')
}
if (chatId.includes(';')) {
const parts = chatId.split(';')
// Group format: service;+;guid (e.g., any;+;534ce85d...)
if (parts.length >= 3 && parts[1] === '+') {
const guidPart = parts.slice(2).join(';')
if (guidPart.length < 8) {
throw new Error('Invalid chatId format: GUID too short')
}
return
}
// DM format: service;-;address (e.g., any;-;+1234567890)
if (parts.length === 3 && parts[1] === '-') {
if (!parts[2]) {
throw new Error('Invalid chatId format: missing address')
}
return
}
// Legacy DM format: service;address (e.g., iMessage;+1234567890)
if (parts.length === 2) {
const service = parts[0] || ''
const address = parts[1] || ''
const allowedServices = new Set(['iMessage', 'SMS', 'RCS', 'any'])
if (!allowedServices.has(service) || !address) {
throw new Error('Invalid chatId format: expected "<service>;<address>" or group GUID')
}
return
}
throw new Error('Invalid chatId format: unrecognized semicolon pattern')
}
// No semicolons: bare GUID or chat-prefixed GUID
if (chatId.length < 8) {
throw new Error('Invalid chatId format: GUID too short')
}
}
/**
* Build a full Messages.app guid for a group chat using the discovered prefix.
* Strips any existing prefixes and reconstructs with the local format.
* @param rawChatId Chat identifier in any format
* @param discoveredPrefix The prefix discovered from chat.db at init (e.g., "any;+;" or "iMessage;+;chat")
*/
export function buildGroupChatGuid(rawChatId: string, discoveredPrefix: string): string {
let guid = rawChatId
if (guid.includes(';')) {
const parts = guid.split(';')
guid = parts[parts.length - 1] ?? guid
}
if (guid.startsWith('chat')) guid = guid.substring(4)
return `${discoveredPrefix}${guid}`
}