Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d72443ec3 | |||
| 41a685d22e | |||
|
|
cd405d1f71 |
@@ -395,6 +395,7 @@
|
||||
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel"></div>
|
||||
|
||||
<script src="./boot.js"></script>
|
||||
<script src="./js/nexus-telegram-bridge.js"></script>
|
||||
<script src="./avatar-customization.js"></script>
|
||||
<script src="./lod-system.js"></script>
|
||||
<script>
|
||||
|
||||
339
js/nexus-telegram-bridge.js
Normal file
339
js/nexus-telegram-bridge.js
Normal file
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* Nexus-Telegram Bridge
|
||||
* Issue #1537: feat: bridge Nexus chat to Hermes Telegram gateway
|
||||
*
|
||||
* Bidirectional bridge between Nexus world chat and Telegram.
|
||||
* - Nexus chat messages forwarded to Telegram
|
||||
* - Telegram messages appear in Nexus chat
|
||||
* - Bidirectional, near-realtime (<5s latency)
|
||||
*/
|
||||
|
||||
class NexusTelegramBridge {
|
||||
constructor(options = {}) {
|
||||
this.telegramToken = options.telegramToken || process.env.TELEGRAM_BOT_TOKEN;
|
||||
this.telegramChatId = options.telegramChatId || process.env.TELEGRAM_CHAT_ID;
|
||||
this.nexusWsUrl = options.nexusWsUrl || 'ws://localhost:8765';
|
||||
this.pollInterval = options.pollInterval || 5000; // 5 seconds
|
||||
|
||||
this.nexusWs = null;
|
||||
this.telegramPollingInterval = null;
|
||||
this.isConnected = false;
|
||||
this.lastTelegramUpdateId = 0;
|
||||
|
||||
// Callbacks
|
||||
this.onNexusMessage = options.onNexusMessage || (() => {});
|
||||
this.onTelegramMessage = options.onTelegramMessage || (() => {});
|
||||
this.onError = options.onError || console.error;
|
||||
|
||||
// Message queue for offline handling
|
||||
this.messageQueue = [];
|
||||
|
||||
// Bind methods
|
||||
this.connectToNexus = this.connectToNexus.bind(this);
|
||||
this.disconnect = this.disconnect.bind(this);
|
||||
this.sendToTelegram = this.sendToTelegram.bind(this);
|
||||
this.sendToNexus = this.sendToNexus.bind(this);
|
||||
this.pollTelegram = this.pollTelegram.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the bridge
|
||||
*/
|
||||
async init() {
|
||||
console.log('Initializing Nexus-Telegram Bridge...');
|
||||
|
||||
// Validate configuration
|
||||
if (!this.telegramToken) {
|
||||
throw new Error('Telegram bot token required. Set TELEGRAM_BOT_TOKEN environment variable.');
|
||||
}
|
||||
|
||||
if (!this.telegramChatId) {
|
||||
throw new Error('Telegram chat ID required. Set TELEGRAM_CHAT_ID environment variable.');
|
||||
}
|
||||
|
||||
// Connect to Nexus WebSocket
|
||||
await this.connectToNexus();
|
||||
|
||||
// Start polling Telegram
|
||||
this.startTelegramPolling();
|
||||
|
||||
console.log('Nexus-Telegram Bridge initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Nexus WebSocket
|
||||
*/
|
||||
async connectToNexus() {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
console.log(`Connecting to Nexus at ${this.nexusWsUrl}...`);
|
||||
|
||||
this.nexusWs = new WebSocket(this.nexusWsUrl);
|
||||
|
||||
this.nexusWs.onopen = () => {
|
||||
console.log('Connected to Nexus WebSocket');
|
||||
this.isConnected = true;
|
||||
|
||||
// Send any queued messages
|
||||
this.processMessageQueue();
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.nexusWs.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleNexusMessage(data);
|
||||
} catch (error) {
|
||||
this.onError('Failed to parse Nexus message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
this.nexusWs.onclose = (event) => {
|
||||
console.log('Nexus WebSocket closed:', event.code, event.reason);
|
||||
this.isConnected = false;
|
||||
|
||||
// Attempt reconnect after delay
|
||||
setTimeout(() => {
|
||||
if (!this.isConnected) {
|
||||
console.log('Attempting to reconnect to Nexus...');
|
||||
this.connectToNexus().catch(this.onError);
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
this.nexusWs.onerror = (error) => {
|
||||
this.onError('Nexus WebSocket error:', error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.onError('Failed to connect to Nexus:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle message from Nexus
|
||||
*/
|
||||
handleNexusMessage(data) {
|
||||
// Filter for chat messages
|
||||
if (data.type === 'chat' && data.text) {
|
||||
console.log('Nexus message:', data.text);
|
||||
|
||||
// Forward to Telegram
|
||||
this.sendToTelegram(data.text, data.agent || 'Nexus');
|
||||
|
||||
// Call callback
|
||||
this.onNexusMessage(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start polling Telegram for new messages
|
||||
*/
|
||||
startTelegramPolling() {
|
||||
if (this.telegramPollingInterval) {
|
||||
clearInterval(this.telegramPollingInterval);
|
||||
}
|
||||
|
||||
console.log(`Starting Telegram polling every ${this.pollInterval / 1000}s...`);
|
||||
|
||||
// Initial poll
|
||||
this.pollTelegram();
|
||||
|
||||
// Set up interval
|
||||
this.telegramPollingInterval = setInterval(this.pollTelegram, this.pollInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll Telegram for new messages
|
||||
*/
|
||||
async pollTelegram() {
|
||||
try {
|
||||
const url = `https://api.telegram.org/bot${this.telegramToken}/getUpdates?offset=${this.lastTelegramUpdateId + 1}&timeout=10`;
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.ok && data.result) {
|
||||
for (const update of data.result) {
|
||||
this.handleTelegramUpdate(update);
|
||||
this.lastTelegramUpdateId = update.update_id;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.onError('Failed to poll Telegram:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle update from Telegram
|
||||
*/
|
||||
handleTelegramUpdate(update) {
|
||||
if (update.message && update.message.text) {
|
||||
const message = update.message;
|
||||
const text = message.text;
|
||||
const from = message.from.first_name || message.from.username || 'Telegram User';
|
||||
|
||||
console.log(`Telegram message from ${from}:`, text);
|
||||
|
||||
// Forward to Nexus
|
||||
this.sendToNexus(text, from);
|
||||
|
||||
// Call callback
|
||||
this.onTelegramMessage({
|
||||
text: text,
|
||||
from: from,
|
||||
timestamp: new Date(message.date * 1000).toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to Telegram
|
||||
*/
|
||||
async sendToTelegram(text, sender = 'Nexus') {
|
||||
try {
|
||||
const url = `https://api.telegram.org/bot${this.telegramToken}/sendMessage`;
|
||||
|
||||
const payload = {
|
||||
chat_id: this.telegramChatId,
|
||||
text: `[${sender}]: ${text}`,
|
||||
parse_mode: 'HTML'
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.ok) {
|
||||
this.onError('Failed to send to Telegram:', result);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.onError('Error sending to Telegram:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to Nexus
|
||||
*/
|
||||
sendToNexus(text, sender = 'Telegram') {
|
||||
if (!this.isConnected || !this.nexusWs) {
|
||||
// Queue message for later
|
||||
this.messageQueue.push({ text, sender });
|
||||
console.log('Message queued (not connected to Nexus)');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = {
|
||||
type: 'chat',
|
||||
text: text,
|
||||
agent: sender,
|
||||
timestamp: Date.now(),
|
||||
source: 'telegram'
|
||||
};
|
||||
|
||||
this.nexusWs.send(JSON.stringify(message));
|
||||
console.log(`Sent to Nexus: [${sender}] ${text}`);
|
||||
|
||||
} catch (error) {
|
||||
this.onError('Failed to send to Nexus:', error);
|
||||
// Queue for retry
|
||||
this.messageQueue.push({ text, sender });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process queued messages
|
||||
*/
|
||||
processMessageQueue() {
|
||||
if (this.messageQueue.length === 0) return;
|
||||
|
||||
console.log(`Processing ${this.messageQueue.length} queued messages...`);
|
||||
|
||||
while (this.messageQueue.length > 0 && this.isConnected) {
|
||||
const { text, sender } = this.messageQueue.shift();
|
||||
this.sendToNexus(text, sender);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from Nexus
|
||||
*/
|
||||
disconnect() {
|
||||
console.log('Disconnecting Nexus-Telegram Bridge...');
|
||||
|
||||
// Stop Telegram polling
|
||||
if (this.telegramPollingInterval) {
|
||||
clearInterval(this.telegramPollingInterval);
|
||||
this.telegramPollingInterval = null;
|
||||
}
|
||||
|
||||
// Close Nexus WebSocket
|
||||
if (this.nexusWs) {
|
||||
this.nexusWs.close();
|
||||
this.nexusWs = null;
|
||||
}
|
||||
|
||||
this.isConnected = false;
|
||||
|
||||
console.log('Nexus-Telegram Bridge disconnected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bridge status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
connected: this.isConnected,
|
||||
nexusWsUrl: this.nexusWsUrl,
|
||||
telegramConfigured: !!this.telegramToken && !!this.telegramChatId,
|
||||
lastTelegramUpdateId: this.lastTelegramUpdateId,
|
||||
queuedMessages: this.messageQueue.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Telegram connection
|
||||
*/
|
||||
async testTelegramConnection() {
|
||||
try {
|
||||
const url = `https://api.telegram.org/bot${this.telegramToken}/getMe`;
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.ok) {
|
||||
console.log('Telegram bot connected:', data.result.username);
|
||||
return true;
|
||||
} else {
|
||||
this.onError('Telegram connection test failed:', data);
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.onError('Telegram connection test error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = NexusTelegramBridge;
|
||||
}
|
||||
|
||||
// Global instance for browser use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.NexusTelegramBridge = NexusTelegramBridge;
|
||||
}
|
||||
243
tests/test_nexus_telegram_bridge.js
Normal file
243
tests/test_nexus_telegram_bridge.js
Normal file
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Tests for Nexus-Telegram Bridge
|
||||
* Issue #1537: feat: bridge Nexus chat to Hermes Telegram gateway
|
||||
*/
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = async (url, options) => {
|
||||
if (url.includes('/getMe')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
ok: true,
|
||||
result: { username: 'test_bot' }
|
||||
})
|
||||
};
|
||||
} else if (url.includes('/getUpdates')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
ok: true,
|
||||
result: []
|
||||
})
|
||||
};
|
||||
} else if (url.includes('/sendMessage')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ ok: true })
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${url}`);
|
||||
};
|
||||
|
||||
// Mock WebSocket
|
||||
class MockWebSocket {
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.readyState = 1; // OPEN
|
||||
this.onopen = null;
|
||||
this.onmessage = null;
|
||||
this.onclose = null;
|
||||
this.onerror = null;
|
||||
|
||||
// Simulate connection
|
||||
setTimeout(() => {
|
||||
if (this.onopen) this.onopen();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
send(data) {
|
||||
// Mock send
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = 3; // CLOSED
|
||||
if (this.onclose) this.onclose({ code: 1000, reason: 'Normal closure' });
|
||||
}
|
||||
}
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
// Load nexus-telegram-bridge.js
|
||||
const bridgePath = path.join(ROOT, 'js', 'nexus-telegram-bridge.js');
|
||||
const bridgeCode = fs.readFileSync(bridgePath, 'utf8');
|
||||
|
||||
// Create VM context
|
||||
const context = {
|
||||
module: { exports: {} },
|
||||
exports: {},
|
||||
console,
|
||||
window: { location: { protocol: 'http:', hostname: 'localhost' } },
|
||||
fetch: global.fetch,
|
||||
WebSocket: global.WebSocket,
|
||||
setInterval: () => {},
|
||||
clearInterval: () => {},
|
||||
setTimeout: (fn, delay) => setTimeout(fn, delay),
|
||||
Date: Date
|
||||
};
|
||||
|
||||
// Execute in context
|
||||
const vm = require('node:vm');
|
||||
vm.runInNewContext(bridgeCode, context);
|
||||
|
||||
// Get NexusTelegramBridge
|
||||
const NexusTelegramBridge = context.module.exports;
|
||||
|
||||
test('NexusTelegramBridge loads correctly', () => {
|
||||
assert.ok(NexusTelegramBridge, 'NexusTelegramBridge should be defined');
|
||||
assert.ok(typeof NexusTelegramBridge === 'function', 'NexusTelegramBridge should be a constructor');
|
||||
});
|
||||
|
||||
test('NexusTelegramBridge can be instantiated', () => {
|
||||
const bridge = new NexusTelegramBridge({
|
||||
telegramToken: 'test_token',
|
||||
telegramChatId: 'test_chat_id'
|
||||
});
|
||||
|
||||
assert.ok(bridge, 'NexusTelegramBridge instance should be created');
|
||||
assert.equal(bridge.telegramToken, 'test_token', 'Should have telegram token');
|
||||
assert.equal(bridge.telegramChatId, 'test_chat_id', 'Should have telegram chat ID');
|
||||
assert.equal(bridge.pollInterval, 5000, 'Should have default poll interval');
|
||||
assert.ok(!bridge.isConnected, 'Should not be connected initially');
|
||||
});
|
||||
|
||||
test('NexusTelegramBridge validates configuration', () => {
|
||||
// Should throw without token
|
||||
assert.throws(
|
||||
() => new NexusTelegramBridge({}),
|
||||
{ message: /Telegram bot token required/ }
|
||||
);
|
||||
|
||||
// Should throw without chat ID
|
||||
assert.throws(
|
||||
() => new NexusTelegramBridge({ telegramToken: 'test' }),
|
||||
{ message: /Telegram chat ID required/ }
|
||||
);
|
||||
});
|
||||
|
||||
test('NexusTelegramBridge can connect to Nexus', async () => {
|
||||
const bridge = new NexusTelegramBridge({
|
||||
telegramToken: 'test_token',
|
||||
telegramChatId: 'test_chat_id',
|
||||
nexusWsUrl: 'ws://localhost:8765'
|
||||
});
|
||||
|
||||
// Mock WebSocket connection
|
||||
let connected = false;
|
||||
global.WebSocket = class extends MockWebSocket {
|
||||
constructor(url) {
|
||||
super(url);
|
||||
connected = true;
|
||||
}
|
||||
};
|
||||
|
||||
await bridge.connectToNexus();
|
||||
|
||||
assert.ok(connected, 'Should attempt to connect to Nexus');
|
||||
assert.ok(bridge.nexusWs, 'Should have WebSocket connection');
|
||||
});
|
||||
|
||||
test('NexusTelegramBridge handles Nexus messages', () => {
|
||||
const bridge = new NexusTelegramBridge({
|
||||
telegramToken: 'test_token',
|
||||
telegramChatId: 'test_chat_id'
|
||||
});
|
||||
|
||||
let messageReceived = false;
|
||||
bridge.onNexusMessage = (data) => {
|
||||
messageReceived = true;
|
||||
assert.equal(data.type, 'chat');
|
||||
assert.equal(data.text, 'Hello from Nexus');
|
||||
};
|
||||
|
||||
// Simulate message from Nexus
|
||||
bridge.handleNexusMessage({
|
||||
type: 'chat',
|
||||
text: 'Hello from Nexus',
|
||||
agent: 'User'
|
||||
});
|
||||
|
||||
assert.ok(messageReceived, 'Should call onNexusMessage callback');
|
||||
});
|
||||
|
||||
test('NexusTelegramBridge handles Telegram messages', () => {
|
||||
const bridge = new NexusTelegramBridge({
|
||||
telegramToken: 'test_token',
|
||||
telegramChatId: 'test_chat_id'
|
||||
});
|
||||
|
||||
let messageReceived = false;
|
||||
bridge.onTelegramMessage = (data) => {
|
||||
messageReceived = true;
|
||||
assert.equal(data.text, 'Hello from Telegram');
|
||||
assert.equal(data.from, 'Test User');
|
||||
};
|
||||
|
||||
// Simulate update from Telegram
|
||||
bridge.handleTelegramUpdate({
|
||||
update_id: 123,
|
||||
message: {
|
||||
text: 'Hello from Telegram',
|
||||
from: { first_name: 'Test User' },
|
||||
date: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
});
|
||||
|
||||
assert.ok(messageReceived, 'Should call onTelegramMessage callback');
|
||||
});
|
||||
|
||||
test('NexusTelegramBridge queues messages when disconnected', () => {
|
||||
const bridge = new NexusTelegramBridge({
|
||||
telegramToken: 'test_token',
|
||||
telegramChatId: 'test_chat_id'
|
||||
});
|
||||
|
||||
// Not connected
|
||||
assert.ok(!bridge.isConnected, 'Should not be connected');
|
||||
|
||||
// Send message (should queue)
|
||||
bridge.sendToNexus('Test message', 'Sender');
|
||||
|
||||
assert.equal(bridge.messageQueue.length, 1, 'Should queue message');
|
||||
assert.equal(bridge.messageQueue[0].text, 'Test message', 'Should queue correct message');
|
||||
});
|
||||
|
||||
test('NexusTelegramBridge gets status', () => {
|
||||
const bridge = new NexusTelegramBridge({
|
||||
telegramToken: 'test_token',
|
||||
telegramChatId: 'test_chat_id'
|
||||
});
|
||||
|
||||
const status = bridge.getStatus();
|
||||
|
||||
assert.ok(status, 'Should return status object');
|
||||
assert.equal(status.connected, false, 'Should not be connected');
|
||||
assert.equal(status.telegramConfigured, true, 'Should be configured');
|
||||
assert.equal(status.queuedMessages, 0, 'Should have 0 queued messages');
|
||||
});
|
||||
|
||||
test('NexusTelegramBridge can be disconnected', () => {
|
||||
const bridge = new NexusTelegramBridge({
|
||||
telegramToken: 'test_token',
|
||||
telegramChatId: 'test_chat_id'
|
||||
});
|
||||
|
||||
// Mock WebSocket
|
||||
bridge.nexusWs = { close: () => {} };
|
||||
bridge.isConnected = true;
|
||||
bridge.telegramPollingInterval = setInterval(() => {}, 1000);
|
||||
|
||||
bridge.disconnect();
|
||||
|
||||
assert.ok(!bridge.isConnected, 'Should not be connected after disconnect');
|
||||
assert.equal(bridge.nexusWs, null, 'Should clear WebSocket');
|
||||
});
|
||||
|
||||
console.log('All NexusTelegramBridge tests passed!');
|
||||
Reference in New Issue
Block a user