Examples
Ready-made scenarios for copying and adapting
Examples
Minimal bot
import { bot } from 'yurst.js';
bot.command('start', 'Почати', (ctx) => ctx.reply('Привіт!'));
bot.command('ping', 'Перевірка', (ctx) => ctx.reply('pong'));
await bot.start();Full-featured bot
Commands, buttons, callbacks, messages, and inline:
import { bot, api, fmtUptime, stats } from 'yurst.js';
// Команди
bot.command('start', 'Почати роботу', (ctx) =>
ctx.reply(`Привіт, ${ctx.user?.Name ?? 'друг'}!`, {
buttons: [[{ text: 'Допомога', command: '/help' }]],
}),
);
bot.command('help', 'Список команд', (ctx) => ctx.reply(ctx.help()));
bot.command('ping', 'Перевірка з\'єднання', async (ctx) => {
const start = Date.now();
await api('GET', '/bot/me');
return ctx.reply(`Pong! ${Date.now() - start}ms`);
});
bot.command('stats', 'Статистика', (ctx) =>
ctx.reply([
`Uptime: ${fmtUptime()}`,
`Updates: ${stats.updates}`,
`Errors: ${stats.errors}`,
].join('\n')),
);
// Прихована команда
bot.command({
name: 'admin',
register: false,
run: (ctx) => ctx.reply('Адмін-панель'),
});
// Callback-кнопки
bot.command('menu', 'Меню', (ctx) =>
ctx.reply('Оберіть дію:', {
buttons: [
[
{ text: 'Так', data: 'choice:yes', style: 'success' },
{ text: 'Ні', data: 'choice:no', style: 'danger' },
],
],
}),
);
bot.callback('choice', async (ctx) => {
await ctx.answer();
const choice = ctx.data.split(':')[1];
await ctx.replace(`Ви обрали: ${choice}`);
});
// Повідомлення
bot.on('message', (ctx) => {
const text = ctx.message.Text ?? '';
if (text === 'Скасувати') {
return ctx.reply('Скасовано', { removeKeyboard: true });
}
});
// Inline
bot.on('inline', (ctx) => {
if (!ctx.query) return;
return ctx.answer([
{ id: '1', title: 'Ехо', text: ctx.query },
{ id: '2', title: 'Верхній регістр', text: ctx.query.toUpperCase() },
]);
});
await bot.start();Counter on buttons
const counters = new Map();
const view = (n) => [
[
{ text: '−', data: 'cnt:dec', style: 'danger' },
{ text: String(n), data: 'cnt:noop' },
{ text: '+', data: 'cnt:inc', style: 'success' },
],
[{ text: 'Скинути', data: 'cnt:reset' }],
];
bot.command('counter', 'Лічильник', (ctx) =>
ctx.reply('Лічильник: 0', { buttons: view(0) }),
);
bot.callback('cnt', async (ctx) => {
await ctx.answer();
const id = ctx.message?.ID;
const action = ctx.data.split(':')[1];
let n = counters.get(id) ?? 0;
switch (action) {
case 'inc': n++; break;
case 'dec': n--; break;
case 'reset': n = 0; break;
}
if (id) counters.set(id, n);
await ctx.replace(`Лічильник: ${n}`, { buttons: view(n) });
});Reply keyboard
const CITIES = ['Київ', 'Львів', 'Одеса', 'Харків'];
bot.command({
name: 'city',
description: 'Обрати місто',
scope: 'private',
run: (ctx) =>
ctx.reply('Виберіть місто:', {
keyboard: [CITIES.slice(0, 2), CITIES.slice(2), ['Скасувати']],
}),
});
bot.on('message', (ctx) => {
const text = ctx.message.Text ?? '';
if (text === 'Скасувати') {
return ctx.reply('Скасовано.', { removeKeyboard: true });
}
if (CITIES.includes(text)) {
return ctx.reply(`Ви обрали: ${text}`, { removeKeyboard: true });
}
});Deep-linking
bot.command({
name: 'start',
register: false,
run: (ctx) => {
if (ctx.payload) {
// https://yurba.one/messenger?bot=username&start=ref_123
return ctx.reply(`Вітаємо! Реферал: ${ctx.payload}`);
}
return ctx.reply('Вітаємо!');
},
});
bot.command('invite', 'Запросити', (ctx) => {
const ref = `ref_${ctx.user?.ID}`;
return ctx.reply(`Поширте посилання:\n${bot.me.invite(ref)}`);
});Pagination
const ITEMS = ['Яблуко', 'Банан', 'Апельсин', 'Груша', 'Кавун', 'Манго'];
const PER_PAGE = 2;
const view = (page) => {
const total = Math.ceil(ITEMS.length / PER_PAGE);
return [
[
{ text: '◀', data: `page:${page - 1}`, style: page === 0 ? 'primary' : undefined },
{ text: `${page + 1}/${total}`, data: 'page:noop' },
{ text: '▶', data: `page:${page + 1}`, style: page === total - 1 ? 'primary' : undefined },
],
];
};
bot.command('list', 'Список', (ctx) => {
const items = ITEMS.slice(0, PER_PAGE).join('\n');
return ctx.reply(items, { buttons: view(0) });
});
bot.callback('page', async (ctx) => {
await ctx.answer();
const page = parseInt(ctx.data.split(':')[1]);
const total = Math.ceil(ITEMS.length / PER_PAGE);
if (page < 0 || page >= total) return;
const start = page * PER_PAGE;
const items = ITEMS.slice(start, start + PER_PAGE).join('\n');
await ctx.replace(items, { buttons: view(page) });
});Poll
const poll = {
question: 'Яка ваша улюблена мова?',
options: ['JavaScript', 'Python', 'Go', 'Rust'],
votes: new Map(),
};
bot.command('poll', 'Опитування', (ctx) => {
const buttons = poll.options.map((opt, i) => [
{ text: opt, data: `vote:${i}` },
]);
const results = poll.options
.map((opt, i) => `${opt}: ${poll.votes.get(i) ?? 0}`)
.join('\n');
return ctx.reply(`${poll.question}\n\n${results}`, { buttons });
});
bot.callback('vote', async (ctx) => {
await ctx.answer('Дякуємо за голос!');
const option = parseInt(ctx.data.split(':')[1]);
poll.votes.set(option, (poll.votes.get(option) ?? 0) + 1);
const buttons = poll.options.map((opt, i) => [
{ text: opt, data: `vote:${i}` },
]);
const results = poll.options
.map((opt, i) => `${opt}: ${poll.votes.get(i) ?? 0}`)
.join('\n');
await ctx.replace(`${poll.question}\n\n${results}`, { buttons });
});Inline search
const PRODUCTS = [
{ id: 1, name: 'iPhone 15', price: 999 },
{ id: 2, name: 'MacBook Pro', price: 1999 },
{ id: 3, name: 'iPad Pro', price: 799 },
{ id: 4, name: 'AirPods Pro', price: 249 },
];
bot.on('inline', (ctx) => {
const q = ctx.query.toLowerCase().trim();
if (!q) return;
const filtered = PRODUCTS.filter(p =>
p.name.toLowerCase().includes(q),
);
const results = filtered.map(p => ({
id: String(p.id),
title: p.name,
text: `${p.name} — $${p.price}`,
}));
return ctx.answer(results);
});Event handling
import { L } from 'yurst.js';
bot.on('stopped', (update) => {
L.info('bot', `User ${update.User?.ID} blocked the bot`);
});
bot.on('added', (update) => {
L.info('bot', 'Added to chat', update);
});
bot.on('edit', (update) => {
L.info('bot', `Message ${update.Message?.ID} edited`);
});Permission check
const ADMINS = [12345, 67890];
bot.command({
name: 'admin',
register: false,
run: (ctx) => {
if (!ADMINS.includes(ctx.user?.ID)) {
return ctx.reply('Немає доступу');
}
return ctx.reply('Адмін-панель', {
buttons: [
[{ text: 'Статистика', data: 'admin:stats' }],
[{ text: 'Розсилка', data: 'admin:broadcast' }],
],
});
},
});
bot.callback('admin', async (ctx) => {
if (!ADMINS.includes(ctx.user?.ID)) {
return ctx.answer({ text: 'Немає доступу', alert: true });
}
await ctx.answer();
// Обробка адмін-дій
});Catalog with many buttons
ButtonsBuilder builds button rows from a dynamic list. Here up to three buttons are added to each row:
import { ButtonsBuilder, bot } from 'yurst.js';
const products = Array.from({ length: 18 }, (_, index) => ({
id: String(index + 1),
name: `Товар ${index + 1}`,
}));
const productMenu = new ButtonsBuilder();
products.forEach((product, index) => {
if (index % 3 === 0) productMenu.row();
productMenu.callback(product.name, `product:${product.id}`).gray();
});
bot.command('catalog', 'Відкрити каталог', (ctx) =>
ctx.reply(`Каталог: ${products.length} товарів`, {
buttons: productMenu.build(),
}),
);
bot.callback('product', async (ctx) => {
await ctx.answer();
const productId = ctx.data.slice('product:'.length);
const product = products.find((item) => item.id === productId);
if (product) await ctx.replace(`Ви обрали: ${product.name}`);
});
await bot.start();row() is called before the first element of each row. The callback contains the product ID, so the handler finds the right record by data rather than by the button's position.