diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..185ff2e --- /dev/null +++ b/.eslintrc @@ -0,0 +1 @@ +{"extends": "standard"} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3e1e9c8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: ci + +on: + push: + paths-ignore: + - 'docs/**' + - '*.md' + pull_request: + paths-ignore: + - 'docs/**' + - '*.md' + +jobs: + test: + permissions: + contents: write + pull-requests: write + uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3 + with: + lint: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index c6bba59..cab5773 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* +.vscode/ +package-lock.json diff --git a/README.md b/README.md index ca40554..a406ebe 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,55 @@ -# telegraf-save-md-reply +# telegraf-safe-md-reply + Reply safely with markdown! + +Are you tired of this error?! + +``` +"type": "TelegramError", +"message": "400: Bad Request: can't parse entities: Character '-' is reserved and must be escaped with the preceding '\\'", +``` + +This package is for you! + +## Usage + +The `telegraf-safe-md-reply` middleware will add new methods to the `ctx` object: + +- `replyWithSafeMarkdownV2`: reply with markdown escaping all the reserved characters +- `escapeMarkdown`: escape a string escaping all the reserved markdown characters + +```js +const Telegraf = require('telegraf') +const safeReply = require('telegraf-safe-md-reply') + +const bot = new Telegraf(process.env.BOT_TOKEN) + +bot.use(safeReply()) + +bot.command('test', async (ctx) => { + // use the new method to reply + ctx.replyWithSafeMarkdownV2('Hello-World(?)') + + // or escape manually: + ctx.replyWithMarkdownV2(`*Hello*${ctx.escapeMarkdown('-World(?)')}`) +}) +``` + +## Options + +You can pass an object with options to the middleware: + +```js +bot.use(safeReply({ + methodName: 'safeReply' +})) + +bot.command('test', (ctx) => { + ctx.safeReply('Hello-World(?)') +}) +``` + + +## License + +Copyright [Manuel Spigolon](https://github.com/Eomm), Licensed under [MIT](./LICENSE). diff --git a/index.js b/index.js new file mode 100644 index 0000000..02814bd --- /dev/null +++ b/index.js @@ -0,0 +1,26 @@ +'use strict' + +module.exports = function helpMiddleware ({ + methodName = 'replyWithSafeMarkdownV2' +} = {}) { + return function middleware (ctx, next) { + ctx[methodName] = function (text, extra) { + return ctx.replyWithMarkdownV2(escapeMarkdown(text), extra) + } + + ctx.escapeMarkdown = escapeMarkdown + + return next() + } +} + +// https://core.telegram.org/bots/api#markdownv2-style +const SPECIAL_CHARS = [ + '\\', '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!' +] + +const regex = new RegExp(`[${SPECIAL_CHARS.join('\\')}]`, 'ig') + +function escapeMarkdown (text) { + return text.replace(regex, '\\$&') +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ea585ba --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "telegraf-safe-md-reply", + "version": "0.0.0", + "description": "Reply safely with markdown!", + "main": "index.js", + "scripts": { + "lint": "standard", + "lint:fix": "standard --fix", + "test": "tap test/*.js" + }, + "keywords": [ + "telegraf", + "telegram", + "bot-middleware", + "middleware", + "markdown", + "reply" + ], + "author": "Manuel Spigolon (https://github.com/Eomm)", + "funding": "https://github.com/Eomm/telegraf-safe-md-reply?sponsor=1", + "license": "MIT", + "bugs": { + "url": "https://github.com/Eomm/telegraf-safe-md-reply/issues" + }, + "engines": { + "node": ">=14" + }, + "homepage": "https://github.com/Eomm/telegraf-safe-md-reply#readme", + "devDependencies": { + "standard": "^17.1.0", + "tap": "^16.3.9" + } +} \ No newline at end of file diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..fe44a08 --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,80 @@ +'use strict' + +const { test } = require('tap') + +const helpMiddleware = require('../index') + +test('helpMiddleware', async t => { + const ctx = {} + const next = () => Promise.resolve() + + const middleware = helpMiddleware() + + await middleware(ctx, next) + + t.ok(ctx.replyWithSafeMarkdownV2, 'ctx.replyWithSafeMarkdownV2 exists') + t.ok(ctx.escapeMarkdown, 'ctx.escapeMarkdown exists') +}) + +test('helpMiddleware with custom methodName', async t => { + const ctx = {} + const next = () => Promise.resolve() + + const middleware = helpMiddleware({ methodName: 'replyWithMarkdown' }) + + await middleware(ctx, next) + + t.ok(ctx.replyWithMarkdown, 'ctx.replyWithMarkdown exists') + t.ok(ctx.escapeMarkdown, 'ctx.escapeMarkdown exists') +}) + +test('replyWithSafeMarkdownV2', async t => { + const ctx = { + replyWithMarkdownV2: (text, extra) => { + // echo + return text + } + } + const next = () => Promise.resolve() + + const middleware = helpMiddleware() + + await middleware(ctx, next) + + t.equal(ctx.replyWithSafeMarkdownV2('foo_bar'), 'foo\\_bar') + t.equal(ctx.replyWithSafeMarkdownV2('foo*bar'), 'foo\\*bar') + t.equal(ctx.replyWithSafeMarkdownV2('foo[bar'), 'foo\\[bar') + t.equal(ctx.replyWithSafeMarkdownV2('foo`bar'), 'foo\\`bar') + t.equal(ctx.replyWithSafeMarkdownV2('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.replyWithSafeMarkdownV2('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.replyWithSafeMarkdownV2('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.replyWithSafeMarkdownV2('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.replyWithSafeMarkdownV2('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.replyWithSafeMarkdownV2('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.replyWithSafeMarkdownV2('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.replyWithSafeMarkdownV2('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.replyWithSafeMarkdownV2('foo_bar*'), 'foo\\_bar\\*') +}) + +test('escapeMarkdown', async t => { + const ctx = {} + const next = () => Promise.resolve() + + const middleware = helpMiddleware() + + await middleware(ctx, next) + + t.equal(ctx.escapeMarkdown('foo_bar'), 'foo\\_bar') + t.equal(ctx.escapeMarkdown('foo*bar'), 'foo\\*bar') + t.equal(ctx.escapeMarkdown('foo[bar'), 'foo\\[bar') + t.equal(ctx.escapeMarkdown('foo`bar'), 'foo\\`bar') + t.equal(ctx.escapeMarkdown('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.escapeMarkdown('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.escapeMarkdown('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.escapeMarkdown('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.escapeMarkdown('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.escapeMarkdown('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.escapeMarkdown('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.escapeMarkdown('foo_bar*'), 'foo\\_bar\\*') + t.equal(ctx.escapeMarkdown('foo_bar*'), 'foo\\_bar\\*') +})