feat: add Telegram connector package for mesh-to-chat bridging
Introduces @claudemesh/connector-telegram — a standalone bridge process that joins a mesh as peerType: "connector" and relays messages bidirectionally between a Telegram chat and mesh peers via long polling. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
96
packages/connector-telegram/src/bridge.ts
Normal file
96
packages/connector-telegram/src/bridge.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Bidirectional bridge between Telegram and a claudemesh mesh.
|
||||
*
|
||||
* Telegram -> Mesh: incoming Telegram messages are formatted as
|
||||
* "[TelegramUser] message" and broadcast to the mesh.
|
||||
*
|
||||
* Mesh -> Telegram: inbound mesh pushes are formatted as
|
||||
* "[MeshPeerName] message" and posted to the Telegram chat.
|
||||
*/
|
||||
|
||||
import type { TelegramClient, TelegramMessage } from "./telegram.js";
|
||||
import type { MeshClient, InboundPush } from "./mesh-client.js";
|
||||
|
||||
export class Bridge {
|
||||
constructor(
|
||||
private telegram: TelegramClient,
|
||||
private mesh: MeshClient,
|
||||
) {}
|
||||
|
||||
/** Wire up both directions. Call once after both clients are connected. */
|
||||
start(): void {
|
||||
// Telegram -> Mesh
|
||||
this.telegram.onMessage((msg: TelegramMessage) => {
|
||||
this.handleTelegramMessage(msg);
|
||||
});
|
||||
|
||||
// Mesh -> Telegram
|
||||
this.mesh.onPush((push: InboundPush) => {
|
||||
this.handleMeshPush(push);
|
||||
});
|
||||
|
||||
console.log("[bridge] relay active");
|
||||
}
|
||||
|
||||
private handleTelegramMessage(msg: TelegramMessage): void {
|
||||
if (!msg.text) {
|
||||
// Skip non-text messages (photos, stickers, etc.)
|
||||
const type = msg.from
|
||||
? "non-text content"
|
||||
: "system message";
|
||||
console.log(`[bridge] skipping ${type} from Telegram`);
|
||||
return;
|
||||
}
|
||||
|
||||
const senderName = formatTelegramSender(msg);
|
||||
const meshMessage = `[${senderName}] ${msg.text}`;
|
||||
|
||||
console.log(`[bridge] tg->mesh: ${meshMessage.slice(0, 80)}...`);
|
||||
|
||||
// Broadcast to all mesh peers
|
||||
this.mesh.send("*", meshMessage).catch((err) => {
|
||||
console.error(`[bridge] failed to relay to mesh:`, err);
|
||||
});
|
||||
}
|
||||
|
||||
private handleMeshPush(push: InboundPush): void {
|
||||
// Decode the message content
|
||||
const plaintext = push.plaintext ?? tryDecodeBase64(push.ciphertext);
|
||||
if (!plaintext) return;
|
||||
|
||||
// Skip messages that originated from this connector (prevent echo)
|
||||
if (push.senderPubkey === this.mesh.pubkey) return;
|
||||
|
||||
// Find the sender's display name from the push metadata
|
||||
const senderName = push.senderDisplayName || push.senderPubkey.slice(0, 8);
|
||||
const telegramMessage = `<b>[${escapeHtml(senderName)}]</b> ${escapeHtml(plaintext)}`;
|
||||
|
||||
console.log(`[bridge] mesh->tg: [${senderName}] ${plaintext.slice(0, 60)}...`);
|
||||
|
||||
this.telegram.sendMessage(telegramMessage).catch((err) => {
|
||||
console.error(`[bridge] failed to relay to Telegram:`, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function formatTelegramSender(msg: TelegramMessage): string {
|
||||
if (!msg.from) return "Unknown";
|
||||
const parts = [msg.from.first_name];
|
||||
if (msg.from.last_name) parts.push(msg.from.last_name);
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function tryDecodeBase64(b64: string): string | null {
|
||||
try {
|
||||
return Buffer.from(b64, "base64").toString("utf-8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
Reference in New Issue
Block a user