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:
gavrielc
2026-02-15 15:31:57 +02:00
committed by GitHub
parent c8ab3d95e1
commit 9261a25531
13 changed files with 202 additions and 140 deletions

View File

@@ -2,7 +2,7 @@ import Database from 'better-sqlite3';
import fs from 'fs';
import path from 'path';
import { DATA_DIR, STORE_DIR } from './config.js';
import { ASSISTANT_NAME, DATA_DIR, STORE_DIR } from './config.js';
import { NewMessage, RegisteredGroup, ScheduledTask, TaskRunLog } from './types.js';
let db: Database.Database;
@@ -22,6 +22,7 @@ function createSchema(database: Database.Database): void {
content TEXT,
timestamp TEXT,
is_from_me INTEGER,
is_bot_message INTEGER DEFAULT 0,
PRIMARY KEY (id, chat_jid),
FOREIGN KEY (chat_jid) REFERENCES chats(jid)
);
@@ -82,6 +83,19 @@ function createSchema(database: Database.Database): void {
} catch {
/* column already exists */
}
// Add is_bot_message column if it doesn't exist (migration for existing DBs)
try {
database.exec(
`ALTER TABLE messages ADD COLUMN is_bot_message INTEGER DEFAULT 0`,
);
// Backfill: mark existing bot messages that used the content prefix pattern
database.prepare(
`UPDATE messages SET is_bot_message = 1 WHERE content LIKE ?`,
).run(`${ASSISTANT_NAME}:%`);
} catch {
/* column already exists */
}
}
export function initDatabase(): void {
@@ -194,7 +208,7 @@ export function setLastGroupSync(): void {
*/
export function storeMessage(msg: NewMessage): void {
db.prepare(
`INSERT OR REPLACE INTO messages (id, chat_jid, sender, sender_name, content, timestamp, is_from_me) VALUES (?, ?, ?, ?, ?, ?, ?)`,
`INSERT OR REPLACE INTO messages (id, chat_jid, sender, sender_name, content, timestamp, is_from_me, is_bot_message) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
).run(
msg.id,
msg.chat_jid,
@@ -203,6 +217,7 @@ export function storeMessage(msg: NewMessage): void {
msg.content,
msg.timestamp,
msg.is_from_me ? 1 : 0,
msg.is_bot_message ? 1 : 0,
);
}
@@ -217,9 +232,10 @@ export function storeMessageDirect(msg: {
content: string;
timestamp: string;
is_from_me: boolean;
is_bot_message?: boolean;
}): void {
db.prepare(
`INSERT OR REPLACE INTO messages (id, chat_jid, sender, sender_name, content, timestamp, is_from_me) VALUES (?, ?, ?, ?, ?, ?, ?)`,
`INSERT OR REPLACE INTO messages (id, chat_jid, sender, sender_name, content, timestamp, is_from_me, is_bot_message) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
).run(
msg.id,
msg.chat_jid,
@@ -228,6 +244,7 @@ export function storeMessageDirect(msg: {
msg.content,
msg.timestamp,
msg.is_from_me ? 1 : 0,
msg.is_bot_message ? 1 : 0,
);
}
@@ -239,11 +256,13 @@ export function getNewMessages(
if (jids.length === 0) return { messages: [], newTimestamp: lastTimestamp };
const placeholders = jids.map(() => '?').join(',');
// Filter out bot's own messages by checking content prefix (not is_from_me, since user shares the account)
// Filter bot messages using both the is_bot_message flag AND the content
// prefix as a backstop for messages written before the migration ran.
const sql = `
SELECT id, chat_jid, sender, sender_name, content, timestamp
FROM messages
WHERE timestamp > ? AND chat_jid IN (${placeholders}) AND content NOT LIKE ?
WHERE timestamp > ? AND chat_jid IN (${placeholders})
AND is_bot_message = 0 AND content NOT LIKE ?
ORDER BY timestamp
`;
@@ -264,11 +283,13 @@ export function getMessagesSince(
sinceTimestamp: string,
botPrefix: string,
): NewMessage[] {
// Filter out bot's own messages by checking content prefix
// Filter bot messages using both the is_bot_message flag AND the content
// prefix as a backstop for messages written before the migration ran.
const sql = `
SELECT id, chat_jid, sender, sender_name, content, timestamp
FROM messages
WHERE chat_jid = ? AND timestamp > ? AND content NOT LIKE ?
WHERE chat_jid = ? AND timestamp > ?
AND is_bot_message = 0 AND content NOT LIKE ?
ORDER BY timestamp
`;
return db