diff --git a/packages/targets/chat-telegram/src/index.test.ts b/packages/targets/chat-telegram/src/index.test.ts index 746ef6ec..6ae75541 100644 --- a/packages/targets/chat-telegram/src/index.test.ts +++ b/packages/targets/chat-telegram/src/index.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { contractTestTarget, fakeShipContext } from '@profullstack/sh1pt-core/testing'; +import { contractTestTarget, fakeBuildContext, fakeShipContext } from '@profullstack/sh1pt-core/testing'; +import { join } from 'node:path'; import adapter from './index.js'; contractTestTarget(adapter, { @@ -15,6 +16,16 @@ afterEach(() => { }); describe('chat-telegram API calls', () => { + it('sanitizes botUsername when building the manifest artifact path', async () => { + const outDir = '/tmp/sh1pt-out'; + const result = await adapter.build(fakeBuildContext({ outDir }) as any, { + botUsername: '../demo/bot', + webhookUrl: 'https://example.com/telegram', + }); + + expect(result.artifact).toBe(join(outDir, 'telegram-___demo_bot.json')); + }); + it('sets webhook, commands, and bot descriptions', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, diff --git a/packages/targets/chat-telegram/src/index.ts b/packages/targets/chat-telegram/src/index.ts index f96bbed6..f391434a 100644 --- a/packages/targets/chat-telegram/src/index.ts +++ b/packages/targets/chat-telegram/src/index.ts @@ -1,9 +1,10 @@ import { defineTarget, manualSetup } from '@profullstack/sh1pt-core'; +import { join } from 'node:path'; -// Telegram bots. No "store" — a bot is just a token + webhook URL. This +// Telegram bots. No "store" means a bot is just a token + webhook URL. This // adapter registers the webhook with Telegram, sets commands/description/ // about text, and optionally submits to bot directories (t.me/BotFather, -// storebot.me, combot.org). Hosting the bot itself is orthogonal — pair +// storebot.me, combot.org). Hosting the bot itself is orthogonal, pair // with deploy-workers / deploy-fly. interface Config { botUsername: string; // e.g. 'my_sh1pt_bot' (no @) @@ -30,17 +31,18 @@ export default defineTarget({ kind: 'chat', label: 'Telegram Bot', async build(ctx, config) { - ctx.log(`telegram · prepare bot manifest for @${config.botUsername}`); - return { artifact: `${ctx.outDir}/telegram-${config.botUsername}.json` }; + const username = normalizeUsername(config.botUsername); + ctx.log(`telegram prepare bot manifest for @${username}`); + return { artifact: join(ctx.outDir, `telegram-${safeFilename(username)}.json`) }; }, async ship(ctx, config) { const username = normalizeUsername(config.botUsername); - ctx.log(`telegram · setWebhook + setMyCommands for @${username}`); + ctx.log(`telegram setWebhook + setMyCommands for @${username}`); if (ctx.dryRun) return { id: 'dry-run' }; const tokenKey = config.tokenKey ?? 'TELEGRAM_BOT_TOKEN'; const token = ctx.secret(tokenKey); - if (!token) throw new Error(`${tokenKey} not in vault — run: sh1pt secret set ${tokenKey} `); + if (!token) throw new Error(`${tokenKey} not in vault - run: sh1pt secret set ${tokenKey} `); await callTelegram(ctx.log, token, 'setWebhook', { url: config.webhookUrl, @@ -71,8 +73,8 @@ export default defineTarget({ label: "Telegram Bot (@BotFather)", vendorDocUrl: "https://t.me/BotFather", steps: [ - "Open Telegram \u2192 chat with @BotFather \u2192 /newbot", - "Copy the HTTP API token \u2014 sh1pt will store it", + "Open Telegram -> chat with @BotFather -> /newbot", + "Copy the HTTP API token - sh1pt will store it", "Run: sh1pt secret set TELEGRAM_BOT_TOKEN ", ], }), @@ -84,7 +86,7 @@ async function callTelegram( method: string, body: Record, ): Promise { - log(`telegram · ${method}`); + log(`telegram ${method}`); const res = await fetch(`https://api.telegram.org/bot${token}/${method}`, { method: 'POST', headers: { 'content-type': 'application/json' }, @@ -104,6 +106,10 @@ function normalizeUsername(username: string): string { return clean; } +function safeFilename(value: string): string { + return value.replace(/[^a-zA-Z0-9_-]/g, '_'); +} + function normalizeCommand(command: TelegramCommand): TelegramCommand { return { command: command.command.replace(/^\//, ''), @@ -113,6 +119,6 @@ function normalizeCommand(command: TelegramCommand): TelegramCommand { function requireSecret(ctx: { secret(key: string): string | undefined }, key: string): string { const value = ctx.secret(key); - if (!value) throw new Error(`${key} not in vault — run: sh1pt secret set ${key} `); + if (!value) throw new Error(`${key} not in vault - run: sh1pt secret set ${key} `); return value; }