262 lines
6.9 KiB
JavaScript
262 lines
6.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* lettabot-message - Send messages to channels
|
|
*
|
|
* Usage:
|
|
* lettabot-message send --text "Hello!" [--channel telegram] [--chat 123456]
|
|
* lettabot-message send -t "Hello!"
|
|
*
|
|
* The agent can use this CLI via Bash to send messages during silent mode
|
|
* (heartbeats, cron jobs) or to send to different channels during conversations.
|
|
*/
|
|
|
|
import 'dotenv/config';
|
|
import { resolve } from 'node:path';
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
|
|
// Types
|
|
interface LastTarget {
|
|
channel: string;
|
|
chatId: string;
|
|
}
|
|
|
|
interface AgentStore {
|
|
agentId?: string;
|
|
lastMessageTarget?: LastTarget; // Note: field is "lastMessageTarget" not "lastTarget"
|
|
}
|
|
|
|
// Store path (same location as bot uses)
|
|
const STORE_PATH = resolve(process.cwd(), 'lettabot-agent.json');
|
|
|
|
function loadLastTarget(): LastTarget | null {
|
|
try {
|
|
if (existsSync(STORE_PATH)) {
|
|
const store: AgentStore = JSON.parse(readFileSync(STORE_PATH, 'utf-8'));
|
|
return store.lastMessageTarget || null;
|
|
}
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Channel senders
|
|
async function sendTelegram(chatId: string, text: string): Promise<void> {
|
|
const token = process.env.TELEGRAM_BOT_TOKEN;
|
|
if (!token) {
|
|
throw new Error('TELEGRAM_BOT_TOKEN not set');
|
|
}
|
|
|
|
const url = `https://api.telegram.org/bot${token}/sendMessage`;
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
chat_id: chatId,
|
|
text: text,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
throw new Error(`Telegram API error: ${error}`);
|
|
}
|
|
|
|
const result = await response.json() as { ok: boolean; result?: { message_id: number } };
|
|
if (!result.ok) {
|
|
throw new Error(`Telegram API returned ok=false`);
|
|
}
|
|
|
|
console.log(`✓ Sent to telegram:${chatId} (message_id: ${result.result?.message_id})`);
|
|
}
|
|
|
|
async function sendSlack(chatId: string, text: string): Promise<void> {
|
|
const token = process.env.SLACK_BOT_TOKEN;
|
|
if (!token) {
|
|
throw new Error('SLACK_BOT_TOKEN not set');
|
|
}
|
|
|
|
const response = await fetch('https://slack.com/api/chat.postMessage', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
channel: chatId,
|
|
text: text,
|
|
}),
|
|
});
|
|
|
|
const result = await response.json() as { ok: boolean; ts?: string; error?: string };
|
|
if (!result.ok) {
|
|
throw new Error(`Slack API error: ${result.error}`);
|
|
}
|
|
|
|
console.log(`✓ Sent to slack:${chatId} (ts: ${result.ts})`);
|
|
}
|
|
|
|
async function sendSignal(chatId: string, text: string): Promise<void> {
|
|
const apiUrl = process.env.SIGNAL_CLI_REST_API_URL || 'http://localhost:8080';
|
|
const phoneNumber = process.env.SIGNAL_PHONE_NUMBER;
|
|
|
|
if (!phoneNumber) {
|
|
throw new Error('SIGNAL_PHONE_NUMBER not set');
|
|
}
|
|
|
|
const response = await fetch(`${apiUrl}/v2/send`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
message: text,
|
|
number: phoneNumber,
|
|
recipients: [chatId],
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
throw new Error(`Signal API error: ${error}`);
|
|
}
|
|
|
|
console.log(`✓ Sent to signal:${chatId}`);
|
|
}
|
|
|
|
async function sendWhatsApp(chatId: string, text: string): Promise<void> {
|
|
// WhatsApp requires a running session, so we write to a queue file
|
|
// that the bot process picks up. For now, error out.
|
|
throw new Error('WhatsApp sending via CLI not yet supported (requires active session)');
|
|
}
|
|
|
|
async function sendToChannel(channel: string, chatId: string, text: string): Promise<void> {
|
|
switch (channel.toLowerCase()) {
|
|
case 'telegram':
|
|
return sendTelegram(chatId, text);
|
|
case 'slack':
|
|
return sendSlack(chatId, text);
|
|
case 'signal':
|
|
return sendSignal(chatId, text);
|
|
case 'whatsapp':
|
|
return sendWhatsApp(chatId, text);
|
|
default:
|
|
throw new Error(`Unknown channel: ${channel}. Supported: telegram, slack, signal`);
|
|
}
|
|
}
|
|
|
|
// Command handlers
|
|
async function sendCommand(args: string[]): Promise<void> {
|
|
let text = '';
|
|
let channel = '';
|
|
let chatId = '';
|
|
|
|
// Parse args
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
const next = args[i + 1];
|
|
|
|
if ((arg === '--text' || arg === '-t') && next) {
|
|
text = next;
|
|
i++;
|
|
} else if ((arg === '--channel' || arg === '-c') && next) {
|
|
channel = next;
|
|
i++;
|
|
} else if ((arg === '--chat' || arg === '--to') && next) {
|
|
chatId = next;
|
|
i++;
|
|
}
|
|
}
|
|
|
|
if (!text) {
|
|
console.error('Error: --text is required');
|
|
console.error('Usage: lettabot-message send --text "Hello!" [--channel telegram] [--chat 123456]');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Resolve defaults from last target
|
|
if (!channel || !chatId) {
|
|
const lastTarget = loadLastTarget();
|
|
if (lastTarget) {
|
|
channel = channel || lastTarget.channel;
|
|
chatId = chatId || lastTarget.chatId;
|
|
}
|
|
}
|
|
|
|
if (!channel) {
|
|
console.error('Error: --channel is required (no default available)');
|
|
console.error('Specify: --channel telegram|slack|signal');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!chatId) {
|
|
console.error('Error: --chat is required (no default available)');
|
|
console.error('Specify: --chat <chat_id>');
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
await sendToChannel(channel, chatId, text);
|
|
} catch (error) {
|
|
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function showHelp(): void {
|
|
console.log(`
|
|
lettabot-message - Send messages to channels
|
|
|
|
Commands:
|
|
send [options] Send a message
|
|
|
|
Send options:
|
|
--text, -t <text> Message text (required)
|
|
--channel, -c <name> Channel: telegram, slack, signal (default: last used)
|
|
--chat, --to <id> Chat/conversation ID (default: last messaged)
|
|
|
|
Examples:
|
|
# Send to last messaged user/channel
|
|
lettabot-message send --text "Hello!"
|
|
|
|
# Send to specific Telegram chat
|
|
lettabot-message send --text "Hello!" --channel telegram --chat 123456789
|
|
|
|
# Short form
|
|
lettabot-message send -t "Done!" -c telegram -to 123456789
|
|
|
|
Environment variables:
|
|
TELEGRAM_BOT_TOKEN Required for Telegram
|
|
SLACK_BOT_TOKEN Required for Slack
|
|
SIGNAL_PHONE_NUMBER Required for Signal
|
|
SIGNAL_CLI_REST_API_URL Signal API URL (default: http://localhost:8080)
|
|
`);
|
|
}
|
|
|
|
// Main
|
|
const args = process.argv.slice(2);
|
|
const command = args[0];
|
|
|
|
switch (command) {
|
|
case 'send':
|
|
sendCommand(args.slice(1));
|
|
break;
|
|
|
|
case 'help':
|
|
case '--help':
|
|
case '-h':
|
|
showHelp();
|
|
break;
|
|
|
|
default:
|
|
if (command) {
|
|
// Assume it's send with args starting with the command
|
|
// e.g., `lettabot-message --text "Hi"` (no 'send' subcommand)
|
|
if (command.startsWith('-')) {
|
|
sendCommand(args);
|
|
break;
|
|
}
|
|
console.error(`Unknown command: ${command}`);
|
|
}
|
|
showHelp();
|
|
break;
|
|
}
|