From 978ca2a169c83ad552053740caf41eddb40dff5f Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 25 Mar 2026 19:04:13 +0000 Subject: [PATCH 1/3] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/xlabtg/teleton-plugins/issues/44 --- .gitkeep | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitkeep b/.gitkeep index c46a5b9..3e4d17e 100644 --- a/.gitkeep +++ b/.gitkeep @@ -1 +1,2 @@ -# .gitkeep file auto-generated at 2026-03-19T10:37:13.073Z for PR creation at branch issue-19-f54b585823d1 for issue https://github.com/xlabtg/teleton-plugins/issues/19 \ No newline at end of file +# .gitkeep file auto-generated at 2026-03-19T10:37:13.073Z for PR creation at branch issue-19-f54b585823d1 for issue https://github.com/xlabtg/teleton-plugins/issues/19 +# Updated: 2026-03-25T19:04:13.644Z \ No newline at end of file From d8d243a8796d1f31631cd0e19039abfb6e329965 Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 25 Mar 2026 19:08:17 +0000 Subject: [PATCH 2/3] feat(ton-bridge): add inline keyboard URL button support Replace plain text link with a proper Telegram inline keyboard URL button. `buildMessageWithLink` now returns `{ text, opts }` where `opts.inlineKeyboard` contains the TON Bridge Mini App URL button. All three tools (`ton_bridge_open`, `ton_bridge_about`, `ton_bridge_custom_message`) pass the opts to `sdk.telegram.sendMessage`. Tests updated to verify inline keyboard structure, button text (with and without emoji), and URL content. README examples updated. Fixes xlabtg/teleton-plugins#44 Co-Authored-By: Claude Sonnet 4.6 --- plugins/ton-bridge/README.md | 11 +- plugins/ton-bridge/index.js | 34 +++--- plugins/ton-bridge/tests/index.test.js | 145 ++++++++++++++++++------- 3 files changed, 131 insertions(+), 59 deletions(-) diff --git a/plugins/ton-bridge/README.md b/plugins/ton-bridge/README.md index dbc328c..760a8c8 100644 --- a/plugins/ton-bridge/README.md +++ b/plugins/ton-bridge/README.md @@ -24,11 +24,12 @@ Restart Teleton — the plugin is auto-loaded from `~/.teleton/plugins/`. No cha ## Usage examples -- "Open TON Bridge" -- "Tell me about TON Bridge" -- "Send a message about TON Bridge with a button" -- "Open TON Bridge, no emoji on the button" -- "Share a TON Bridge link with the text: Transfer your assets seamlessly" +- "Open TON Bridge" → sends message with `[TON Bridge No1]` button +- "Open TON Bridge, emoji on the button" → sends message with `[🚀 TON Bridge]` button +- "Open TON Bridge, no emoji on the button" → sends message with `[TON Bridge]` button +- "Tell me about TON Bridge" → info message with inline button +- "Send a message about TON Bridge with a button" → custom message with inline button +- "Share a TON Bridge link with the text: Transfer your assets seamlessly" → custom text + inline button ## Configuration diff --git a/plugins/ton-bridge/index.js b/plugins/ton-bridge/index.js index 2282083..2f4e90f 100644 --- a/plugins/ton-bridge/index.js +++ b/plugins/ton-bridge/index.js @@ -4,8 +4,7 @@ * Provides LLM-callable tools to share the TON Bridge Mini App link. * Pattern B (SDK) — uses sdk.pluginConfig, sdk.log, sdk.telegram.sendMessage * - * Sends messages with the Mini App deep link embedded in the text. - * The link renders as a tappable URL in DMs, groups, and channels. + * Sends messages with an inline keyboard URL button for the Mini App link. */ // ─── Manifest (inline) ──────────────────────────────────────────────────────── @@ -34,14 +33,18 @@ function buildUrl(startParam) { } /** - * Build the message text with the Mini App link appended. - * sdk.telegram.sendMessage inlineKeyboard only supports callback_data buttons; - * URL buttons cause Buffer.from(undefined) inside TelegramBridge. Instead we - * embed the link directly in the message text where Telegram renders it as a - * tappable deep link. + * Build the message options with an inline keyboard URL button. + * Returns the message text and inlineKeyboard opts for sdk.telegram.sendMessage. */ function buildMessageWithLink(text, buttonText, url) { - return `${text}\n\n${buttonText}: ${url}`; + return { + text, + opts: { + inlineKeyboard: [ + [{ text: buttonText, url }], + ], + }, + }; } // ─── Tools ──────────────────────────────────────────────────────────────────── @@ -84,11 +87,12 @@ export const tools = (sdk) => [ `ton_bridge_open called by ${context?.senderId ?? "unknown"}` ); - const fullText = buildMessageWithLink(text, buttonText, url); + const { text: fullText, opts } = buildMessageWithLink(text, buttonText, url); const messageId = await sdk.telegram.sendMessage( context.chatId, - fullText + fullText, + opts ); return { @@ -134,11 +138,12 @@ export const tools = (sdk) => [ ); const aboutText = "About TON Bridge\n\nTON Bridge is the #1 bridge in the TON Catalog. Transfer assets across chains seamlessly via the official Mini App."; - const fullText = buildMessageWithLink(aboutText, buttonText, url); + const { text: fullText, opts } = buildMessageWithLink(aboutText, buttonText, url); const messageId = await sdk.telegram.sendMessage( context.chatId, - fullText + fullText, + opts ); return { @@ -195,11 +200,12 @@ export const tools = (sdk) => [ `ton_bridge_custom_message called by ${context?.senderId ?? "unknown"}` ); - const fullText = buildMessageWithLink(customMessage, buttonText, url); + const { text: fullText, opts } = buildMessageWithLink(customMessage, buttonText, url); const messageId = await sdk.telegram.sendMessage( context.chatId, - fullText + fullText, + opts ); return { diff --git a/plugins/ton-bridge/tests/index.test.js b/plugins/ton-bridge/tests/index.test.js index c16ae8b..5ccd095 100644 --- a/plugins/ton-bridge/tests/index.test.js +++ b/plugins/ton-bridge/tests/index.test.js @@ -135,7 +135,6 @@ describe("ton-bridge plugin", () => { assert.equal(result.data.chat_id, 111); assert.equal(capturedChatId, 111); assert.ok(capturedText, "message text should be provided"); - assert.ok(capturedText.includes("TONBridge_robot"), "message text should include Mini App link"); }); it("uses custom message when provided", async () => { @@ -154,52 +153,43 @@ describe("ton-bridge plugin", () => { }); it("uses custom buttonText when provided", async () => { - let capturedText; + let capturedOpts; const sdk = makeSdk({ telegram: { - sendMessage: async (chatId, text) => { - capturedText = text; + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; return 1; }, }, }); const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); await tool.execute({ buttonText: "Open Bridge" }, makeContext()); - assert.ok(capturedText.includes("Open Bridge"), "message should include custom button text"); + assert.ok( + capturedOpts?.inlineKeyboard?.[0]?.[0]?.text === "Open Bridge", + "inline keyboard button should have custom button text" + ); }); it("falls back to sdk.pluginConfig.buttonText when no buttonText param", async () => { - let capturedText; + let capturedOpts; const sdk = makeSdk({ pluginConfig: { buttonText: "My Bridge Button", startParam: "" }, telegram: { - sendMessage: async (chatId, text) => { - capturedText = text; - return 1; - }, - }, - }); - const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); - await tool.execute({}, makeContext()); - assert.ok(capturedText.includes("My Bridge Button"), "message should include config button text"); - }); - - it("message text includes TON Bridge URL", async () => { - let capturedText; - const sdk = makeSdk({ - telegram: { - sendMessage: async (chatId, text) => { - capturedText = text; + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; return 1; }, }, }); const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); await tool.execute({}, makeContext()); - assert.ok(capturedText.includes("TONBridge_robot"), `message should include TONBridge_robot URL, got: ${capturedText}`); + assert.ok( + capturedOpts?.inlineKeyboard?.[0]?.[0]?.text === "My Bridge Button", + "inline keyboard button should have config button text" + ); }); - it("does not pass inlineKeyboard option to sendMessage", async () => { + it("passes inlineKeyboard with TON Bridge URL button to sendMessage", async () => { let capturedOpts; const sdk = makeSdk({ telegram: { @@ -211,7 +201,13 @@ describe("ton-bridge plugin", () => { }); const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); await tool.execute({}, makeContext()); - assert.equal(capturedOpts, undefined, "should not pass options with inlineKeyboard"); + assert.ok(capturedOpts, "opts should be passed to sendMessage"); + assert.ok(capturedOpts.inlineKeyboard, "opts should have inlineKeyboard"); + assert.ok(Array.isArray(capturedOpts.inlineKeyboard), "inlineKeyboard should be an array"); + const button = capturedOpts.inlineKeyboard[0][0]; + assert.ok(button, "should have at least one button in first row"); + assert.ok(button.url, "button should have a url property"); + assert.ok(button.url.includes("TONBridge_robot"), `button url should include Mini App link, got: ${button.url}`); }); it("returns failure when sendMessage throws", async () => { @@ -251,6 +247,24 @@ describe("ton-bridge plugin", () => { assert.ok(capturedText.toLowerCase().includes("bridge"), "about message should mention bridge"); }); + it("passes inlineKeyboard with URL button to sendMessage", async () => { + let capturedOpts; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_about"); + await tool.execute({}, makeContext()); + assert.ok(capturedOpts?.inlineKeyboard, "opts should have inlineKeyboard"); + const button = capturedOpts.inlineKeyboard[0][0]; + assert.ok(button.url, "button should have a url property"); + assert.ok(button.url.includes("TONBridge_robot"), `button url should include Mini App link, got: ${button.url}`); + }); + it("returns failure when sendMessage throws", async () => { const sdk = makeSdk({ telegram: { @@ -265,11 +279,12 @@ describe("ton-bridge plugin", () => { describe("ton_bridge_custom_message", () => { it("sends customMessage as text", async () => { - let capturedText; + let capturedText, capturedOpts; const sdk = makeSdk({ telegram: { - sendMessage: async (chatId, text) => { + sendMessage: async (chatId, text, opts) => { capturedText = text; + capturedOpts = opts; return 1; }, }, @@ -277,7 +292,7 @@ describe("ton-bridge plugin", () => { const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_custom_message"); await tool.execute({ customMessage: "Hello TON!" }, makeContext()); assert.ok(capturedText.startsWith("Hello TON!"), "message should start with custom message"); - assert.ok(capturedText.includes("TONBridge_robot"), "message should include Mini App link"); + assert.ok(capturedOpts?.inlineKeyboard?.[0]?.[0]?.url?.includes("TONBridge_robot"), "inline keyboard button url should include Mini App link"); }); it("returns success with message_id and chat_id", async () => { @@ -341,39 +356,89 @@ describe("ton-bridge plugin", () => { }); }); + describe("inline keyboard URL button", () => { + it("button text matches configured buttonText", async () => { + let capturedOpts; + const sdk = makeSdk({ + pluginConfig: { buttonText: "🚀 TON Bridge", startParam: "" }, + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({}, makeContext()); + assert.equal(capturedOpts?.inlineKeyboard?.[0]?.[0]?.text, "🚀 TON Bridge", "button text should include emoji when configured"); + }); + + it("button text works without emoji", async () => { + let capturedOpts; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({ buttonText: "TON Bridge" }, makeContext()); + assert.equal(capturedOpts?.inlineKeyboard?.[0]?.[0]?.text, "TON Bridge", "button text should work without emoji"); + }); + + it("inline keyboard has exactly one row with one button", async () => { + let capturedOpts; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({}, makeContext()); + assert.equal(capturedOpts.inlineKeyboard.length, 1, "should have one row"); + assert.equal(capturedOpts.inlineKeyboard[0].length, 1, "should have one button in the row"); + }); + }); + describe("startParam URL building", () => { - it("appends startParam to URL when set", async () => { - let capturedText; + it("appends startParam to URL in button when set", async () => { + let capturedOpts; const sdk = makeSdk({ pluginConfig: { buttonText: "Bridge", startParam: "myref" }, telegram: { - sendMessage: async (chatId, text) => { - capturedText = text; + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; return 1; }, }, }); const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); await tool.execute({}, makeContext()); - assert.ok(capturedText.includes("myref"), `message should include startParam, got: ${capturedText}`); + const buttonUrl = capturedOpts?.inlineKeyboard?.[0]?.[0]?.url; + assert.ok(buttonUrl?.includes("myref"), `button url should include startParam, got: ${buttonUrl}`); }); it("does not append startParam when empty", async () => { - let capturedText; + let capturedOpts; const sdk = makeSdk({ pluginConfig: { buttonText: "Bridge", startParam: "" }, telegram: { - sendMessage: async (chatId, text) => { - capturedText = text; + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; return 1; }, }, }); const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); await tool.execute({}, makeContext()); - // URL in message should be the base URL without extra params appended via = - assert.ok(capturedText.includes("startapp"), `message should include base URL ending with 'startapp'`); - assert.ok(!capturedText.includes("startapp="), `message URL should not have startParam appended, got: ${capturedText}`); + const buttonUrl = capturedOpts?.inlineKeyboard?.[0]?.[0]?.url; + assert.ok(buttonUrl?.includes("startapp"), `button url should include base URL ending with 'startapp'`); + assert.ok(!buttonUrl?.includes("startapp="), `button url should not have startParam appended, got: ${buttonUrl}`); }); }); }); From d6df6012ae29c051d2483a540515b68a023216fd Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 25 Mar 2026 19:08:50 +0000 Subject: [PATCH 3/3] Revert "Initial commit with task details" This reverts commit 978ca2a169c83ad552053740caf41eddb40dff5f. --- .gitkeep | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitkeep b/.gitkeep index 3e4d17e..c46a5b9 100644 --- a/.gitkeep +++ b/.gitkeep @@ -1,2 +1 @@ -# .gitkeep file auto-generated at 2026-03-19T10:37:13.073Z for PR creation at branch issue-19-f54b585823d1 for issue https://github.com/xlabtg/teleton-plugins/issues/19 -# Updated: 2026-03-25T19:04:13.644Z \ No newline at end of file +# .gitkeep file auto-generated at 2026-03-19T10:37:13.073Z for PR creation at branch issue-19-f54b585823d1 for issue https://github.com/xlabtg/teleton-plugins/issues/19 \ No newline at end of file