• Stars
    star
    349
  • Rank 121,528 (Top 3 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created about 6 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Inline Menus for Telegram made simple. Successor of telegraf-inline-menu.

grammY Inline Menu

This menu library is made to easily create an inline menu for your Telegram bot.

Example shown as a gif

Installation

$ npm install grammy grammy-inline-menu

Consider using TypeScript with this library as it helps with finding some common mistakes faster.

A good starting point for TypeScript and Telegram bots might be this repo: EdJoPaTo/telegram-typescript-bot-template

Examples

Basic Example

import {Bot} from 'grammy'
import {MenuTemplate, MenuMiddleware} from 'grammy-inline-menu'

const menuTemplate = new MenuTemplate<MyContext>(ctx => `Hey ${ctx.from.first_name}!`)

menuTemplate.interact('I am excited!', 'a', {
	do: async ctx => {
		await ctx.reply('As am I!')
		return false
	}
})

const bot = new Bot(process.env.BOT_TOKEN)

const menuMiddleware = new MenuMiddleware('/', menuTemplate)
bot.command('start', ctx => menuMiddleware.replyToContext(ctx))
bot.use(menuMiddleware)

bot.launch()

More interesting one

Example shown as a gif

Look at the code here: TypeScript / JavaScript (consider using TypeScript)

Migrate from version 4 to 5

If your project still uses version 4 of this library see v4 documentation and consider refactoring to version 5.

List of things to migrate:

Migrate from version 5 to 6

Version 6 switched from Telegraf 3.38 to 4.0. See the Telegraf migration guide for this set of changes.

telegraf-inline-menu is relatively unaffected by this. The only change required besides the Telegraf changes is the change of ctx.match. Simply add match to your MyContext type:

export interface MyContext extends TelegrafContext {
	readonly match: RegExpExecArray | undefined;
	…
}

Telegraf knows when match is available or not. The default Context does not have match anymore. telegraf-inline-menu should also know this in a future release.

Migrate from version 6 to 7

Version 7 switches from Telegraf to grammY as a Telegram Bot framework. grammY has various benefits over Telegraf as Telegraf is quite old and grammY learned a lot from its mistakes and shortcomings.

-import {Telegraf} from 'telegraf'
-import {MenuTemplate, MenuMiddleware} from 'telegraf-inline-menu'
+import {Bot} from 'grammy'
+import {MenuTemplate, MenuMiddleware} from 'grammy-inline-menu'

How does it work

Telegrams inline keyboards have buttons. These buttons have a text and callback data.

When a button is hit, the callback data is sent to the bot. You know this from grammY as bot.callbackQuery.

This library both creates the buttons and listens for callback data events. When a button is pressed and its callback data is occurring the function relevant to the button is executed.

In order to handle tree like menu structures with submenus the buttons itself use a tree like structure to differentiate between the buttons. Imagine it as the file structure on a PC.

The main menu uses / as callback data. A button in the main menu will use /my-button as callback data. When you create a submenu it will end with a / again: /my-submenu/.

This way the library knows what do to when an action occurs: If the callback data ends with a / it will show the corresponding menu. If it does not end with a / it is an interaction to be executed.

You can use a middleware in order to see which callback data is used when you hit a button:

bot.use((ctx, next) => {
	if (ctx.callbackQuery) {
		console.log('callback data just happened', ctx.callbackQuery.data)
	}

	return next()
})

bot.use(menuMiddleware)

You can also take a look on all the regular expressions the menu middleware is using to notice a button click with console.log(menuMiddleware.tree()). Don't be scared by the output and try to find where you can find the structure in the source code. When you hit a button, the specific callback data will be matched by one of the regular expressions. Also try to create a new button and find it within the tree.

If you want to manually send your submenu /my-submenu/ you have to supply the same path that is used when you press the button in the menu.

Improve the docs

If you have any questions on how the library works head out to the issues and ask ahead. You can also join the grammY community chat in order to talk about the questions on your mind.

When you think there is something to improve on this explanation, feel free to open a Pull Request! I am already stuck in my bubble on how this is working. You are the expert on getting the knowledge about this library. Let's improve things together!

FAQ

Can I use HTML / MarkdownV2 in the message body?

Maybe this is also useful: NPM package telegram-format

const menuTemplate = new MenuTemplate<MyContext>(ctx => {
	const text = '_Hey_ *there*!'
	return {text, parse_mode: 'Markdown'}
})

Can the menu body be some media?

The menu body can be an object containing media and type for media. The media and type is the same as Telegrams InputMedia. The media is just passed to grammY so check its documentation on how to work with files.

The example features a media submenu with all currently supported media types.

const menuTemplate = new MenuTemplate<MyContext>((ctx, path) => {
	// Do something

	return {
		type: 'photo',
		media: {
			source: `./${ctx.from.id}.jpg`
		},
		text: 'Some *caption*',
		parse_mode: 'Markdown'
	}
})

The argument of the MenuTemplate can be passed a body or a function returning a body. A body can be a string or an object with options like seen above. When using as a function the arguments are the context and the path of the menu when called.

How can I run a simple method when pressing a button?

menuTemplate.interact('Text', 'unique', {
	do: async ctx => {
		await ctx.answerCallbackQuery('yaay')
		return false
	}
})

Why do I have to return a boolean or string for the do/set function?

You can control if you want to update the menu afterwards or not. When the user presses a button which changes something in the menu text, you want the user to see the updated content. You can return a relative path to go to afterwards or a simple boolean (yes = true, no = false).

Using paths can become super handy. For example when you want to return to the parent menu you can use the path ... Or to a sibling menu with ../sibling.

If you just want to navigate without doing logic, you should prefer .navigate(…).

menuTemplate.interact('Text', 'unique', {
	do: async ctx => {
		await ctx.answerCallbackQuery('go to parent menu after doing some logic')
		return '..'
	}
})

How to use a dynamic text of a button?

This is often required when translating (i18n) your bot.

Also check out other buttons like toggle as they do some formatting of the button text for you.

menuTemplate.interact(ctx => ctx.i18n.t('button'), 'unique', {
	do: async ctx => {
		await ctx.answerCallbackQuery(ctx.i18n.t('reponse'))
		return '.'
	}
})

How can I show a URL button?

menuTemplate.url('Text', 'https://edjopato.de')

How can I display two buttons in the same row?

Use joinLastRow in the second button

menuTemplate.interact('Text', 'unique', {
	do: async ctx => {
		await ctx.answerCallbackQuery('yaay')
		return false
	}
})

menuTemplate.interact('Text', 'unique', {
	joinLastRow: true,
	do: async ctx => {
		await ctx.answerCallbackQuery('yaay')
		return false
	}
})

How can I toggle a value easily?

menuTemplate.toggle('Text', 'unique', {
	isSet: ctx => ctx.session.isFunny,
	set: (ctx, newState) => {
		ctx.session.isFunny = newState
		return true
	}
})

How can I select one of many values?

menuTemplate.select('unique', ['human', 'bird'], {
	isSet: (ctx, key) => ctx.session.choice === key,
	set: (ctx, key) => {
		ctx.session.choice = key
		return true
	}
})

How can I toggle many values?

menuTemplate.select('unique', ['has arms', 'has legs', 'has eyes', 'has wings'], {
	showFalseEmoji: true,
	isSet: (ctx, key) => Boolean(ctx.session.bodyparts[key]),
	set: (ctx, key, newState) => {
		ctx.session.bodyparts[key] = newState
		return true
	}
})

How can I interact with many values based on the pressed button?

menuTemplate.choose('unique', ['walk', 'swim'], {
	do: async (ctx, key) => {
		await ctx.answerCallbackQuery(`Lets ${key}`)
		// You can also go back to the parent menu afterwards for some 'quick' interactions in submenus
		return '..'
	}
})

What's the difference between choose and select?

If you want to do something based on the choice, use menuTemplate.choose(…). If you want to change the state of something, select one out of many options for example, use menuTemplate.select(…).

menuTemplate.select(…) automatically updates the menu on pressing the button and shows what it currently selected. menuTemplate.choose(…) runs the method you want to run.

How can I use dynamic text for many values with choose or select?

One way of doing so is via Record<string, string> as input for the choices:

const choices: Record<string, string> = {
	a: 'Alphabet',
	b: 'Beta',
	c: 'Canada'
}

menuTemplate.choose('unique', choices, …)

You can also use the buttonText function for .choose(…) or formatState for .select(…) (and .toggle(…))

menuTemplate.choose('unique', ['a', 'b'], {
	do: …,
	buttonText: (context, text) => {
		return text.toUpperCase()
	}
})

I have too much content for one message. Can I use a pagination?

menuTemplate.pagination is basically a glorified choose. You can supply the amount of pages you have and what's your current page is, and it tells you which page the user what's to see. Splitting your content into pages is still your job to do. This allows you for all kinds of variations on your side.

menuTemplate.pagination('unique', {
	getTotalPages: () => 42,
	getCurrentPage: context => context.session.page,
	setPage: (context, page) => {
		context.session.page = page
	}
})

My choose/select has too many choices. Can I use a pagination?

When you don't use a pagination, you might have noticed that not all of your choices are displayed. Per default only the first page is shown. You can select the amount of rows and columns via maxRows and columns. The pagination works similar to menuTemplate.pagination but you do not need to supply the amount of total pages as this is calculated from your choices.

menuTemplate.choose('eat', ['cheese', 'bread', 'salad', 'tree', …], {
	columns: 1,
	maxRows: 2,
	getCurrentPage: context => context.session.page,
	setPage: (context, page) => {
		context.session.page = page
	}
})

How can I use a submenu?

const submenuTemplate = new MenuTemplate<MyContext>('I am a submenu')
submenuTemplate.interact('Text', 'unique', {
	do: async ctx => ctx.answerCallbackQuery('You hit a button in a submenu')
})
submenuTemplate.manualRow(createBackMainMenuButtons())

menuTemplate.submenu('Text', 'unique', submenuTemplate)

How can I use a submenu with many choices?

const submenuTemplate = new MenuTemplate<MyContext>(ctx => `You chose city ${ctx.match[1]}`)
submenuTemplate.interact('Text', 'unique', {
	do: async ctx => {
		console.log('Take a look at ctx.match. It contains the chosen city', ctx.match)
		await ctx.answerCallbackQuery('You hit a button in a submenu')
		return false
	}
})
submenuTemplate.manualRow(createBackMainMenuButtons())

menuTemplate.chooseIntoSubmenu('unique', ['Gotham', 'Mos Eisley', 'Springfield'], submenuTemplate)

Can I close the menu?

You can delete the message like you would do with grammY: ctx.deleteMessage(). Keep in mind: You can not delete messages which are older than 48 hours.

deleteMenuFromContext tries to help you with that: It tries to delete the menu. If that does not work the keyboard is removed from the message, so the user will not accidentally press something.

menuTemplate.interact('Delete the menu', 'unique', {
	do: async context => {
		await deleteMenuFromContext(context)
		// Make sure not to try to update the menu afterwards. You just deleted it and it would just fail to update a missing message.
		return false
	}
})

Can I send the menu manually?

If you want to send the root menu use ctx => menuMiddleware.replyToContext(ctx)

const menuMiddleware = new MenuMiddleware('/', menuTemplate)
bot.command('start', ctx => menuMiddleware.replyToContext(ctx))

You can also specify a path to the replyToContext function for the specific submenu you want to open. See How does it work to understand which path you have to supply as the last argument.

const menuMiddleware = new MenuMiddleware('/', menuTemplate)
bot.command('start', ctx => menuMiddleware.replyToContext(ctx, path))

You can also use sendMenu functions like replyMenuToContext to send a menu manually.

import {MenuTemplate, replyMenuToContext} from 'grammy-inline-menu'
const settingsMenu = new MenuTemplate('Settings')
bot.command('settings', async ctx => replyMenuToContext(settingsMenu, ctx, '/settings/'))

Can I send the menu from external events?

When sending from external events you still have to supply the context to the message or some parts of your menu might not work as expected!

See How does it work to understand which path you have to supply as the last argument of generateSendMenuToChatFunction.

const sendMenuFunction = generateSendMenuToChatFunction(bot.telegram, menu, '/settings/')

async function externalEventOccured() {
	await sendMenuFunction(userId, context)
}

Didn't this menu had a question function?

Yes. It was moved into a separate library with version 5 as it made the source code overly complicated.

When you want to use it check grammy-stateless-question.

import {getMenuOfPath} from 'grammy-inline-menu'

const myQuestion = new TelegrafStatelessQuestion<MyContext>('unique', async (context, additionalState) => {
	const answer = context.message.text
	console.log('user responded with', answer)
	await replyMenuToContext(menuTemplate, context, additionalState)
})

bot.use(myQuestion.middleware())

menuTemplate.interact('Question', 'unique', {
	do: async (context, path) => {
		const text = 'Tell me the answer to the world and everything.'
		const additionalState = getMenuOfPath(path)
		await myQuestion.replyWithMarkdown(context, text, additionalState)
		return false
	}
})

Documentation

The methods should have explaining documentation by itself. Also, there should be multiple @example entries in the docs to see different ways of using the method.

If you think the jsdoc / README can be improved just go ahead and create a Pull Request. Let's improve things together!

More Repositories

1

mqttui

Subscribe to a MQTT Topic or publish something quickly from the terminal
Rust
356
star
2

tui-rs-tree-widget

Tree Widget for ratatui
Rust
79
star
3

telegram-typescript-bot-template

Template for Telegram bots written in TypeScript
TypeScript
68
star
4

website-stalker

Track changes on websites via git
Rust
54
star
5

telegram-format

Format Telegram message texts with Markdown or HTML
TypeScript
45
star
6

deno-semver-redirect

Redirect Deno dependencies from semantic versions to the newest fitting version on deno.land/x
Rust
10
star
7

markdown-to-standalone-html

Create a standalone HTML file from Markdown with basic CSS
Rust
8
star
8

telegram-chat-record-bot

A Telegram Bot to record messages in a given chat for a period of time
TypeScript
7
star
9

ip-changed-telegram-message

Send a Telegram message when the public IP address changes
Rust
7
star
10

wikidata-telegram-bot

Quick Look on Wikidata Entities via Telegram
TypeScript
6
star
11

website-changed-bot

This Telegram Bot can notify you on changed website source
TypeScript
6
star
12

mqtt2influxdb

Subscribe to MQTT topics and push them to InfluxDB 1.x or v2
Rust
5
star
13

website-stalker-example

Demonstrate the website-stalker being run via github-actions
HTML
5
star
14

homebridge-fake-rgb

Fake RGB Bulb plugin for homebridge: https://github.com/nfarina/homebridge
JavaScript
5
star
15

telegraf-middleware-console-time

Quick and dirty way to see what's incoming to your Telegraf or grammY Telegram bot while developing
TypeScript
5
star
16

BastionSiegeAssistBot

This bot can help with many things in Bastion Siege. Like buildings, battles, player strength estimation, alerts, …
TypeScript
5
star
17

rain-brainz.de

Pictures I took displayed on a minimalist HTML & CSS only webpage
TypeScript
4
star
18

wikibase-types

Types the Wikibase / Wikidata API returns
TypeScript
4
star
19

bastion-siege-logic

Reverse engineered logic of BastionSiege, a Telegram Game
TypeScript
4
star
20

pling

Send notifications via Slack, Telegram, E-Mail, ...
Rust
3
star
21

wikidata-person-names

Set of given and last names pulled from Wikidata
TypeScript
3
star
22

esp-http-neomatrix-text

Control a Neopixel Matrix over HTTP
C++
3
star
23

hawhh.de

Inoffizielle HAW Hamburg Website mit ein paar hΓ€ufig benutzten Links und einer Suche ΓΌber die offizielle HAW Website
HTML
3
star
24

project-below

Quickly find or run commands in many projects
Rust
3
star
25

mpd-internetradio-destuck

Fix mpd getting stuck on internet radio when the daily disconnect happens
Rust
3
star
26

bastion-siege-clone

Clone of Bastion Siege making use of telegraf-inline-menu and Wikidata labels
TypeScript
3
star
27

resilio-sync-watch-config

Small tool to create a resilio config and watch for changes to restart the sync daemon on changes
Rust
3
star
28

update-aur-package-github-action

Update an AUR package to the given tag version
Shell
2
star
29

esp-mqtt-neopixel-clock

NodeMCU with a WS2812 Ring showing a clock
C++
2
star
30

eve

old EVE Tools - replaced by silfani-eve-tools
PHP
2
star
31

html-inline

Reads an HTML file and inlines all the images and stylesheets
Rust
2
star
32

mpdPi

This project installs MPD on the Raspberry Pi, copies the webradio playlist and starts playing. Endlessly.
Shell
2
star
33

esp-mqtt-dht

A NodeMCU with a DHT Temperature Sensor sending data over MQTT
C++
2
star
34

rell-tik-tac-toe-telegram-bot

TypeScript
2
star
35

esp-mqtt-neomatrix-text

Set color and text of an Neopixel matrix via MQTT
C++
2
star
36

home-telegram-bot

A Telegram Bot for my personal home environment
TypeScript
2
star
37

wikidata-sdk-got

Run wikidata-sdk requests from NodeJS without handling urls
TypeScript
2
star
38

u2711-hdmi-linux

Use the full resolution of the Dell Monitor U2711 over HDMI on Linux
Shell
2
star
39

rust-binary-metafile-template

Some metafiles I use within my Rust binary projects
Shell
2
star
40

space-game-telegram-bot

Telegram Bot of the untitled space-game
TypeScript
2
star
41

35c3-circuit-sticker-bot

Generate 35c3 Telegram Stickers with Bleeptracks awesome Circuit Generator
JavaScript
1
star
42

LinuxScripts

mostly installation scripts for my Arch Linux + Gnome 3 systems
Shell
1
star
43

angle-distance

Calculates the difference between two angles
TypeScript
1
star
44

esp-mqtt-neomatrix-advent

Advent Wreath with 16x16 NeoPixel Matrix controllable via MQTT
C
1
star
45

eve-kill-showoff

Show off some zKillboard Kills (EVE Online)
TypeScript
1
star
46

telegram-lock-bot

A small bot for the Telegram bot API: https://telegram.me/LockBot
TypeScript
1
star
47

eve-database-to-json

scripts to parse database tables to json files
PHP
1
star
48

sun-sets-gtk-theme

Set GTK Theme based on the sun position
Rust
1
star
49

website-stalker-github-action

Installation of the website-stalker in a Linux GitHub Action workflow
1
star
50

websocket-ledmatrix-esp

LED Matrix connected to an ESP listening to a websocket
C++
1
star
51

mqtt-smarthome

My personal "smart" home logic is (partially) realized with this MQTT helper
Rust
1
star
52

array-filter-unique

JavaScript array.filter() compatible unique filter
TypeScript
1
star
53

wikidata-misfit-bot

This Telegram bot sends you some wikidata item pictures. Find the Misfit!
TypeScript
1
star
54

silfani-eve-tools

A collection of tools to use with the great game EVE Online build with angular
TypeScript
1
star