Telegram Menu Automated
(telegram-menu-from-structure) is a powerful module designed to simplify the creation and management of Telegram menus based on structured data. This module allows developers to define complex menu structures using JavaScript objects, making it easy to generate dynamic and interactive Telegram bot interfaces.
With Telegram Menu Automated
, you can create nested menus, handle user inputs, and interact with Telegram seamlessly. The module supports various data types, including booleans, strings, numbers, and arrays, and provides a flexible way to manage menu items and their states.
Whether you are building a simple bot or a complex application, Telegram Menu Automated
offers the tools you need to create intuitive and user-friendly Telegram menus.
See the CHANGELOG
- Telegram Menu Automated
- About
- Changelog
- Table of Contents
- Installation
- Key Features
- Key Components
- Usage
- Examples
- Projects uses the Telegram Menu Automated
- Testing
- Contributing
- License
To install the package, run:
npm install telegram-menu-from-structure
- Generates a Telegram menu from a structured object
- Each submenu can be described as an object with any amount of fields or array of objects
- If a submenu is an array, it automatically generates a list of items and allows the user to modify, add or delete items
- Values of the menu items can be boolean, string, number, or array
- For boolean values, the menu item is a button that toggles the value
- For string values, the menu item is a button that allows the user to change the value via sending message to bot
- For number values it is possible to set a min, max and step values
- For array values, the menu item is a button that allows the user to add, delete or modify items in the array
- Items of the arrays can be a boolean, string, number, or object with any amount of fields
- So you can create a complex structure of menus with any amount of submenus and items
- Currently the deep of the structure is limited by maximum size of "command" in Telegram API (64 characters)
In current version the module exports the following items:
MenuItemRoot
- a class that generates a menu from a structured objectmenuDefaults
- an object with default options for the menu, which contains the following fields:columnsMaxCount
- number, the maximum number of columns in the menurowsMaxCount
- number, the maximum number of rows in the menutextSummaryMaxLength
- number, the maximum length of the text summary of the menu itemspaceBetweenColumns
- number, the number of spaces between columnscmdPrefix
-/
, the prefix of the command that triggers the menu
The menu structure object is a JSON object that describes the menu. It has the following fields:
const menuStructure = {
isRoot: true,
label: 'Main Menu',
text: 'This is the main menu',
id: 'start',
options: {
getValue: (key, type) => data[key],
setValue: (key, value, type) => (data[key] = value),
removeValue: (key) => delete data[key],
...menuDefaults,
},
structure: {
},
};
-
isRoot
- boolean, if true, the menu is a root menu, otherwise it is a submenu -
label
- string, the label of the menu -
text
- string, the text of the menu -
id
- string, the id of the menu and the command that triggers the menu with adding/
at the beginning -
options
- object, the options of the menuContains a really important options - "links" to the functions which will manage a data behind the Menu.
getValue
- function, a function that returns the value of the menu itemsetValue
- function, a function that sets the value of the menu itemremoveValue
- function, a function that removes the value of the menu item
When the user changes the value of the menu item, the corresponding function is called. By default, the
key
of the data is anid
of the main submenu (item of astructure
object), where thedata
is an array of objects or object with the values of the subordinates items. See below in description ofstructure
object. For example if one of the main submenu (item of astructure
object) has a keyitem1
and it has a submenu with a keysubitem1
, the full path to thesubitem1
will beitem1.subitem1
. If these functions are not provided, the menu will not be able to show and change the values of the menu items.The next options are the options for the menu drawing and processing:
columnsMaxCount
- number, the maximum number of columns in the menurowsMaxCount
- number, the maximum number of rows in the menutextSummaryMaxLength
- number, the maximum length of the text summary of the menu itemspaceBetweenColumns
- number, the number of spaces between columns
You can or not define it or use the default values from
menuDefaults
. As it presented in the example above.
This field is an object that describes the structure of the menu. Each key of the object is a submenu item. The value of the key is an object that describes the submenu item.
There is two examples of the submenu item:
-
Object with two fields with data
... structure: { configuration: { type: 'object', label: 'Configuration', save: () => logMenu('Saving configuration. Data:', JSON.stringify(data)), structure: { itemContent: { language: { type: 'string', presence: 'mandatory', editable: true, sourceType: 'list', source: getLanguages, onSetAfter: onLanguageChange, default: 'en', label: 'Menu language', text: 'Language of the Menu', }, buttonsMaxCount: { type: 'number', subType: 'integer', options: { min: menuDefaults.buttonsMaxCount.min, max: menuDefaults.buttonsMaxCount.max, step: menuDefaults.buttonsMaxCount.step, }, sourceType: 'input', presence: 'mandatory', editable: true, onSetAfter: onButtonMaxCountChange, default: menuDefaults.buttonsMaxCount.default, label: 'Max buttons on "page"', text: 'Max count of buttons on the one "page" of the menu', }, }, }, }, }
In this example the
configuration
is a key of thestructure
object. It's a key which will be used to store a data object of the submenu. -
Array of objects with several fields with data
... structure: { items: { type: 'array', label: 'Items', save: () => logMenu('Saving items. Data:', JSON.stringify(data)), structure: { primaryId: (data, isShort = false) => `${data.label} ${data.enabled ? '✅' : '❌'}`, label: 'Item', text: 'Item for example', itemContent: { label: { type: 'string', presence: 'mandatory', editable: true, sourceType: 'input', label: 'Label', text: 'Item identification label', }, enabled: { type: 'boolean', presence: 'mandatory', editable: true, default: false, onSetBefore: (currentItem, key, data, path) => { logMenu( `onSetBefore: currentItem: ${JSON.stringify(currentItem)}, key: ${key}, data: ${JSON.stringify( data, )}, path: ${path}`, ); if (data.label !== undefined && data.type !== undefined || currentItem[key] === true) { return true; } else { logMenu('Item is not ready for enabling'); return false; } }, label: 'Enabled', text: 'Enable/disable item', }, type: { type: 'string', presence: 'mandatory', editable: true, sourceType: 'list', source: sourceTypes, onSetReset: ['enabled'], label: 'Type of source', text: 'Type of source, i.e. chat/group/channel', }, }, }, }, },
In this example the
items
is a key of thestructure
object. It's a key which will be used to store a data array of the submenu.
type
- string, the type of the submenu item. On this level it can be one of the following values:object
- the submenu item is an object with any amount of fieldsarray
- the submenu item is an list of objects with any amount of fields
label
- string, the label of the submenu itemsave
- function, a function that is called when the data of this submenu item is savedstructure
- description of the structure of the submenu item. It will have some differences forobject
andarray
types.
There is only one field for object
type:
itemContent
- object, the structure of the submenu item. Each key of the object is a field of the submenu item. The value of the key is an object that describes the field of the submenu item.
There is several additional fields for array
type:
primaryId
string or function - used for the identification of the submenu item. Asfunction(data, isShort)
which will return the Id based on the data of the submenu item.- label - string, the label of the submenu item These two above fields are used to generate header of the submenu item.
text
string or function - the text of the submenu item header. Asfunction(data)
which will return the text based on the data of the submenu item.plain
- boolean, if true, the submenu items will be shown as single buttons, otherwise they will be shown as a list of buttons. There is some specifics of "plain" structures - they are is objects too, but with one predefined fieldvalue
which is a value of the submenu item.itemContent
- object, the structure of the submenu item. Each key of the object is a field of the submenu item. The value of the key is an object that describes the field of the submenu item.
The itemContent
object has the following has an list of attributes, where the key is a field of the submenu item. The value of the key is an object that describes the field of the submenu item.
This object can have the following fields:
type
- string, the type of the field. It can be one of the following values:boolean
- the field is a boolean valuestring
- the field is a string valuenumber
- the field is a number valuearray
- the field is an array of objects or values
presence
- string, the presence of the field. Or function which will return the presence based on the data of the submenu item. It can be one of the following values:mandatory
- the field is mandatoryoptional
- the field is optional, will shown if contains a value
editable
- boolean, if true, the field is editablesourceType
- string, the source type of the field. It can be one of the following values:list
- the field is a list of valuesinput
- the field is an input field, i.e. the user can enter a value by sending a message to the bot on request
source
orsourceAsync
function - afunction (data, force)
that returns the source of the field. It is used for thelist
source type. Should return a Map object with the values of the list. As argument it receives thedata
of the submenu item andforce
boolean flag. Ifforce
is true, the source should be deeply refreshed.Force
is used only if the flagextraRefresh
is set totrue
- see below.extraRefresh
- boolean, if true, the source can be deeply refreshed. As result the additional button "Refresh" will be shown in the bottom of the list of the values. It will be used to do a deep refresh of the source. The source function should be able to process theforce
flag.onSetBefore
function - afunction (data, key, value, path)
that is called before the value of the field is set. It can be used to check the value of the field before it is setonSetAfter
function, afunction (data, key, path)
that is called after the value of the field is setonSetReset
- array, an array of fields that should be reset when the value of the field is changeddefault
- any, the default value of the fieldlabel
string or function - the label of the field. In case offunction (data)
it should return the appropriate string value based on the data of the submenu itemtext
string or function - the text of the field. In case offunction (data)
it should return the appropriate string value based on the data of the submenu item
Explanation of the functions parameters and return values:
- For
onSetBefore
andonSetAfter
functions:data
- the current value of the submenu item, including all fieldskey
- the key of the fieldvalue
- the new value of the field (used only foronSetBefore
)path
- array of keys as a path to the field in the submenu item, including the key of the field as the last elementonSetBefore
function should returntrue
if the value of the field is correct and should be set, otherwise it should returnfalse
- for
primaryId
:data
- the current value of the submenu item, including all fieldsisShort
- boolean, if true, the short version of the text should be returned. It is used to generate the header of the submenu item Return value should be a string with the Id of the submenu item
- for
label
andtext
:data
- the current value of the submenu item, including all fields Return value should be a string with the label or text of the submenu item
Note: only one of the source
or sourceAsync
should be defined. If both are defined, the sourceAsync
will be used.
There are four external function, dependant on the external library to work with Telegram, which should be provided to the MenuItemRoot
class:
- The first one is a function that generates a button for the menu, acceptable by external library to work with Telegram.
makeButton
- it should has the following parameters:label
- the label of the buttoncommand
- the command that triggers the button And should return the button object acceptable by external library to work with Telegram
- And three functions to interact with Telegram. These functions can be "usual", i.e. synchronous type or "async" type. It depends on the external library to work with Telegram. Please see details below
sendMessage
orsendMessageAsync
- a function that sends the message(Menu) to Telegram It should has three parameters:handler
- the unique handler, used by external library to interact with TelegrammessageText
- the text part of the message(Menu)messageButtons
- the array with a buttons part of the message(Menu) And should return the number identifier of the newly sended message(Menu) in Telegram
editMessage
oreditMessageAsync
- a function that edits the the message(Menu) in Telegram It should has four parameters:handler
- the unique handler, used by external library to interact with TelegrammessageId
- the number identifier of the message(Menu) in TelegrammessageText
- the text part of the message(Menu)messageButtons
- the array with a buttons part of the message(Menu) And should return the true or false if the message was edited successfully
deleteMessage
ordeleteMessageAsync
- a function that deletes the message (user input and Menu itself) It should has two parameters:handler
- the unique handler, used by external library to interact with TelegrammessageId
- the number identifier of the message(Menu) in Telegram And should return the true or false if the message was deleted successfully
confirmCallBackQuery
orconfirmCallBackQueryAsync
- a function that confirms the callback query from the menu button It should has only one parametershandler
- the unique handler, used by external library to interact with Telegram And should return the true or false if the callback query was confirmed successfully
The main and only one method to receive and process user input is the method onCommand
of the MenuItemRoot
class. There is an async function which should be called on reaction of the external library to work with Telegram when the user sends a message to the bot or pressed a button in the menu.
It currently has a lot of parameters:
handler
- the unique handler, used by external library to interact with Telegram. For details see Receive and process user inputuserId
- the unique identifier of the user in external library to work with Telegram. Used internally to separate the technical data of different users (like last message id, etc.)messageId
- the number identifier of the message(Menu) in Telegramcommand
- the command which was sent by the user. It can be a command from menu button or an user input to change the value of the menu itemisEvent
- boolean, if the command is an event from the menu button it has to betrue
, otherwise if it is a user input - should befalse
isTarge
- boolean, should be alwaysfalse
or skipped. It is used internally
Is used to create a new instance of the MenuItemRoot
class. It has only one parameter:
menuStructure
- the menu structure object that describes the menu. See details in Menu Structure Object
Is used to initialize the menu. It has only one parameter:
menuInitializationObject
with several fields:makeButton
- the function that generates a button for the menu. Mandatory. The telegram interaction functions can be synchronous or asynchronous. It is critically important to assign this function to the appropriate type. Do not assignasync
send message function to synchronoussendMessage
parameter of themenuInitializationObject
and vice versa.sendMessage
orsendMessageAsync
- the function that sends the message(Menu) to Telegram. MandatoryeditMessage
oreditMessageAsync
- the function that edits the the message(Menu) in Telegram. MandatorydeleteMessage
ordeleteMessageAsync
- the function that deletes the message (user input and Menu itself). MandatoryconfirmCallBackQuery
orconfirmCallBackQueryAsync
- the function that confirms the callback query from the menu button. MandatorylogLevel
- the level of logging. Can be skipped. It can be one of the following values:error
- only errors are loggedwarning
- errors and warnings are loggedinfo
- errors and info messages are loggeddebug
- errors, info messages and debug messages are logged
logger
- the instance of any external logger class. Can be skipped. The logger should have the following methods:error
- logs an error messagewarn
- logs a warning messageinfo
- logs an info messagedebug
- logs a debug message
i18n
- the instance of any external i18n class. Can be skipped. The i18n should have the following methods:__
- translates a text. Should has possibility to process formatted strings with parameters. Similar tonode:util.format
At first you should import the module and then create a new instance of the MenuItemRoot
class with the menu structure object as a parameter:
import { MenuItemRoot, menuDefaults } from 'telegram-menu-from-structure';
...
const menu = new MenuItemRoot(menuStructure);
At second you should prepare the makeButton
amd Telegram interaction functions and initialize the menu:
...
const makeButton = (label, command) => {
// Generate the button for Telegram
};
const sendMessageAsync = async (handler, messageText, messageButtons) => {
// Send the message to Telegram
};
const editMessageAsync = async (handler, messageId, messageText, messageButtons) => {
// Edit the message in Telegram
};
const deleteMessageAsync = async (handler, messageId) => {
// Delete the message in Telegram
};
const confirmCallBackQueryAsync = async (handler) => {
// Confirm the callback query in Telegram
};
menu.init({
makeButton: makeButton,
sendMessageAsync: sendMessageAsync,
editMessageAsync: editMessageAsync,
deleteMessageAsync: deleteMessageAsync,
confirmCallBackQueryAsync: confirmCallBackQueryAsync,
logLevel: 'debug',
},
});
The example of usage the GramJs library
import { MenuItemRoot, menuDefaults } from 'telegram-menu-from-structure';
import { TelegramClient } from 'telegram';
...
const client = new TelegramClient(
...
);
const allowedUsers = [
...
]; // List of allowed users
...
const menu = new MenuItemRoot(menuStructure);
...
menu.init(
...
);
...
function parseEvent(event) {
let result = null;
if (event instanceof CallbackQueryEvent) {
const {userId, peer, msgId: messageId, data} = event.query;
if (data !== undefined) {
result = {userId, peer, messageId, command: data.toString(), isEvent: true};
}
} else if (event instanceof NewMessageEvent) {
const {peerId: peer, id: messageId, message: command} = event.message;
if (command !== undefined && peer.userId !== undefined) {
result = {userId: peer.userId, peer, messageId, command, isEvent: false};
}
}
return result;
}
...
const sendMessageAsync = async (event, messageText, messageButtons) => {
if (client !== null && client.connected === true) {
const messageObject = {message: messageText, buttons: messageButtons};
const {peer} = parseEvent(event) || {};
if (peer !== undefined) {
return await client.sendMessage(peer, messageObject);
}
}
return null;
};
const editMessageAsync = async (event, messageId, messageText, messageButtons) => {
if (client !== null && client.connected === true) {
const messageObject = {message: messageId, text: messageText, buttons: messageButtons};
const {peer} = parseEvent(event) || {};
if (peer !== undefined) {
return await client.editMessage(peer, messageObject);
}
}
return null;
};
const deleteMessageAsync = async (event, messageId) => {
if (client !== null && client.connected === true) {
const {peer} = parseEvent(event) || {};
if (peer !== undefined) {
return await client.deleteMessages(peer, [messageId], {revoke: true});
}
}
return null;
};
const confirmCallBackQueryAsync = async (event) => {
if (client !== null && client.connected === true && typeof event?.query?.queryId !== undefined) {
return await event.answer();
}
return null;
};
...
function onCommand(event) {
const parsedEvent = parseEvent(event);
if (parsedEvent !== null) {
const {userId, messageId, command, isEvent} = parsedEvent;
if (userId !== undefined && allowedUsers.includes(Number(userId))) {
menu.onCommand(event, userId, messageId, command, isEvent);
}
} else {
}
}
...
client.addEventHandler(onCommand, new CallbackQuery({chats: allowedUsers}));
client.addEventHandler(onCommand, new NewMessage({chats: allowedUsers}));
...
As you can see the event
object is used to interact with the Telegram (handler
). It is an event object of the GramJs library.
The example of usage the Telegraf library
const { Telegraf, Markup } = require('telegraf');
const { MenuItemRoot, menuDefaults } = require('telegram-menu-from-structure');
...
const menu = new MenuItemRoot(menuStructure);
...
menu.init(
...
);
...
const bot = new Telegraf(process.env.BOT_TOKEN);
...
const makeButton = (label, command) => {
return Markup.button.callback(label, command);
};
const sendMessageAsync = async (ctx, messageText, messageButtons) => {
console.log(`Sending message to ${ctx.chat.id}.`);
const sentMessage = await ctx.reply(messageText, {
parse_mode: 'HTML',
...Markup.inlineKeyboard(messageButtons),
});
};
const editMessageAsync = async (ctx, messageId, messageText, messageButtons) => {
console.log(`Message with id ${messageId} to ${ctx.chat.id} is edited.`);
await ctx.editMessageText(messageText, {
message_id: messageId,
parse_mode: 'HTML',
...Markup.inlineKeyboard(messageButtons),
});
};
const deleteMessageAsync = async (ctx, menuMessageId) => {
console.log(
'Deleting message:',
menuMessageId,
'from',
menuMessageId === data[`menuMessageId.${ctx.chat.id}`] ? 'bot' : `user "${ctx.from.id}".`,
);
await ctx.deleteMessage(menuMessageId);
};
const confirmCallBackQueryAsync = async (ctx) => {
console.log('Confirming callback query:', ctx.callbackQuery.id);
return await ctx.answerCbQuery();
};
...
bot.on(message('text'), (ctx) => {
const message = ctx.message;
menu.onCommand(ctx, message.chat.id, message.message_id, message.text, false);
return true;
});
bot.on(callbackQuery('data'), (ctx) => {
const callback = ctx.callbackQuery;
menu.onCommand(ctx, callback.message.chat.id, callback.message.message_id, callback.data, true);
return true;
});
As you can see the ctx
object is used to interact with the Telegram (handler
). It is a context object of the Telegraf library.
This example is a simple console example that demonstrates how to module will work from the user point of view. It's emulates the Bot interaction with the user in the console.
You can test this module without real Telegram bot.
It is configured to provide two submenu items: Configuration
and Items
. The Configuration
submenu item has two fields: language
and buttonsMaxCount
. The Items
submenu item is an array of objects with three fields: label
, enabled
, and type
.
You can go thru the menu, change the values of the fields, add, delete or modify the items of the array. The menu will show the changes in the console. At start it will have no data, but you can manage it as you want during the work with the menu. After exit the menu the data will not be saved.
To run the example, use the following command:
cd examples/simple-no-telegram-console-mode
npm install
npm start
By default this example demo will use the "external" version of Menu package. It will be downloaded from the npm repository by npm install
command.
If you want to use the local version of the Menu package, you should use the following command, instead of npm start
:
npm run start-local
But npm install
should be run before this command to install other required packages.
As result you will see the menu in the console and you can interact with it.
It will at start emulate the sending /start
command to the bot and will show the result of the menu generation in the console:
Deleting message: 101 from user "test"
Sending message to user:
text: This is the main menu
buttons:
( #0) Configuration
( #1) Items []
( #2) Exit
Enter button Id in format "#Number" or input data (if requested):
You can navigate thru the menu by input the number of the button with mandatory #
symbol as prefix. Or you can input the data for the field of the submenu item, if it will be requested. Like this:
Message with id 201 to user is edited:
text:
Please enter the "Max buttons on "page"" integer (min: 1, max: 100, step: 1) value:
buttons:
( #0) Cancel
Enter button Id in format "#Number" or input data (if requested):
Except the menu you will see the additional messages to explain the actions and results of the actions. Like this:
Deleting message: 101 from user "test"
or
Message with id 201 to user is edited:
or
Saving configuration. Data: {"buttonsOffset.test":0,"lastCommand.test":"/configuration?buttonsMaxCount","menuMessageId.test":201,"configuration":{"buttonsMaxCount":10}}
Take in account, you will work as "user" with id "test", as it is hardcoded in the example. User messages will start from 101. Bot messages will start from 201.
Notes:
- The example is not a real Telegram bot. It is a console application that emulates the interaction with the bot.
- If you receive errors on start, or try to use "local" mode or update an dependencies, please run
npm install
ornpm update
to install or update the required packages.
This example is a simple Telegram bot example that demonstrates how to module will work with real Telegram bot. It's based on the Telegraf library.
You can test this module only if you have a real Telegram bot token.
Instructions how to get the token and create a bot can be found here.
To run the example, use the following command:
cd examples/simple-telegraf
npm install
npm start
By default this example demo will use the "external" version of Menu package. It will be downloaded from the npm repository by npm install
command.
If you want to use the local version of the Menu package, you should use the following command, instead of npm start
:
npm run start-local
But npm install
should be run before this command to install other required packages.
You have to input the Telegram bot token on a script request. After that the bot will be started and you can interact with it in the Telegram chat.
As result you will see the menu in the Telegram chat with the bot and you can interact with it.
Additional logging will be shown in the console. You can see the what is happening with data and messages.
Notes:
- If you receive errors on start, or try to use "local" mode or update an dependencies, please run
npm install
ornpm update
to install or update the required packages.
Here are some projects that utilize Telegram Menu Automated
:
- Telegram Forward User Bot
- Description: A Telegram user bot that forwards messages between chats, groups, and channels using MTProto (gram-js/gramjs). Fully configurable via an intuitive bot menu.
- Repository: GitHub Link
If you have a project that uses Telegram Menu Automated
, feel free to submit a pull request to add it to this list!
To run tests, use the following command:
npm test
The tests are written using Jest and can be found in the test
directory.
Contributions are welcome! Please open an issue or submit a pull request on GitHub.
This project is licensed under the MIT License. See the LICENSE
file for details.