├── .github ├── README.md └── workflows │ └── main.yml ├── src ├── regex.ts ├── mod.ts ├── types.ts ├── transformers.ts ├── README.md └── callback-data.ts ├── .vscode └── settings.json ├── examples ├── package.json ├── greeting.ts ├── counter-bot.ts └── package-lock.json ├── deno.jsonc ├── tsconfig.json ├── package.json ├── .gitignore └── test └── callback-data.test.ts /.github/README.md: -------------------------------------------------------------------------------- 1 | ../src/README.md -------------------------------------------------------------------------------- /src/regex.ts: -------------------------------------------------------------------------------- 1 | export const VALUE_REGEX = /(.+)?/; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /src/mod.ts: -------------------------------------------------------------------------------- 1 | export { createCallbackData } from "./callback-data.ts"; 2 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "private": true, 4 | "dependencies": { 5 | "callback-data": "file:../", 6 | "grammy": "^1.0.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false, 3 | "tasks": { 4 | "dev": "deno run --watch src/mod.ts" 5 | }, 6 | "fmt": { 7 | "exclude": [ 8 | "./dist/" 9 | ] 10 | }, 11 | "lint": { 12 | "exclude": [ 13 | "./dist/" 14 | ] 15 | }, 16 | "test": { 17 | "exclude": [ 18 | "./dist/" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2020"], 4 | "module": "CommonJS", 5 | "moduleResolution": "Node16", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "declaration": true, 11 | "outDir": "./dist" 12 | }, 13 | "include": ["src/*"] 14 | } 15 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type DataType = 2 | | NumberConstructor 3 | | StringConstructor 4 | | BooleanConstructor; 5 | 6 | export type Schema = { 7 | [key in string]: DataType; 8 | }; 9 | 10 | export type ValueOf = T extends NumberConstructor ? number 11 | : T extends StringConstructor ? string 12 | : T extends BooleanConstructor ? boolean 13 | : never; 14 | 15 | export type CallbackData = { 16 | [key in keyof T]: ValueOf; 17 | }; 18 | 19 | export type FilterClause = Partial>; 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - uses: denoland/setup-deno@v1 16 | with: 17 | deno-version: v1.x 18 | 19 | - run: deno fmt --check 20 | 21 | - run: deno check src/mod.ts 22 | 23 | - name: Test 24 | run: deno test 25 | 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 16.x 29 | 30 | - run: npm ci 31 | 32 | - name: Build 33 | run: npm run build -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "callback-data", 3 | "version": "1.1.1", 4 | "description": "Easy callback data management for Telegram bots", 5 | "author": "deptyped ", 6 | "homepage": "https://github.com/deptyped/callback-data#readme", 7 | "bugs": { 8 | "url": "https://github.com/deptyped/callback-data/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/deptyped/callback-data.git" 13 | }, 14 | "keywords": [ 15 | "telegram", 16 | "telegram-bot", 17 | "telegram-bot-api", 18 | "callback-data", 19 | "library", 20 | "bot", 21 | "grammy", 22 | "grammyjs", 23 | "telegraf" 24 | ], 25 | "license": "MIT", 26 | "main": "dist/mod.js", 27 | "types": "dist/mod.d.ts", 28 | "scripts": { 29 | "build": "deno2node", 30 | "prepack": "cp ./src/README.md README.md" 31 | }, 32 | "devDependencies": { 33 | "deno2node": "^1.9.0", 34 | "typescript": "^5.2.2" 35 | }, 36 | "files": [ 37 | "dist" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/transformers.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from "./types.ts"; 2 | 3 | export const serialize = ( 4 | value: number | string | boolean, 5 | targetType: DataType, 6 | ) => { 7 | if (targetType === Boolean) { 8 | if (value === true) { 9 | return "1"; 10 | } else { 11 | return "0"; 12 | } 13 | } 14 | 15 | return value.toString(); 16 | }; 17 | 18 | export const deserialize = (value: string, targetType: DataType) => { 19 | if (targetType === Number) { 20 | const num = parseFloat(value); 21 | 22 | if (isNaN(num)) { 23 | throw new Error( 24 | `Callback data parsing error. Invalid number value ("${value}")`, 25 | ); 26 | } 27 | 28 | return num; 29 | } 30 | 31 | if (targetType === Boolean) { 32 | if (value === "1") { 33 | return true; 34 | } else if (value === "0") { 35 | return false; 36 | } else { 37 | throw new Error( 38 | `Callback data parsing error. Invalid boolean value ("${value}")`, 39 | ); 40 | } 41 | } 42 | 43 | return value; 44 | }; 45 | -------------------------------------------------------------------------------- /examples/greeting.ts: -------------------------------------------------------------------------------- 1 | import { Bot, InlineKeyboard } from "grammy"; 2 | import { createCallbackData } from "callback-data"; 3 | 4 | const bot = new Bot(process.env.BOT_TOKEN); 5 | 6 | const greetingData = createCallbackData("greeting", { 7 | type: String, 8 | }); 9 | 10 | const greetingKeyboard = new InlineKeyboard() 11 | .text( 12 | "oldschool", 13 | greetingData.pack({ 14 | type: "oldschool", 15 | }), // callback data is equal to "greeting:oldschool" 16 | ) 17 | .text( 18 | "modern", 19 | greetingData.pack({ 20 | type: "modern", 21 | }), // callback data is equal to "greeting:modern" 22 | ); 23 | 24 | bot.command("start", (ctx) => 25 | ctx.reply("How to greet?", { 26 | reply_markup: greetingKeyboard, 27 | })); 28 | 29 | bot.callbackQuery( 30 | greetingData.filter({ 31 | type: "oldschool", 32 | }), 33 | (ctx) => ctx.answerCallbackQuery("Hello"), 34 | ); 35 | 36 | bot.callbackQuery( 37 | greetingData.filter({ 38 | type: "modern", 39 | }), 40 | (ctx) => ctx.answerCallbackQuery("Yo"), 41 | ); 42 | 43 | bot.start(); 44 | -------------------------------------------------------------------------------- /examples/counter-bot.ts: -------------------------------------------------------------------------------- 1 | import { Bot, InlineKeyboard } from "grammy"; 2 | import { createCallbackData } from "callback-data"; 3 | 4 | const bot = new Bot(process.env.BOT_TOKEN); 5 | 6 | let counter = 0; 7 | 8 | const resetCounterData = createCallbackData("reset", {}); 9 | const counterData = createCallbackData("counter", { 10 | action: String, 11 | number: Number, 12 | }); 13 | 14 | const counterKeyboard = new InlineKeyboard() 15 | .text( 16 | "+1", 17 | counterData.pack({ 18 | number: 1, 19 | action: "plus", 20 | }), // callback data is equal to "counter:1:plus" 21 | ) 22 | .text( 23 | "+5", 24 | counterData.pack({ 25 | number: 5, 26 | action: "plus", 27 | }), // callback data is equal to "counter:5:plus" 28 | ) 29 | .row() 30 | .text( 31 | "-1", 32 | counterData.pack({ 33 | number: 1, 34 | action: "minus", 35 | }), // callback data is equal to "counter:1:minus" 36 | ) 37 | .text( 38 | "-5", 39 | counterData.pack({ 40 | number: 5, 41 | action: "minus", 42 | }), // callback data is equal to "counter:5:minus" 43 | ) 44 | .row() 45 | .text( 46 | "Reset", 47 | resetCounterData.pack({}), // callback data is equal to "reset" 48 | ); 49 | 50 | bot.command("start", (ctx) => 51 | ctx.reply(`Hi! Counter: ${counter}`, { 52 | reply_markup: counterKeyboard, 53 | })); 54 | 55 | bot.callbackQuery( 56 | counterData.filter(), 57 | async (ctx) => { 58 | const { number, action } = counterData.unpack( 59 | ctx.callbackQuery.data, 60 | ); 61 | 62 | if (action === "plus") { 63 | counter += Number(number); 64 | } else if (action === "minus") { 65 | counter -= Number(number); 66 | } 67 | 68 | await ctx.answerCallbackQuery(); 69 | await ctx.editMessageText(`Counter: ${counter}`, { 70 | reply_markup: counterKeyboard, 71 | }); 72 | }, 73 | ); 74 | 75 | bot.callbackQuery( 76 | resetCounterData.filter(), 77 | async (ctx) => { 78 | counter = 0; 79 | 80 | await ctx.answerCallbackQuery(); 81 | await ctx.editMessageText(`Counter: ${counter}`, { 82 | reply_markup: counterKeyboard, 83 | }); 84 | }, 85 | ); 86 | 87 | bot.start(); 88 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | Lightweight and simple library that helps manage callback data when using inline 4 | keyboards. 5 | 6 | ## Installation 7 | 8 | ### Node 9 | 10 | ``` 11 | $ npm install callback-data 12 | ``` 13 | 14 | ### Deno 15 | 16 | ```ts 17 | import { createCallbackData } from "https://deno.land/x/callback_data/mod.ts"; 18 | ``` 19 | 20 | ## Example 21 | 22 | ```ts 23 | import { Bot, InlineKeyboard } from "grammy"; 24 | import { createCallbackData } from "callback-data"; 25 | 26 | const bot = new Bot(process.env.BOT_TOKEN); 27 | 28 | const greetingData = createCallbackData("greeting", { 29 | type: String, 30 | }); 31 | 32 | const greetingKeyboard = new InlineKeyboard() 33 | .text( 34 | "oldschool", 35 | greetingData.pack({ 36 | type: "oldschool", 37 | }), // callback data is equal to "greeting:oldschool" 38 | ) 39 | .text( 40 | "modern", 41 | greetingData.pack({ 42 | type: "modern", 43 | }), // callback data is equal to "greeting:modern" 44 | ); 45 | 46 | bot.command("start", (ctx) => 47 | ctx.reply("How to greet?", { 48 | reply_markup: greetingKeyboard, 49 | })); 50 | 51 | bot.callbackQuery( 52 | greetingData.filter({ 53 | type: "oldschool", 54 | }), 55 | (ctx) => ctx.answerCallbackQuery("Hello"), 56 | ); 57 | 58 | bot.callbackQuery( 59 | greetingData.filter({ 60 | type: "modern", 61 | }), 62 | (ctx) => ctx.answerCallbackQuery("Yo"), 63 | ); 64 | 65 | bot.start(); 66 | ``` 67 | 68 | There's more complex example 69 | [counter-bot](https://github.com/deptyped/callback-data/blob/main/examples/counter-bot.ts). 70 | 71 | ## Usage 72 | 73 | ### Create callback data builder 74 | 75 | ```ts 76 | const callbackData = createCallbackData( 77 | // unique prefix for callback data 78 | "data", 79 | // callback data fields 80 | { 81 | id: Number, 82 | name: String, 83 | isAdmin: Boolean, 84 | // only Number, String, Boolean data types are supported 85 | }, 86 | ); 87 | ``` 88 | 89 | ### Pack callback data 90 | 91 | ```ts 92 | const packedData = callbackData.pack({ 93 | id: 1337, 94 | name: "John", 95 | isAdmin: true, 96 | }); 97 | 98 | console.log(packedData); // data:1337:John:1 99 | ``` 100 | 101 | ### Unpack callback data 102 | 103 | ```ts 104 | const unpackedData = callbackData.unpack(packedData); 105 | 106 | console.log(unpackedData); 107 | // { 108 | // id: 1337, 109 | // name: "John", 110 | // isAdmin: true, 111 | // } 112 | ``` 113 | 114 | ### Filter by callback data 115 | 116 | ```ts 117 | const regex = callbackData.filter({ 118 | id: 1337, 119 | }); 120 | 121 | console.log(regex); // /^data:1337:(.+)?:(.+)?$/ 122 | ``` 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | README.md 133 | -------------------------------------------------------------------------------- /src/callback-data.ts: -------------------------------------------------------------------------------- 1 | import type { CallbackData, FilterClause, Schema } from "./types.ts"; 2 | import { deserialize, serialize } from "./transformers.ts"; 3 | import { VALUE_REGEX } from "./regex.ts"; 4 | 5 | const CALLBACK_DATA_SIZE_LIMIT = 64; 6 | 7 | type Options = { 8 | delimiter?: string; 9 | }; 10 | 11 | export const createCallbackData = ( 12 | id: string, 13 | schema: T, 14 | options?: Options, 15 | ) => { 16 | if (id.length === 0) { 17 | throw new Error("Callback data ID cannot be empty."); 18 | } 19 | 20 | const delimiter = options?.delimiter ?? ":"; 21 | const schemaSize = Object.keys(schema).length; 22 | const sortedSchema = Object.keys(schema).sort(); 23 | 24 | return { 25 | pack(data: CallbackData) { 26 | const valuesCount = Object.keys(data).length; 27 | if (valuesCount !== schemaSize) { 28 | throw new Error( 29 | `Callback data serialization error. Invalid number of callback data values (defined in schema ${schemaSize}, received ${valuesCount}).`, 30 | ); 31 | } 32 | 33 | const dataValues = [id]; 34 | 35 | for (const field of Object.keys(data).sort()) { 36 | if (!(field in schema)) { 37 | continue; 38 | } 39 | 40 | const dataType = schema[field]; 41 | const dataValue = data[field]; 42 | 43 | dataValues.push(serialize(dataValue, dataType)); 44 | } 45 | 46 | if (dataValues.join().includes(delimiter)) { 47 | throw new Error( 48 | `Callback data serialization error. Use of delimiter character ("${delimiter}") in values is not allowed`, 49 | ); 50 | } 51 | 52 | const packedData = dataValues.join(delimiter); 53 | 54 | if (packedData.length > CALLBACK_DATA_SIZE_LIMIT) { 55 | throw new Error( 56 | `Callback data serialization error. Size overflow (${packedData.length} > ${CALLBACK_DATA_SIZE_LIMIT})`, 57 | ); 58 | } 59 | 60 | return packedData; 61 | }, 62 | 63 | unpack(packedData: string): CallbackData { 64 | const splittedData = packedData.split(delimiter); 65 | const unpackedData = new Map(); 66 | const dataId = splittedData.shift(); 67 | 68 | if (dataId !== id) { 69 | throw new Error( 70 | `Callback data parsing error. Invalid callback data ID ("${dataId}" does not match "${id}").`, 71 | ); 72 | } 73 | 74 | if (splittedData.length !== schemaSize) { 75 | throw new Error( 76 | `Callback data parsing error. Invalid number of callback data values (defined in schema ${schemaSize}, received ${splittedData.length}).`, 77 | ); 78 | } 79 | 80 | for (const field of sortedSchema) { 81 | const dataType = schema[field]; 82 | const dataValue = splittedData.shift() as string; 83 | 84 | unpackedData.set(field, deserialize(dataValue, dataType)); 85 | } 86 | 87 | return Object.fromEntries(unpackedData); 88 | }, 89 | 90 | filter(clause?: FilterClause) { 91 | const regexValues = [id]; 92 | 93 | for (const field of sortedSchema) { 94 | const dataType = schema[field]; 95 | const clauseValue = clause?.[field]; 96 | 97 | if (typeof clauseValue !== "undefined") { 98 | regexValues.push(serialize(clauseValue, dataType)); 99 | continue; 100 | } 101 | 102 | regexValues.push(VALUE_REGEX.source); 103 | } 104 | 105 | return new RegExp(`^${regexValues.join(delimiter)}$`); 106 | }, 107 | }; 108 | }; 109 | -------------------------------------------------------------------------------- /test/callback-data.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertMatch, 4 | assertThrows, 5 | } from "https://deno.land/std@0.174.0/testing/asserts.ts"; 6 | import { createCallbackData } from "../src/callback-data.ts"; 7 | import { VALUE_REGEX } from "../src/regex.ts"; 8 | 9 | Deno.test("create callback data with identifier", () => { 10 | const callbackData = createCallbackData("test", {}); 11 | 12 | assertEquals(callbackData.pack({}), "test"); 13 | }); 14 | 15 | Deno.test("create callback data with identifier and data", () => { 16 | const callbackData = createCallbackData("test", { 17 | data1: Number, 18 | data2: String, 19 | data3: Boolean, 20 | }); 21 | 22 | assertEquals( 23 | callbackData.pack({ 24 | data3: true, 25 | data2: "Some text", 26 | data1: 1337, 27 | }), 28 | "test:1337:Some text:1", 29 | ); 30 | assertEquals( 31 | callbackData.pack({ 32 | data2: "Some text", 33 | data3: false, 34 | data1: 1337, 35 | }), 36 | "test:1337:Some text:0", 37 | ); 38 | assertEquals( 39 | callbackData.pack({ 40 | data1: 1337, 41 | data2: "", 42 | data3: true, 43 | }), 44 | "test:1337::1", 45 | ); 46 | }); 47 | 48 | Deno.test("change of callback data delimiter", () => { 49 | const callbackData = createCallbackData("test", { 50 | data1: Number, 51 | }, { 52 | delimiter: "|", 53 | }); 54 | 55 | assertEquals( 56 | callbackData.pack({ 57 | data1: 1337, 58 | }), 59 | "test|1337", 60 | ); 61 | assertMatch( 62 | "test|1337", 63 | callbackData.filter(), 64 | ); 65 | }); 66 | 67 | Deno.test("throw error when callback data reach overflow", () => { 68 | const callbackData = createCallbackData("test", { 69 | data: String, 70 | }); 71 | 72 | assertThrows(() => { 73 | callbackData.pack({ 74 | data: "text".repeat(20), 75 | }); 76 | }); 77 | }); 78 | 79 | Deno.test("throw error when callback data ID is empty", () => { 80 | assertThrows(() => { 81 | createCallbackData("", {}); 82 | }); 83 | }); 84 | 85 | Deno.test("throw error when number of callback data value is invalid", () => { 86 | const callbackData = createCallbackData("test", { 87 | data: String, 88 | }); 89 | 90 | assertThrows(() => { 91 | // @ts-expect-error to test 92 | callbackData.pack({}); 93 | }); 94 | 95 | assertThrows(() => { 96 | callbackData.unpack("test"); 97 | }); 98 | }); 99 | 100 | Deno.test("throw error when using delimiter in callback data values", () => { 101 | const callbackData = createCallbackData("test", { 102 | data: String, 103 | }); 104 | 105 | assertThrows(() => { 106 | callbackData.pack({ 107 | data: "te:xt", 108 | }); 109 | }); 110 | }); 111 | 112 | Deno.test("parse callback data string", () => { 113 | const callbackData = createCallbackData("test", { 114 | data1: Number, 115 | data2: String, 116 | data3: Boolean, 117 | }); 118 | 119 | assertEquals(callbackData.unpack("test:1337:Some text:1"), { 120 | data2: "Some text", 121 | data3: true, 122 | data1: 1337, 123 | }); 124 | }); 125 | 126 | Deno.test("throw error when callback data ID mismatch", () => { 127 | const callbackData = createCallbackData("test", {}); 128 | 129 | assertThrows(() => { 130 | callbackData.unpack("test1"); 131 | }); 132 | }); 133 | 134 | Deno.test("throw error when callback data values have an invalid type", () => { 135 | const callbackData = createCallbackData("test", { 136 | data1: Number, 137 | data2: String, 138 | data3: Boolean, 139 | }); 140 | 141 | assertThrows(() => { 142 | callbackData.unpack("test:not_a_number:test:1"); 143 | }); 144 | assertThrows(() => { 145 | callbackData.unpack("test:1337:test:not_a_boolean"); 146 | }); 147 | }); 148 | 149 | Deno.test("create callback data filter", () => { 150 | const callbackData = createCallbackData("test", { 151 | data1: Number, 152 | data2: String, 153 | data3: Boolean, 154 | }); 155 | 156 | assertEquals( 157 | callbackData.filter({ 158 | data1: 1337, 159 | }), 160 | new RegExp(`^test:1337:${VALUE_REGEX.source}:${VALUE_REGEX.source}$`), 161 | ); 162 | 163 | assertEquals( 164 | callbackData.filter({ 165 | data2: "text", 166 | }), 167 | new RegExp(`^test:${VALUE_REGEX.source}:text:${VALUE_REGEX.source}$`), 168 | ); 169 | 170 | assertEquals( 171 | callbackData.filter({ 172 | data2: "тест", 173 | }), 174 | new RegExp(`^test:${VALUE_REGEX.source}:тест:${VALUE_REGEX.source}$`), 175 | ); 176 | 177 | assertEquals( 178 | callbackData.filter({ 179 | data3: true, 180 | }), 181 | new RegExp(`^test:${VALUE_REGEX.source}:${VALUE_REGEX.source}:1$`), 182 | ); 183 | }); 184 | 185 | Deno.test("match callback data filter", () => { 186 | const callbackData = createCallbackData("test", { 187 | data1: Number, 188 | data2: String, 189 | data3: Boolean, 190 | }); 191 | 192 | assertMatch( 193 | "test:1337:test:1", 194 | callbackData.filter({ 195 | data1: 1337, 196 | }), 197 | ); 198 | assertMatch( 199 | "test:1337:тест:1", 200 | callbackData.filter({ 201 | data1: 1337, 202 | }), 203 | ); 204 | 205 | assertMatch( 206 | "test:1337:тест:1", 207 | callbackData.filter({ 208 | data2: "тест", 209 | }), 210 | ); 211 | 212 | assertMatch( 213 | "test:1337::1", 214 | callbackData.filter(), 215 | ); 216 | }); 217 | -------------------------------------------------------------------------------- /examples/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "examples", 8 | "dependencies": { 9 | "callback-data": "file:../", 10 | "grammy": "^1.0.0" 11 | } 12 | }, 13 | "node_modules/@grammyjs/types": { 14 | "version": "2.11.2", 15 | "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-2.11.2.tgz", 16 | "integrity": "sha512-qglunvUIL8mz/TS0vqdcg2uABwM34UkSkUQcZVWjxWH34c9ptsMvY3zO0n9P0eLBaRLV4BwnD8MBx35BN2FFNw==" 17 | }, 18 | "node_modules/abort-controller": { 19 | "version": "3.0.0", 20 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 21 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 22 | "dependencies": { 23 | "event-target-shim": "^5.0.0" 24 | }, 25 | "engines": { 26 | "node": ">=6.5" 27 | } 28 | }, 29 | "node_modules/callback-data": { 30 | "version": "1.0.0", 31 | "resolved": "file:..", 32 | "license": "MIT" 33 | }, 34 | "node_modules/debug": { 35 | "version": "4.3.4", 36 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 37 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 38 | "dependencies": { 39 | "ms": "2.1.2" 40 | }, 41 | "engines": { 42 | "node": ">=6.0" 43 | }, 44 | "peerDependenciesMeta": { 45 | "supports-color": { 46 | "optional": true 47 | } 48 | } 49 | }, 50 | "node_modules/event-target-shim": { 51 | "version": "5.0.1", 52 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 53 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 54 | "engines": { 55 | "node": ">=6" 56 | } 57 | }, 58 | "node_modules/grammy": { 59 | "version": "1.13.1", 60 | "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.13.1.tgz", 61 | "integrity": "sha512-kAxja6QDjw5oTJOFtrqhaQlLfCco3AwO4Mb4YRdP8A+v59roJA8mgEd0S5dOgFPyn74bPp8VWaKCI+of8wnPow==", 62 | "dependencies": { 63 | "@grammyjs/types": "^2.11.0", 64 | "abort-controller": "^3.0.0", 65 | "debug": "^4.3.4", 66 | "node-fetch": "^2.6.7" 67 | }, 68 | "engines": { 69 | "node": "^12.20.0 || >=14.13.1" 70 | } 71 | }, 72 | "node_modules/ms": { 73 | "version": "2.1.2", 74 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 75 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 76 | }, 77 | "node_modules/node-fetch": { 78 | "version": "2.6.9", 79 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", 80 | "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", 81 | "dependencies": { 82 | "whatwg-url": "^5.0.0" 83 | }, 84 | "engines": { 85 | "node": "4.x || >=6.0.0" 86 | }, 87 | "peerDependencies": { 88 | "encoding": "^0.1.0" 89 | }, 90 | "peerDependenciesMeta": { 91 | "encoding": { 92 | "optional": true 93 | } 94 | } 95 | }, 96 | "node_modules/tr46": { 97 | "version": "0.0.3", 98 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 99 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 100 | }, 101 | "node_modules/webidl-conversions": { 102 | "version": "3.0.1", 103 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 104 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 105 | }, 106 | "node_modules/whatwg-url": { 107 | "version": "5.0.0", 108 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 109 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 110 | "dependencies": { 111 | "tr46": "~0.0.3", 112 | "webidl-conversions": "^3.0.0" 113 | } 114 | } 115 | }, 116 | "dependencies": { 117 | "@grammyjs/types": { 118 | "version": "2.11.2", 119 | "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-2.11.2.tgz", 120 | "integrity": "sha512-qglunvUIL8mz/TS0vqdcg2uABwM34UkSkUQcZVWjxWH34c9ptsMvY3zO0n9P0eLBaRLV4BwnD8MBx35BN2FFNw==" 121 | }, 122 | "abort-controller": { 123 | "version": "3.0.0", 124 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 125 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 126 | "requires": { 127 | "event-target-shim": "^5.0.0" 128 | } 129 | }, 130 | "callback-data": { 131 | "version": "1.0.0" 132 | }, 133 | "debug": { 134 | "version": "4.3.4", 135 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 136 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 137 | "requires": { 138 | "ms": "2.1.2" 139 | } 140 | }, 141 | "event-target-shim": { 142 | "version": "5.0.1", 143 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 144 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" 145 | }, 146 | "grammy": { 147 | "version": "1.13.1", 148 | "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.13.1.tgz", 149 | "integrity": "sha512-kAxja6QDjw5oTJOFtrqhaQlLfCco3AwO4Mb4YRdP8A+v59roJA8mgEd0S5dOgFPyn74bPp8VWaKCI+of8wnPow==", 150 | "requires": { 151 | "@grammyjs/types": "^2.11.0", 152 | "abort-controller": "^3.0.0", 153 | "debug": "^4.3.4", 154 | "node-fetch": "^2.6.7" 155 | } 156 | }, 157 | "ms": { 158 | "version": "2.1.2", 159 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 160 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 161 | }, 162 | "node-fetch": { 163 | "version": "2.6.9", 164 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", 165 | "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", 166 | "requires": { 167 | "whatwg-url": "^5.0.0" 168 | } 169 | }, 170 | "tr46": { 171 | "version": "0.0.3", 172 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 173 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 174 | }, 175 | "webidl-conversions": { 176 | "version": "3.0.1", 177 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 178 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 179 | }, 180 | "whatwg-url": { 181 | "version": "5.0.0", 182 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 183 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 184 | "requires": { 185 | "tr46": "~0.0.3", 186 | "webidl-conversions": "^3.0.0" 187 | } 188 | } 189 | } 190 | } 191 | --------------------------------------------------------------------------------