feat: add is_bot_message column and support dedicated phone numbers (#235)
* feat: add is_bot_message column and support dedicated phone numbers Replace fragile content-prefix bot detection with an explicit is_bot_message database column. The old prefix check (content NOT LIKE 'Andy:%') is kept as a backstop for pre-migration messages. - Add is_bot_message column with automatic backfill migration - Add ASSISTANT_HAS_OWN_NUMBER env var to skip name prefix when the assistant has its own WhatsApp number - Move prefix logic into WhatsApp channel (no longer a router concern) - Remove prefixAssistantName from Channel interface - Load .env via dotenv so launchd-managed processes pick up config - WhatsApp bot detection: fromMe for own number, prefix match for shared Based on #160 and #173. Co-Authored-By: Stefan Gasser <stefan@stefangasser.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract shared .env parser and remove dotenv dependency Extract .env parsing into src/env.ts, used by both config.ts and container-runner.ts. Reads only requested keys without loading secrets into process.env, avoiding leaking API keys to child processes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Stefan Gasser <stefan@stefangasser.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -53,7 +53,7 @@ describe('storeMessage', () => {
|
||||
timestamp: '2024-01-01T00:00:01.000Z',
|
||||
});
|
||||
|
||||
const messages = getMessagesSince('group@g.us', '2024-01-01T00:00:00.000Z', 'BotName');
|
||||
const messages = getMessagesSince('group@g.us', '2024-01-01T00:00:00.000Z', 'Andy');
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0].id).toBe('msg-1');
|
||||
expect(messages[0].sender).toBe('123@s.whatsapp.net');
|
||||
@@ -73,7 +73,7 @@ describe('storeMessage', () => {
|
||||
timestamp: '2024-01-01T00:00:04.000Z',
|
||||
});
|
||||
|
||||
const messages = getMessagesSince('group@g.us', '2024-01-01T00:00:00.000Z', 'BotName');
|
||||
const messages = getMessagesSince('group@g.us', '2024-01-01T00:00:00.000Z', 'Andy');
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0].content).toBe('');
|
||||
});
|
||||
@@ -92,7 +92,7 @@ describe('storeMessage', () => {
|
||||
});
|
||||
|
||||
// Message is stored (we can retrieve it — is_from_me doesn't affect retrieval)
|
||||
const messages = getMessagesSince('group@g.us', '2024-01-01T00:00:00.000Z', 'BotName');
|
||||
const messages = getMessagesSince('group@g.us', '2024-01-01T00:00:00.000Z', 'Andy');
|
||||
expect(messages).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('storeMessage', () => {
|
||||
timestamp: '2024-01-01T00:00:01.000Z',
|
||||
});
|
||||
|
||||
const messages = getMessagesSince('group@g.us', '2024-01-01T00:00:00.000Z', 'BotName');
|
||||
const messages = getMessagesSince('group@g.us', '2024-01-01T00:00:00.000Z', 'Andy');
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0].content).toBe('updated');
|
||||
});
|
||||
@@ -129,22 +129,23 @@ describe('getMessagesSince', () => {
|
||||
beforeEach(() => {
|
||||
storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
|
||||
|
||||
const msgs = [
|
||||
{ id: 'm1', content: 'first', ts: '2024-01-01T00:00:01.000Z', sender: 'Alice' },
|
||||
{ id: 'm2', content: 'second', ts: '2024-01-01T00:00:02.000Z', sender: 'Bob' },
|
||||
{ id: 'm3', content: 'Andy: bot reply', ts: '2024-01-01T00:00:03.000Z', sender: 'Bot' },
|
||||
{ id: 'm4', content: 'third', ts: '2024-01-01T00:00:04.000Z', sender: 'Carol' },
|
||||
];
|
||||
for (const m of msgs) {
|
||||
store({
|
||||
id: m.id,
|
||||
chat_jid: 'group@g.us',
|
||||
sender: `${m.sender}@s.whatsapp.net`,
|
||||
sender_name: m.sender,
|
||||
content: m.content,
|
||||
timestamp: m.ts,
|
||||
});
|
||||
}
|
||||
store({
|
||||
id: 'm1', chat_jid: 'group@g.us', sender: 'Alice@s.whatsapp.net',
|
||||
sender_name: 'Alice', content: 'first', timestamp: '2024-01-01T00:00:01.000Z',
|
||||
});
|
||||
store({
|
||||
id: 'm2', chat_jid: 'group@g.us', sender: 'Bob@s.whatsapp.net',
|
||||
sender_name: 'Bob', content: 'second', timestamp: '2024-01-01T00:00:02.000Z',
|
||||
});
|
||||
storeMessage({
|
||||
id: 'm3', chat_jid: 'group@g.us', sender: 'Bot@s.whatsapp.net',
|
||||
sender_name: 'Bot', content: 'bot reply', timestamp: '2024-01-01T00:00:03.000Z',
|
||||
is_bot_message: true,
|
||||
});
|
||||
store({
|
||||
id: 'm4', chat_jid: 'group@g.us', sender: 'Carol@s.whatsapp.net',
|
||||
sender_name: 'Carol', content: 'third', timestamp: '2024-01-01T00:00:04.000Z',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns messages after the given timestamp', () => {
|
||||
@@ -154,17 +155,28 @@ describe('getMessagesSince', () => {
|
||||
expect(msgs[0].content).toBe('third');
|
||||
});
|
||||
|
||||
it('excludes messages from the assistant (content prefix)', () => {
|
||||
it('excludes bot messages via is_bot_message flag', () => {
|
||||
const msgs = getMessagesSince('group@g.us', '2024-01-01T00:00:00.000Z', 'Andy');
|
||||
const botMsgs = msgs.filter((m) => m.content.startsWith('Andy:'));
|
||||
const botMsgs = msgs.filter((m) => m.content === 'bot reply');
|
||||
expect(botMsgs).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('returns all messages when sinceTimestamp is empty', () => {
|
||||
it('returns all non-bot messages when sinceTimestamp is empty', () => {
|
||||
const msgs = getMessagesSince('group@g.us', '', 'Andy');
|
||||
// 3 user messages (bot message excluded)
|
||||
expect(msgs).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('filters pre-migration bot messages via content prefix backstop', () => {
|
||||
// Simulate a message written before migration: has prefix but is_bot_message = 0
|
||||
store({
|
||||
id: 'm5', chat_jid: 'group@g.us', sender: 'Bot@s.whatsapp.net',
|
||||
sender_name: 'Bot', content: 'Andy: old bot reply',
|
||||
timestamp: '2024-01-01T00:00:05.000Z',
|
||||
});
|
||||
const msgs = getMessagesSince('group@g.us', '2024-01-01T00:00:04.000Z', 'Andy');
|
||||
expect(msgs).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// --- getNewMessages ---
|
||||
@@ -174,22 +186,23 @@ describe('getNewMessages', () => {
|
||||
storeChatMetadata('group1@g.us', '2024-01-01T00:00:00.000Z');
|
||||
storeChatMetadata('group2@g.us', '2024-01-01T00:00:00.000Z');
|
||||
|
||||
const msgs = [
|
||||
{ id: 'a1', chat: 'group1@g.us', content: 'g1 msg1', ts: '2024-01-01T00:00:01.000Z' },
|
||||
{ id: 'a2', chat: 'group2@g.us', content: 'g2 msg1', ts: '2024-01-01T00:00:02.000Z' },
|
||||
{ id: 'a3', chat: 'group1@g.us', content: 'Andy: reply', ts: '2024-01-01T00:00:03.000Z' },
|
||||
{ id: 'a4', chat: 'group1@g.us', content: 'g1 msg2', ts: '2024-01-01T00:00:04.000Z' },
|
||||
];
|
||||
for (const m of msgs) {
|
||||
store({
|
||||
id: m.id,
|
||||
chat_jid: m.chat,
|
||||
sender: 'user@s.whatsapp.net',
|
||||
sender_name: 'User',
|
||||
content: m.content,
|
||||
timestamp: m.ts,
|
||||
});
|
||||
}
|
||||
store({
|
||||
id: 'a1', chat_jid: 'group1@g.us', sender: 'user@s.whatsapp.net',
|
||||
sender_name: 'User', content: 'g1 msg1', timestamp: '2024-01-01T00:00:01.000Z',
|
||||
});
|
||||
store({
|
||||
id: 'a2', chat_jid: 'group2@g.us', sender: 'user@s.whatsapp.net',
|
||||
sender_name: 'User', content: 'g2 msg1', timestamp: '2024-01-01T00:00:02.000Z',
|
||||
});
|
||||
storeMessage({
|
||||
id: 'a3', chat_jid: 'group1@g.us', sender: 'user@s.whatsapp.net',
|
||||
sender_name: 'User', content: 'bot reply', timestamp: '2024-01-01T00:00:03.000Z',
|
||||
is_bot_message: true,
|
||||
});
|
||||
store({
|
||||
id: 'a4', chat_jid: 'group1@g.us', sender: 'user@s.whatsapp.net',
|
||||
sender_name: 'User', content: 'g1 msg2', timestamp: '2024-01-01T00:00:04.000Z',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns new messages across multiple groups', () => {
|
||||
@@ -198,7 +211,7 @@ describe('getNewMessages', () => {
|
||||
'2024-01-01T00:00:00.000Z',
|
||||
'Andy',
|
||||
);
|
||||
// Excludes 'Andy: reply', returns 3 messages
|
||||
// Excludes bot message, returns 3 user messages
|
||||
expect(messages).toHaveLength(3);
|
||||
expect(newTimestamp).toBe('2024-01-01T00:00:04.000Z');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user