imessage-bot 0.1.0
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/LICENSE +21 -0
- package/README.md +251 -0
- package/dist/db.d.ts +33 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +170 -0
- package/dist/db.js.map +1 -0
- package/dist/find-chats.d.ts +2 -0
- package/dist/find-chats.d.ts.map +1 -0
- package/dist/find-chats.js +44 -0
- package/dist/find-chats.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/messenger.d.ts +8 -0
- package/dist/messenger.d.ts.map +1 -0
- package/dist/messenger.js +22 -0
- package/dist/messenger.js.map +1 -0
- package/dist/poller.d.ts +75 -0
- package/dist/poller.d.ts.map +1 -0
- package/dist/poller.js +120 -0
- package/dist/poller.js.map +1 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 imessage-bot contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# imessage-bot
|
|
2
|
+
|
|
3
|
+
> **Use at your own risk.** This project reads from your local iMessage database and sends messages via AppleScript. The author is not liable for any unintended messages sent, data accessed, or consequences arising from use of this software. Review the code before running it.
|
|
4
|
+
|
|
5
|
+
A Node.js toolkit for reading and responding to iMessages on macOS.
|
|
6
|
+
|
|
7
|
+
I originally built this to power a weight-tracking accountability bot for a friend group — members log their weight via iMessage commands, the bot parses them and stores the data. Friends wanted to use the polling layer for their own projects, so I extracted it into this standalone toolkit.
|
|
8
|
+
|
|
9
|
+
Poll any iMessage group chat or direct message, react to commands, and send replies — all from a TypeScript script running on your Mac.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- **macOS** (uses the local Messages database and AppleScript)
|
|
14
|
+
- **Node.js 18+**
|
|
15
|
+
- **Full Disk Access** granted to Terminal (or your IDE) — see below
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## ⚠️ Full Disk Access — Read This First
|
|
20
|
+
|
|
21
|
+
This is the most common setup issue. Without it, Node.js cannot read `~/Library/Messages/chat.db` and you'll get a permission error immediately.
|
|
22
|
+
|
|
23
|
+
### How to grant it
|
|
24
|
+
|
|
25
|
+
1. Open **System Settings → Privacy & Security → Full Disk Access**
|
|
26
|
+
2. Click the **+** button and add **Terminal.app** (located in `/Applications/Utilities/`)
|
|
27
|
+
3. Make sure the toggle next to Terminal is **on**
|
|
28
|
+
4. Fully quit and reopen Terminal
|
|
29
|
+
|
|
30
|
+
### Known UI quirk
|
|
31
|
+
|
|
32
|
+
On some macOS versions, after adding Terminal via the **+** button it may not appear visually in the list — but it has actually been granted. This is a known display glitch.
|
|
33
|
+
|
|
34
|
+
**To verify it actually worked**, run this in Terminal:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
sqlite3 ~/Library/Messages/chat.db ".tables"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- If you see a list of table names → Full Disk Access is working correctly.
|
|
41
|
+
- If you see `unable to open database file` → it was not granted. Try removing and re-adding Terminal, then restart your Mac.
|
|
42
|
+
|
|
43
|
+
> If you're running your bot from VS Code or another IDE instead of Terminal, you need to grant Full Disk Access to **that app** instead.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/yourusername/imessage-bot.git
|
|
49
|
+
cd imessage-bot
|
|
50
|
+
npm install
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### 1. Find your chat GUID
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm run find-chats
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This lists all your iMessage chats with their GUIDs. Copy the one you want.
|
|
62
|
+
|
|
63
|
+
### 2. Write your bot
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// my-bot.ts
|
|
67
|
+
import { createPoller } from './src/index.js';
|
|
68
|
+
|
|
69
|
+
const bot = createPoller({
|
|
70
|
+
chatGuid: 'iMessage;+;chat123456789', // paste your GUID here
|
|
71
|
+
onMessage: async ({ message, reply }) => {
|
|
72
|
+
if (message.text === '!ping') {
|
|
73
|
+
await reply('pong!');
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
bot.start();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx tsx my-bot.ts
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
That's it. Your bot is running.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## API
|
|
90
|
+
|
|
91
|
+
### `createPoller(options)`
|
|
92
|
+
|
|
93
|
+
The main entry point. Returns a `Poller` with `start()` and `stop()` methods.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { createPoller } from './src/index.js';
|
|
97
|
+
|
|
98
|
+
const bot = createPoller({
|
|
99
|
+
chatGuid: 'iMessage;+;chat123', // required
|
|
100
|
+
pollIntervalMs: 10_000, // default: 10 seconds
|
|
101
|
+
seedWeeksBack: 1, // how far back to look on first run (just for watermarking, not processing)
|
|
102
|
+
stateFile: '~/.my-bot-state.json', // where to persist the ROWID watermark
|
|
103
|
+
|
|
104
|
+
onReady: ({ chatGuid, stateFile }) => {
|
|
105
|
+
console.log(`Bot started, state at ${stateFile}`);
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
onMessage: async ({ message, reply, chatGuid }) => {
|
|
109
|
+
// message.text — the message text
|
|
110
|
+
// message.senderId — phone number (e.g. "+15551234567") or "Me"
|
|
111
|
+
// message.isFromMe — boolean
|
|
112
|
+
// message.date — JS timestamp in ms
|
|
113
|
+
// message.rowid — iMessage database row ID
|
|
114
|
+
// reply(text) — send a reply to the same chat
|
|
115
|
+
// chatGuid — the GUID of the chat
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
onError: (err) => {
|
|
119
|
+
console.error('Error:', err.message);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
bot.start();
|
|
124
|
+
// bot.stop(); // gracefully stops polling
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `sendMessage(chatGuid, text)`
|
|
128
|
+
|
|
129
|
+
Send a message to any chat directly, without the poller.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { sendMessage } from './src/index.js';
|
|
133
|
+
|
|
134
|
+
await sendMessage('iMessage;+;chat123', 'Hello from my bot!');
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `findChats(options?)`
|
|
138
|
+
|
|
139
|
+
List chats programmatically.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { findChats } from './src/index.js';
|
|
143
|
+
|
|
144
|
+
const all = findChats(); // all chats
|
|
145
|
+
const groups = findChats({ groupOnly: true, limit: 20 }); // group chats only
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Returns `ChatInfo[]`:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
interface ChatInfo {
|
|
152
|
+
guid: string;
|
|
153
|
+
displayName: string | null;
|
|
154
|
+
chatIdentifier: string;
|
|
155
|
+
isGroup: boolean;
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `getChatParticipants(chatGuid)`
|
|
160
|
+
|
|
161
|
+
Get the phone numbers of all participants in a chat.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { getChatParticipants } from './src/index.js';
|
|
165
|
+
|
|
166
|
+
const numbers = getChatParticipants('iMessage;+;chat123');
|
|
167
|
+
// ['+15551234567', '+15559876543']
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Examples
|
|
173
|
+
|
|
174
|
+
Both examples are ready to run after replacing `YOUR_CHAT_GUID_HERE`.
|
|
175
|
+
|
|
176
|
+
| Example | Command | What it does |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| Echo bot | `npx tsx examples/echo-bot.ts` | `!echo` and `!ping` commands |
|
|
179
|
+
| Weight bot | `npx tsx examples/weight-bot.ts` | `/w` weight logging with history |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Running as a Background Service (launchd)
|
|
184
|
+
|
|
185
|
+
To keep your bot running permanently on macOS, register it as a launchd agent.
|
|
186
|
+
|
|
187
|
+
Create `~/Library/LaunchAgents/com.imessage-bot.mybot.plist`:
|
|
188
|
+
|
|
189
|
+
```xml
|
|
190
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
191
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
192
|
+
<plist version="1.0">
|
|
193
|
+
<dict>
|
|
194
|
+
<key>Label</key>
|
|
195
|
+
<string>com.imessage-bot.mybot</string>
|
|
196
|
+
<key>ProgramArguments</key>
|
|
197
|
+
<array>
|
|
198
|
+
<string>/usr/local/bin/node</string>
|
|
199
|
+
<string>--import</string>
|
|
200
|
+
<string>tsx/esm</string>
|
|
201
|
+
<string>/absolute/path/to/my-bot.ts</string>
|
|
202
|
+
</array>
|
|
203
|
+
<key>RunAtLoad</key>
|
|
204
|
+
<true/>
|
|
205
|
+
<key>KeepAlive</key>
|
|
206
|
+
<true/>
|
|
207
|
+
<key>StandardOutPath</key>
|
|
208
|
+
<string>/tmp/imessage-bot.log</string>
|
|
209
|
+
<key>StandardErrorPath</key>
|
|
210
|
+
<string>/tmp/imessage-bot.err</string>
|
|
211
|
+
</dict>
|
|
212
|
+
</plist>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
launchctl load ~/Library/LaunchAgents/com.imessage-bot.mybot.plist
|
|
217
|
+
launchctl start com.imessage-bot.mybot
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## How It Works
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
iMessage → chat.db (SQLite, read-only)
|
|
226
|
+
↓ polled every N seconds via ROWID watermark
|
|
227
|
+
imessage-bot
|
|
228
|
+
↓ onMessage handler
|
|
229
|
+
your code (store data, call APIs, etc.)
|
|
230
|
+
↓ reply()
|
|
231
|
+
AppleScript → Messages.app → iMessage reply
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**ROWID watermark**: The poller tracks the last-seen message row ID in a local state file. On each poll it only fetches rows newer than that ID — no duplicate processing, no re-reading old messages.
|
|
235
|
+
|
|
236
|
+
**First run**: On first start, the poller seeds the watermark from existing messages without processing them. Your bot only reacts to messages sent *after* it starts for the first time.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Limitations
|
|
241
|
+
|
|
242
|
+
- macOS only — relies on `~/Library/Messages/chat.db` and AppleScript
|
|
243
|
+
- Requires the Mac to be awake and Messages.app to be running
|
|
244
|
+
- Sending messages via AppleScript requires Messages.app to be signed in to the Apple ID that owns the chat
|
|
245
|
+
- Polling is not real-time — default latency is up to 10 seconds (configurable)
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
MIT
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ChatInfo {
|
|
2
|
+
guid: string;
|
|
3
|
+
displayName: string | null;
|
|
4
|
+
chatIdentifier: string;
|
|
5
|
+
isGroup: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface RawMessage {
|
|
8
|
+
rowid: number;
|
|
9
|
+
text: string;
|
|
10
|
+
date: number;
|
|
11
|
+
senderId: string;
|
|
12
|
+
isFromMe: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* List chats. By default returns all chats.
|
|
16
|
+
* Pass `groupOnly: true` to return only group chats.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findChats(options?: {
|
|
19
|
+
limit?: number;
|
|
20
|
+
groupOnly?: boolean;
|
|
21
|
+
}): ChatInfo[];
|
|
22
|
+
/**
|
|
23
|
+
* Fetch messages from a chat.
|
|
24
|
+
*
|
|
25
|
+
* - `lastSeenRowId > 0` → only messages newer than that ROWID (normal polling)
|
|
26
|
+
* - `lastSeenRowId === 0` → messages from the last `weeksBack` weeks (first-run seed)
|
|
27
|
+
*/
|
|
28
|
+
export declare function getMessagesFromChat(chatGuid: string, weeksBack?: number, lastSeenRowId?: number): RawMessage[];
|
|
29
|
+
/**
|
|
30
|
+
* Get all participant phone numbers for a chat.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getChatParticipants(chatGuid: string): string[];
|
|
33
|
+
//# sourceMappingURL=db.d.ts.map
|
package/dist/db.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAoED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,QAAQ,EAAE,CA+B3F;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,SAAS,SAAI,EACb,aAAa,SAAI,GAChB,UAAU,EAAE,CAmEd;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAe9D"}
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { Unarchiver } from "node-typedstream";
|
|
5
|
+
const DB_PATH = join(process.env.HOME || "", "Library/Messages/chat.db");
|
|
6
|
+
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
7
|
+
function getDb() {
|
|
8
|
+
if (!existsSync(DB_PATH)) {
|
|
9
|
+
throw new Error(`iMessage database not found at ${DB_PATH}.\n` +
|
|
10
|
+
`Make sure Full Disk Access is enabled for Terminal (or your IDE) in:\n` +
|
|
11
|
+
`System Settings → Privacy & Security → Full Disk Access`);
|
|
12
|
+
}
|
|
13
|
+
return new Database(DB_PATH, { readonly: true });
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Apple stores timestamps as nanoseconds since 2001-01-01.
|
|
17
|
+
* Convert to a standard JS timestamp (ms since 1970-01-01).
|
|
18
|
+
*/
|
|
19
|
+
function appleTimestampToMs(ts) {
|
|
20
|
+
const APPLE_EPOCH_OFFSET_S = 978307200; // seconds between Unix and Apple epochs
|
|
21
|
+
return (ts / 1_000_000_000 + APPLE_EPOCH_OFFSET_S) * 1000;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Decode binary iMessage attributedBody blobs (NSAttributedString).
|
|
25
|
+
* Uses node-typedstream — the same approach used by BlueBubbles server.
|
|
26
|
+
*/
|
|
27
|
+
function extractTextFromAttributedBody(body) {
|
|
28
|
+
if (!body || body.length === 0)
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
const decoded = Unarchiver.open(body).decodeAll();
|
|
32
|
+
if (!decoded)
|
|
33
|
+
return null;
|
|
34
|
+
const items = (Array.isArray(decoded) ? decoded : [decoded]).flat();
|
|
35
|
+
for (const item of items) {
|
|
36
|
+
if (item && typeof item === "object") {
|
|
37
|
+
if ("string" in item && typeof item.string === "string" && item.string.trim()) {
|
|
38
|
+
return item.string.trim();
|
|
39
|
+
}
|
|
40
|
+
if ("values" in item && Array.isArray(item.values)) {
|
|
41
|
+
for (const val of item.values) {
|
|
42
|
+
if (val && typeof val === "object" && "string" in val && typeof val.string === "string" && val.string.trim()) {
|
|
43
|
+
return val.string.trim();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* List chats. By default returns all chats.
|
|
58
|
+
* Pass `groupOnly: true` to return only group chats.
|
|
59
|
+
*/
|
|
60
|
+
export function findChats(options = {}) {
|
|
61
|
+
const { limit = 50, groupOnly = false } = options;
|
|
62
|
+
const db = getDb();
|
|
63
|
+
const query = `
|
|
64
|
+
SELECT c.guid, c.display_name, c.chat_identifier, c.style
|
|
65
|
+
FROM chat c
|
|
66
|
+
LEFT JOIN (
|
|
67
|
+
SELECT cmj.chat_id, MAX(m.date) AS last_date
|
|
68
|
+
FROM message m
|
|
69
|
+
INNER JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
|
|
70
|
+
GROUP BY cmj.chat_id
|
|
71
|
+
) last ON c.ROWID = last.chat_id
|
|
72
|
+
${groupOnly ? "WHERE c.style = 43" : ""}
|
|
73
|
+
ORDER BY last.last_date DESC NULLS LAST
|
|
74
|
+
LIMIT ?
|
|
75
|
+
`;
|
|
76
|
+
const rows = db.prepare(query).all(limit);
|
|
77
|
+
db.close();
|
|
78
|
+
return rows.map((r) => ({
|
|
79
|
+
guid: r.guid,
|
|
80
|
+
displayName: r.display_name,
|
|
81
|
+
chatIdentifier: r.chat_identifier,
|
|
82
|
+
isGroup: r.style === 43,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fetch messages from a chat.
|
|
87
|
+
*
|
|
88
|
+
* - `lastSeenRowId > 0` → only messages newer than that ROWID (normal polling)
|
|
89
|
+
* - `lastSeenRowId === 0` → messages from the last `weeksBack` weeks (first-run seed)
|
|
90
|
+
*/
|
|
91
|
+
export function getMessagesFromChat(chatGuid, weeksBack = 1, lastSeenRowId = 0) {
|
|
92
|
+
const db = getDb();
|
|
93
|
+
let query;
|
|
94
|
+
let params;
|
|
95
|
+
if (lastSeenRowId > 0) {
|
|
96
|
+
query = `
|
|
97
|
+
SELECT
|
|
98
|
+
m.ROWID AS rowid,
|
|
99
|
+
m.text,
|
|
100
|
+
m.attributedBody,
|
|
101
|
+
m.date,
|
|
102
|
+
m.is_from_me,
|
|
103
|
+
COALESCE(h.id, 'Me') AS sender_id
|
|
104
|
+
FROM message m
|
|
105
|
+
INNER JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
|
|
106
|
+
INNER JOIN chat c ON cmj.chat_id = c.ROWID
|
|
107
|
+
LEFT JOIN handle h ON m.handle_id = h.ROWID
|
|
108
|
+
WHERE c.guid = ?
|
|
109
|
+
AND m.ROWID > ?
|
|
110
|
+
AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL)
|
|
111
|
+
ORDER BY m.ROWID ASC
|
|
112
|
+
`;
|
|
113
|
+
params = [chatGuid, lastSeenRowId];
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const startMs = Date.now() - weeksBack * 7 * 24 * 60 * 60 * 1000;
|
|
117
|
+
const APPLE_EPOCH_OFFSET_S = 978307200;
|
|
118
|
+
const startApple = (startMs / 1000 - APPLE_EPOCH_OFFSET_S) * 1_000_000_000;
|
|
119
|
+
query = `
|
|
120
|
+
SELECT
|
|
121
|
+
m.ROWID AS rowid,
|
|
122
|
+
m.text,
|
|
123
|
+
m.attributedBody,
|
|
124
|
+
m.date,
|
|
125
|
+
m.is_from_me,
|
|
126
|
+
COALESCE(h.id, 'Me') AS sender_id
|
|
127
|
+
FROM message m
|
|
128
|
+
INNER JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
|
|
129
|
+
INNER JOIN chat c ON cmj.chat_id = c.ROWID
|
|
130
|
+
LEFT JOIN handle h ON m.handle_id = h.ROWID
|
|
131
|
+
WHERE c.guid = ?
|
|
132
|
+
AND m.date > ?
|
|
133
|
+
AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL)
|
|
134
|
+
ORDER BY m.ROWID ASC
|
|
135
|
+
`;
|
|
136
|
+
params = [chatGuid, startApple];
|
|
137
|
+
}
|
|
138
|
+
const rows = db.prepare(query).all(...params);
|
|
139
|
+
db.close();
|
|
140
|
+
return rows
|
|
141
|
+
.map((row) => {
|
|
142
|
+
const text = row.text || extractTextFromAttributedBody(row.attributedBody);
|
|
143
|
+
if (!text?.trim())
|
|
144
|
+
return null;
|
|
145
|
+
return {
|
|
146
|
+
rowid: row.rowid,
|
|
147
|
+
text: text.trim(),
|
|
148
|
+
date: appleTimestampToMs(row.date),
|
|
149
|
+
senderId: row.is_from_me ? "Me" : row.sender_id,
|
|
150
|
+
isFromMe: row.is_from_me === 1,
|
|
151
|
+
};
|
|
152
|
+
})
|
|
153
|
+
.filter((m) => m !== null);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get all participant phone numbers for a chat.
|
|
157
|
+
*/
|
|
158
|
+
export function getChatParticipants(chatGuid) {
|
|
159
|
+
const db = getDb();
|
|
160
|
+
const rows = db
|
|
161
|
+
.prepare(`SELECT h.id
|
|
162
|
+
FROM handle h
|
|
163
|
+
INNER JOIN chat_handle_join chj ON h.ROWID = chj.handle_id
|
|
164
|
+
INNER JOIN chat c ON chj.chat_id = c.ROWID
|
|
165
|
+
WHERE c.guid = ?`)
|
|
166
|
+
.all(chatGuid);
|
|
167
|
+
db.close();
|
|
168
|
+
return rows.map((r) => r.id);
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=db.js.map
|
package/dist/db.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,EAAE,0BAA0B,CAAC,CAAC;AA4BzE,gFAAgF;AAEhF,SAAS,KAAK;IACZ,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,kCAAkC,OAAO,KAAK;YAC5C,wEAAwE;YACxE,yDAAyD,CAC5D,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;AACnD,CAAC;AAED;;;GAGG;AACH,SAAS,kBAAkB,CAAC,EAAU;IACpC,MAAM,oBAAoB,GAAG,SAAS,CAAC,CAAC,wCAAwC;IAChF,OAAO,CAAC,EAAE,GAAG,aAAa,GAAG,oBAAoB,CAAC,GAAG,IAAI,CAAC;AAC5D,CAAC;AAED;;;GAGG;AACH,SAAS,6BAA6B,CAAC,IAAmB;IACxD,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE5C,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;QAClD,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC;QAE1B,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAEpE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrC,IAAI,QAAQ,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;oBAC9E,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;gBAC5B,CAAC;gBACD,IAAI,QAAQ,IAAI,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;oBACnD,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;wBAC9B,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,QAAQ,IAAI,GAAG,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;4BAC7G,OAAO,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;wBAC3B,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,UAAmD,EAAE;IAC7E,MAAM,EAAE,KAAK,GAAG,EAAE,EAAE,SAAS,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC;IAClD,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;IAEnB,MAAM,KAAK,GAAG;;;;;;;;;MASV,SAAS,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE;;;GAGxC,CAAC;IAEF,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,KAAK,CAKrC,CAAC;IAAE,EAAE,CAAC,KAAK,EAAE,CAAC;IAEjB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACtB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,WAAW,EAAE,CAAC,CAAC,YAAY;QAC3B,cAAc,EAAE,CAAC,CAAC,eAAe;QACjC,OAAO,EAAE,CAAC,CAAC,KAAK,KAAK,EAAE;KACxB,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,QAAgB,EAChB,SAAS,GAAG,CAAC,EACb,aAAa,GAAG,CAAC;IAEjB,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;IAEnB,IAAI,KAAa,CAAC;IAClB,IAAI,MAA2B,CAAC;IAEhC,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;QACtB,KAAK,GAAG;;;;;;;;;;;;;;;;KAgBP,CAAC;QACF,MAAM,GAAG,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IACrC,CAAC;SAAM,CAAC;QACN,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;QACjE,MAAM,oBAAoB,GAAG,SAAS,CAAC;QACvC,MAAM,UAAU,GAAG,CAAC,OAAO,GAAG,IAAI,GAAG,oBAAoB,CAAC,GAAG,aAAa,CAAC;QAE3E,KAAK,GAAG;;;;;;;;;;;;;;;;KAgBP,CAAC;QACF,MAAM,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAmB,CAAC;IAChE,EAAE,CAAC,KAAK,EAAE,CAAC;IAEX,OAAO,IAAI;SACR,GAAG,CAAC,CAAC,GAAG,EAAqB,EAAE;QAC9B,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,6BAA6B,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAC3E,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE;YAAE,OAAO,IAAI,CAAC;QAE/B,OAAO;YACL,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;YACjB,IAAI,EAAE,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC;YAClC,QAAQ,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS;YAC/C,QAAQ,EAAE,GAAG,CAAC,UAAU,KAAK,CAAC;SAC/B,CAAC;IACJ,CAAC,CAAC;SACD,MAAM,CAAC,CAAC,CAAC,EAAmB,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AAChD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAAgB;IAClD,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;IAEnB,MAAM,IAAI,GAAG,EAAE;SACZ,OAAO,CACN;;;;wBAIkB,CACnB;SACA,GAAG,CAAC,QAAQ,CAAqB,CAAC;IAErC,EAAE,CAAC,KAAK,EAAE,CAAC;IACX,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-chats.d.ts","sourceRoot":"","sources":["../src/find-chats.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI utility to discover your iMessage chat GUIDs.
|
|
3
|
+
*
|
|
4
|
+
* Run with: npm run find-chats
|
|
5
|
+
*
|
|
6
|
+
* Copy the GUID of your target chat and use it as the `chatGuid` option
|
|
7
|
+
* when calling `createPoller()`.
|
|
8
|
+
*
|
|
9
|
+
* Note: Group chat names are only stored in chat.db when the group has been
|
|
10
|
+
* explicitly named inside Messages.app. Otherwise the name is NULL — this is
|
|
11
|
+
* a macOS limitation, not a bug. Participants are shown so you can identify
|
|
12
|
+
* which chat is which.
|
|
13
|
+
*/
|
|
14
|
+
import { findChats, getChatParticipants } from "./db.js";
|
|
15
|
+
console.log("🔍 Scanning iMessage chats...\n");
|
|
16
|
+
const chats = findChats({ limit: 50 });
|
|
17
|
+
if (chats.length === 0) {
|
|
18
|
+
console.log("No chats found. Make sure Full Disk Access is enabled for Terminal.");
|
|
19
|
+
console.log("System Settings → Privacy & Security → Full Disk Access");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const groups = chats.filter((c) => c.isGroup);
|
|
23
|
+
const direct = chats.filter((c) => !c.isGroup);
|
|
24
|
+
if (groups.length > 0) {
|
|
25
|
+
console.log(`─── Group Chats (${groups.length}) ─────────────────────────`);
|
|
26
|
+
for (const chat of groups) {
|
|
27
|
+
const participants = getChatParticipants(chat.guid);
|
|
28
|
+
const name = chat.displayName || "(no name set in Messages.app)";
|
|
29
|
+
console.log(`Name : ${name}`);
|
|
30
|
+
console.log(`GUID : ${chat.guid}`);
|
|
31
|
+
console.log(`Participants : ${participants.length > 0 ? participants.join(", ") : "none found"}`);
|
|
32
|
+
console.log("");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (direct.length > 0) {
|
|
36
|
+
console.log(`─── Direct Messages (${direct.length}) ──────────────────────`);
|
|
37
|
+
for (const chat of direct) {
|
|
38
|
+
console.log(`Contact : ${chat.chatIdentifier}`);
|
|
39
|
+
console.log(`GUID : ${chat.guid}`);
|
|
40
|
+
console.log("");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
console.log("💡 Copy the GUID above and pass it as chatGuid to createPoller().");
|
|
44
|
+
//# sourceMappingURL=find-chats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-chats.js","sourceRoot":"","sources":["../src/find-chats.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEzD,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;AAE/C,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;AAEvC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IACvB,OAAO,CAAC,GAAG,CAAC,qEAAqE,CAAC,CAAC;IACnF,OAAO,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;IACvE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AAC9C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AAE/C,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;IACtB,OAAO,CAAC,GAAG,CAAC,oBAAoB,MAAM,CAAC,MAAM,6BAA6B,CAAC,CAAC;IAC5E,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,MAAM,YAAY,GAAG,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,IAAI,+BAA+B,CAAC;QACjE,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3C,OAAO,CAAC,GAAG,CAAC,kBAAkB,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC;QAClG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;IACtB,OAAO,CAAC,GAAG,CAAC,wBAAwB,MAAM,CAAC,MAAM,0BAA0B,CAAC,CAAC;IAC7E,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC;QAChD,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,OAAO,CAAC,GAAG,CAAC,mEAAmE,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createPoller } from "./poller.js";
|
|
2
|
+
export type { PollerOptions, Poller, MessageContext } from "./poller.js";
|
|
3
|
+
export { sendMessage } from "./messenger.js";
|
|
4
|
+
export { findChats, getMessagesFromChat, getChatParticipants } from "./db.js";
|
|
5
|
+
export type { ChatInfo, RawMessage } from "./db.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGzE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC9E,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Core bot API
|
|
2
|
+
export { createPoller } from "./poller.js";
|
|
3
|
+
// Low-level messaging
|
|
4
|
+
export { sendMessage } from "./messenger.js";
|
|
5
|
+
// Database utilities
|
|
6
|
+
export { findChats, getMessagesFromChat, getChatParticipants } from "./db.js";
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAe;AACf,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,sBAAsB;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,qBAAqB;AACrB,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send a message to an iMessage chat via AppleScript.
|
|
3
|
+
*
|
|
4
|
+
* Requires Messages.app to be signed in and the chat to exist.
|
|
5
|
+
* The `chatGuid` can be obtained from `findChats()` or `npm run find-chats`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sendMessage(chatGuid: string, text: string): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=messenger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messenger.d.ts","sourceRoot":"","sources":["../src/messenger.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAa/E"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
/**
|
|
5
|
+
* Send a message to an iMessage chat via AppleScript.
|
|
6
|
+
*
|
|
7
|
+
* Requires Messages.app to be signed in and the chat to exist.
|
|
8
|
+
* The `chatGuid` can be obtained from `findChats()` or `npm run find-chats`.
|
|
9
|
+
*/
|
|
10
|
+
export async function sendMessage(chatGuid, text) {
|
|
11
|
+
// Escape for AppleScript string literal
|
|
12
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
13
|
+
const script = `
|
|
14
|
+
tell application "Messages"
|
|
15
|
+
set targetChat to chat id "${chatGuid}"
|
|
16
|
+
send "${escaped}" to targetChat
|
|
17
|
+
end tell
|
|
18
|
+
`;
|
|
19
|
+
// Escape single quotes for the shell -e argument
|
|
20
|
+
await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}'`);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=messenger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messenger.js","sourceRoot":"","sources":["../src/messenger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAEjC,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;AAElC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAgB,EAAE,IAAY;IAC9D,wCAAwC;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAEjE,MAAM,MAAM,GAAG;;mCAEkB,QAAQ;cAC7B,OAAO;;GAElB,CAAC;IAEF,iDAAiD;IACjD,MAAM,SAAS,CAAC,iBAAiB,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;AACrE,CAAC"}
|
package/dist/poller.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type RawMessage } from "./db.js";
|
|
2
|
+
export type { RawMessage };
|
|
3
|
+
export interface MessageContext {
|
|
4
|
+
/** The incoming message */
|
|
5
|
+
message: RawMessage;
|
|
6
|
+
/** The GUID of the chat this message was received in */
|
|
7
|
+
chatGuid: string;
|
|
8
|
+
/** Send a reply to the same chat */
|
|
9
|
+
reply: (text: string) => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface PollerOptions {
|
|
12
|
+
/** Chat GUID to monitor. Get this by running: npm run find-chats */
|
|
13
|
+
chatGuid: string;
|
|
14
|
+
/**
|
|
15
|
+
* How often to poll for new messages, in milliseconds.
|
|
16
|
+
* @default 10000 (10 seconds)
|
|
17
|
+
*/
|
|
18
|
+
pollIntervalMs?: number;
|
|
19
|
+
/**
|
|
20
|
+
* On first run, how many weeks back to seed the ROWID watermark from.
|
|
21
|
+
* Messages in this window are NOT processed — the poller just advances past them
|
|
22
|
+
* so it only reacts to messages sent after it starts.
|
|
23
|
+
* @default 1
|
|
24
|
+
*/
|
|
25
|
+
seedWeeksBack?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Where to persist the ROWID watermark between restarts.
|
|
28
|
+
* Defaults to ~/.imessage-bot-<hash>.json (one file per chatGuid).
|
|
29
|
+
*/
|
|
30
|
+
stateFile?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Called for every new message received in the chat.
|
|
33
|
+
* Use `context.reply(text)` to respond.
|
|
34
|
+
*/
|
|
35
|
+
onMessage: (context: MessageContext) => Promise<void> | void;
|
|
36
|
+
/**
|
|
37
|
+
* Called when an error occurs during a poll cycle.
|
|
38
|
+
* If not provided, errors are logged to stderr.
|
|
39
|
+
*/
|
|
40
|
+
onError?: (error: Error) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Called once when the poller starts.
|
|
43
|
+
*/
|
|
44
|
+
onReady?: (info: {
|
|
45
|
+
chatGuid: string;
|
|
46
|
+
stateFile: string;
|
|
47
|
+
}) => void;
|
|
48
|
+
}
|
|
49
|
+
export interface Poller {
|
|
50
|
+
/** Start polling. Safe to call once. */
|
|
51
|
+
start(): void;
|
|
52
|
+
/** Stop polling and clear the interval. */
|
|
53
|
+
stop(): void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create an iMessage bot that polls a chat for new messages.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { createPoller } from 'imessage-bot';
|
|
61
|
+
*
|
|
62
|
+
* const bot = createPoller({
|
|
63
|
+
* chatGuid: 'iMessage;+;chat123456',
|
|
64
|
+
* onMessage: async ({ message, reply }) => {
|
|
65
|
+
* if (message.text === '!ping') {
|
|
66
|
+
* await reply('pong!');
|
|
67
|
+
* }
|
|
68
|
+
* },
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* bot.start();
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function createPoller(options: PollerOptions): Poller;
|
|
75
|
+
//# sourceMappingURL=poller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poller.d.ts","sourceRoot":"","sources":["../src/poller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAO/D,YAAY,EAAE,UAAU,EAAE,CAAC;AAE3B,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,UAAU,CAAC;IACpB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,SAAS,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAE7D;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAEjC;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACnE;AAED,MAAM,WAAW,MAAM;IACrB,wCAAwC;IACxC,KAAK,IAAI,IAAI,CAAC;IACd,2CAA2C;IAC3C,IAAI,IAAI,IAAI,CAAC;CACd;AA4BD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAqF3D"}
|
package/dist/poller.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { getMessagesFromChat } from "./db.js";
|
|
2
|
+
import { sendMessage } from "./messenger.js";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
// ─── State helpers ───────────────────────────────────────────────────────────
|
|
6
|
+
function defaultStateFile(chatGuid) {
|
|
7
|
+
// Derive a short stable slug from the chat GUID so multiple bots don't collide
|
|
8
|
+
const slug = Buffer.from(chatGuid).toString("base64url").slice(0, 16);
|
|
9
|
+
return join(process.env.HOME || "", `.imessage-bot-${slug}.json`);
|
|
10
|
+
}
|
|
11
|
+
function loadState(stateFile) {
|
|
12
|
+
try {
|
|
13
|
+
if (existsSync(stateFile)) {
|
|
14
|
+
const data = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
15
|
+
return typeof data.lastSeenRowId === "number" ? data.lastSeenRowId : 0;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// corrupt or missing — start fresh
|
|
20
|
+
}
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
function saveState(stateFile, lastSeenRowId) {
|
|
24
|
+
writeFileSync(stateFile, JSON.stringify({ lastSeenRowId }), "utf-8");
|
|
25
|
+
}
|
|
26
|
+
// ─── createPoller ────────────────────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Create an iMessage bot that polls a chat for new messages.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { createPoller } from 'imessage-bot';
|
|
33
|
+
*
|
|
34
|
+
* const bot = createPoller({
|
|
35
|
+
* chatGuid: 'iMessage;+;chat123456',
|
|
36
|
+
* onMessage: async ({ message, reply }) => {
|
|
37
|
+
* if (message.text === '!ping') {
|
|
38
|
+
* await reply('pong!');
|
|
39
|
+
* }
|
|
40
|
+
* },
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* bot.start();
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function createPoller(options) {
|
|
47
|
+
const { chatGuid, pollIntervalMs = 10_000, seedWeeksBack = 1, onMessage, onError, onReady, } = options;
|
|
48
|
+
const stateFile = options.stateFile ?? defaultStateFile(chatGuid);
|
|
49
|
+
let lastSeenRowId = loadState(stateFile);
|
|
50
|
+
let intervalHandle = null;
|
|
51
|
+
let running = false;
|
|
52
|
+
async function poll() {
|
|
53
|
+
try {
|
|
54
|
+
const isFirstRun = lastSeenRowId === 0;
|
|
55
|
+
const messages = getMessagesFromChat(chatGuid, seedWeeksBack, lastSeenRowId);
|
|
56
|
+
if (isFirstRun) {
|
|
57
|
+
if (messages.length > 0) {
|
|
58
|
+
lastSeenRowId = Math.max(...messages.map((m) => m.rowid));
|
|
59
|
+
saveState(stateFile, lastSeenRowId);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (messages.length === 0)
|
|
64
|
+
return;
|
|
65
|
+
for (const message of messages) {
|
|
66
|
+
const context = {
|
|
67
|
+
message,
|
|
68
|
+
chatGuid,
|
|
69
|
+
reply: (text) => sendMessage(chatGuid, text),
|
|
70
|
+
};
|
|
71
|
+
try {
|
|
72
|
+
await onMessage(context);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
76
|
+
if (onError) {
|
|
77
|
+
onError(error);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
console.error(`[imessage-bot] onMessage error:`, error.message);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
lastSeenRowId = Math.max(...messages.map((m) => m.rowid));
|
|
85
|
+
saveState(stateFile, lastSeenRowId);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
89
|
+
if (onError) {
|
|
90
|
+
onError(error);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.error(`[imessage-bot] Poll error:`, error.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
start() {
|
|
99
|
+
if (running)
|
|
100
|
+
return;
|
|
101
|
+
running = true;
|
|
102
|
+
onReady?.({ chatGuid, stateFile });
|
|
103
|
+
// Immediate first poll, then on interval
|
|
104
|
+
poll();
|
|
105
|
+
intervalHandle = setInterval(poll, pollIntervalMs);
|
|
106
|
+
process.on("SIGINT", () => this.stop());
|
|
107
|
+
process.on("SIGTERM", () => this.stop());
|
|
108
|
+
},
|
|
109
|
+
stop() {
|
|
110
|
+
if (!running)
|
|
111
|
+
return;
|
|
112
|
+
running = false;
|
|
113
|
+
if (intervalHandle !== null) {
|
|
114
|
+
clearInterval(intervalHandle);
|
|
115
|
+
intervalHandle = null;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=poller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poller.js","sourceRoot":"","sources":["../src/poller.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "imessage-bot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Node.js toolkit for reading and responding to iMessages on macOS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"dev": "tsc --watch",
|
|
22
|
+
"find-chats": "npx tsx src/find-chats.ts",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": ["imessage", "bot", "macos", "messages", "automation", "applescript", "sqlite"],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0",
|
|
29
|
+
"os": ["darwin"]
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
33
|
+
"@types/node": "^20.11.0",
|
|
34
|
+
"tsx": "^4.7.0",
|
|
35
|
+
"typescript": "^5.3.3"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"better-sqlite3": "^9.4.0",
|
|
39
|
+
"node-typedstream": "^1.4.1"
|
|
40
|
+
}
|
|
41
|
+
}
|