diff --git a/site/docs/.vitepress/components/LanguagePopup.vue b/site/docs/.vitepress/components/LanguagePopup.vue index d2752235d..c05d512a1 100644 --- a/site/docs/.vitepress/components/LanguagePopup.vue +++ b/site/docs/.vitepress/components/LanguagePopup.vue @@ -5,7 +5,8 @@ const languages: Record = { es: "Spanish", id: "Indonesian", zh: "Chinese", - uk: "Ukrainian" + uk: "Ukrainian", + ru: "Russian" }; const popup = reactive({ diff --git a/site/docs/.vitepress/config.ts b/site/docs/.vitepress/config.ts index 4ad911fd6..221e06e4e 100644 --- a/site/docs/.vitepress/config.ts +++ b/site/docs/.vitepress/config.ts @@ -17,6 +17,7 @@ export default defineConfig({ ...locale.siteId, ...locale.siteUk, ...locale.siteZh, + ...locale.siteRu, }, themeConfig: { diff --git a/site/docs/.vitepress/configs/algolia/index.ts b/site/docs/.vitepress/configs/algolia/index.ts index 5dacf4f7d..1981d0565 100644 --- a/site/docs/.vitepress/configs/algolia/index.ts +++ b/site/docs/.vitepress/configs/algolia/index.ts @@ -13,6 +13,7 @@ export const algolia: DefaultTheme.Config["search"] = { ...locale.searchId, ...locale.searchUk, ...locale.searchZh, + ...locale.searchRu, }, }, }; diff --git a/site/docs/.vitepress/configs/locales/index.ts b/site/docs/.vitepress/configs/locales/index.ts index ab15c30a7..25c1732cf 100644 --- a/site/docs/.vitepress/configs/locales/index.ts +++ b/site/docs/.vitepress/configs/locales/index.ts @@ -1,5 +1,6 @@ export { searchEn, siteEn } from "./en.js"; export { searchEs, siteEs } from "./es.js"; export { searchId, siteId } from "./id.js"; +export { searchRu, siteRu } from "./ru.js"; export { searchUk, siteUk } from "./uk.js"; export { searchZh, siteZh } from "./zh.js"; diff --git a/site/docs/.vitepress/configs/locales/ru.ts b/site/docs/.vitepress/configs/locales/ru.ts new file mode 100644 index 000000000..6781822b2 --- /dev/null +++ b/site/docs/.vitepress/configs/locales/ru.ts @@ -0,0 +1,550 @@ +import type { DocSearchProps } from "node_modules/vitepress/types/docsearch.js"; +import type { LocaleConfig } from "vitepress"; +import { social } from "../../shared/vars.js"; + +const learnGuide = { + text: "Гайд", + items: [ + { + text: "Обзор", + link: "/ru/guide/", + activeMatch: "^/ru/guide/$", + }, + { + text: "Введение", + link: "/ru/guide/introduction", + }, + { + text: "Начало работы", + link: "/ru/guide/getting-started", + }, + { + text: "Отправка и получение сообщений", + link: "/ru/guide/basics", + }, + { + text: "Контекст", + link: "/ru/guide/context", + }, + { + text: "Bot API", + link: "/ru/guide/api", + }, + { + text: "Фильтр запросов и bot.on()", + link: "/ru/guide/filter-queries", + }, + { + text: "Команды", + link: "/ru/guide/commands", + }, + { + text: "Реакции", + link: "/ru/guide/reactions", + }, + { + text: "Middleware", + link: "/ru/guide/middleware", + }, + { + text: "Обработка ошибок", + link: "/ru/guide/errors", + }, + { + text: "Обработчик файлов", + link: "/ru/guide/files", + }, + { + text: "Игры", + link: "/ru/guide/games", + }, + { + text: "Long Polling против Webhooks", + link: "/ru/guide/deployment-types", + }, + ], +}; + +const learnAdvanced = { + text: "Продвинутый", + items: [ + { + text: "Обзор", + link: "/ru/advanced/", + activeMatch: "^/ru/advanced/$", + }, + { + text: "Возможности Middleware", + link: "/ru/advanced/middleware", + }, + { + text: "Масштабирование I: Большая кодовая база", + link: "/ru/advanced/structuring", + }, + { + text: "Масштабирование II: Высокая нагрузка", + link: "/ru/advanced/scaling", + }, + { + text: "Масштабирование III: Надежность", + link: "/ru/advanced/reliability", + }, + { + text: "Масштабирование IV: Ограничения на флуд", + link: "/ru/advanced/flood", + }, + { + text: "Трансформация Bot API", + link: "/ru/advanced/transformers", + }, + { + text: "Telegram Бизнес", + link: "/ru/advanced/business", + }, + { + text: "Поддержка прокси", + link: "/ru/advanced/proxy", + }, + { + text: "Советы по развертыванию", + link: "/ru/advanced/deployment", + }, + ], +}; + +const pluginIntroduction = { + text: "Введение", + items: [ + { + text: "О плагинах", + link: "/ru/plugins/", + activeMatch: "^/ru/plugins/$", + }, + { + text: "Как написать плагин", + link: "/ru/plugins/guide", + }, + ], +}; + +const pluginBuiltin = { + text: "Встроенные", + items: [ + { + text: "Сессии и хранение данных", + link: "/ru/plugins/session", + }, + { + text: "Встроенные и собственные клавиатуры", + link: "/ru/plugins/keyboard", + }, + { + text: "Группы медиа", + link: "/ru/plugins/media-group", + }, + { + text: "Inline запросы", + link: "/ru/plugins/inline-query", + }, + ], +}; + +const pluginOfficial = { + text: "Официальные", + items: [ + { + text: "Диалоги (conversations)", + link: "/ru/plugins/conversations", + }, + { + text: "Интерактивные меню (menu)", + link: "/ru/plugins/menu", + }, + { + text: "Вопросы без состояния (stateless-question)", + link: "/ru/plugins/stateless-question", + }, + { + text: "Runner (runner)", + link: "/ru/plugins/runner", + }, + { + text: "Гидратация (hydrate)", + link: "/ru/plugins/hydrate", + }, + { + text: "Повторные запросы к API (auto-retry)", + link: "/ru/plugins/auto-retry", + }, + { + text: "Контроль флуда (transformer-throttler)", + link: "/ru/plugins/transformer-throttler", + }, + { + text: "Лимит запросов пользователей (ratelimiter)", + link: "/ru/plugins/ratelimiter", + }, + { + text: "Файлы (files)", + link: "/ru/plugins/files", + }, + { + text: "Интернационализация (i18n)", + link: "/ru/plugins/i18n", + }, + { + text: "Интернационализация (fluent)", + link: "/ru/plugins/fluent", + }, + { + text: "Роутер (router)", + link: "/ru/plugins/router", + }, + { + text: "Эмодзи (emoji)", + link: "/ru/plugins/emoji", + }, + { + text: "Режим форматирования (parse-mode)", + link: "/ru/plugins/parse-mode", + }, + { + text: "Пользователи чата (chat-members)", + link: "/ru/plugins/chat-members", + }, + ], +}; + +const pluginThirdparty = { + text: "Сторонние", + items: [ + { + text: "Консоль со временем", + link: "/ru/plugins/console-time", + }, + { + text: "Полезный Middleware", + link: "/ru/plugins/middlewares", + }, + { + text: "Автоцитата", + link: "/ru/plugins/autoquote", + }, + { + text: "[Создайте свой PR!]", + link: "/ru/plugins/#create-your-own-plugins", + }, + ], +}; + +const resourcesGrammy = { + text: "grammY", + items: [ + { + text: "О grammY", + link: "/ru/resources/about", + }, + { + text: "Чат сообщества (Англоязычный)", + link: "https://t.me/grammyjs", + }, + { + text: "Чат сообщества (Русскоязычный)", + link: "https://t.me/grammyjs_ru", + }, + { + text: "Новости", + link: "https://t.me/grammyjs_news", + }, + { + text: "Twitter", + link: "https://twitter.com/grammy_js", + }, + { + text: "ЧаВо", + link: "/ru/resources/faq", + }, + { + text: "Как grammY конкурирует с другими библиотеками", + link: "/ru/resources/comparison", + }, + ], +}; + +const resourcesTelegram = { + text: "Telegram", + items: [ + { + text: "Введение для разработчиков", + link: "https://core.telegram.org/bots", + }, + { + text: "ЧаВо по ботам", + link: "https://core.telegram.org/bots/faq", + }, + { + text: "Возможности ботов", + link: "https://core.telegram.org/bots/features", + }, + { + text: "Ссылка на Bot API", + link: "https://core.telegram.org/bots/api", + }, + { + text: "Примеры обновлений", + link: + "https://core.telegram.org/bots/webhooks#testing-your-bot-with-updates", + }, + ], +}; + +const resourcesTools = { + text: "Инструменты", + items: [ + { + text: "telegram.tools", + link: "https://telegram.tools", + }, + { + text: "Расширение для VS Code", + link: "https://github.com/grammyjs/vscode", + }, + ], +}; + +const hostingOverview = { + text: "Обзор", + items: [ + { + text: "Сравнение", + link: "/ru/hosting/comparison", + }, + ], +}; + +const hostingTutorials = { + text: "Туториалы", + items: [ + { + text: "Виртуальный выделенный сервер (VPS)", + link: "/ru/hosting/vps", + }, + { + text: "Развертывание Deno", + link: "/ru/hosting/deno-deploy", + }, + { + text: "Supabase Edge Functions", + link: "/ru/hosting/supabase", + }, + { + text: "Cloudflare Workers (Deno)", + link: "/ru/hosting/cloudflare-workers", + }, + { + text: "Cloudflare Workers (Node.js)", + link: "/ru/hosting/cloudflare-workers-nodejs", + }, + { + text: "Fly", + link: "/ru/hosting/fly", + }, + { + text: "Firebase Functions", + link: "/ru/hosting/firebase", + }, + { + text: "Vercel", + link: "/ru/hosting/vercel", + }, + { + text: "Zeabur (Deno)", + link: "/ru/hosting/zeabur-deno", + }, + { + text: "Zeabur (Node.js)", + link: "/ru/hosting/zeabur-nodejs", + }, + { + text: "Heroku", + link: "/ru/hosting/heroku", + }, + ], +}; + +export const siteRu: LocaleConfig = { + ru: { + label: "Русский", + lang: "ru-RU", + title: "grammY", + description: "Библиотека для создания Telegram ботов.", + themeConfig: { + nav: [ + { text: "Гайд", link: "/ru/guide/" }, + { + text: "Изучение", + items: [learnGuide, learnAdvanced], + }, + { + text: "Плагины", + items: [ + pluginIntroduction, + pluginBuiltin, + pluginOfficial, + pluginThirdparty, + ], + }, + { + text: "Примеры", + items: [ + { + text: "Примеры", + items: [ + { + text: "Проекты сообщества (awesome grammY)", + link: "https://github.com/grammyjs/awesome-grammY", + }, + { + text: "Примеры ботов", + link: "https://github.com/grammyjs/examples", + }, + ], + }, + ], + }, + { + text: "Ресурсы", + items: [resourcesGrammy, resourcesTelegram, resourcesTools], + }, + { + text: "Хостинг", + items: [hostingOverview, hostingTutorials], + }, + { + text: "Справочник API", + link: "/ref/", + }, + ], + sidebar: { + "/ru/guide/": [ + { collapsed: false, ...learnGuide }, + { collapsed: true, ...learnAdvanced }, + ], + "/ru/advanced/": [ + { collapsed: true, ...learnGuide }, + { collapsed: false, ...learnAdvanced }, + ], + "/ru/plugins/": [ + { collapsed: false, ...pluginIntroduction }, + { collapsed: false, ...pluginBuiltin }, + { collapsed: false, ...pluginOfficial }, + { collapsed: false, ...pluginThirdparty }, + ], + "/ru/resources/": [ + { collapsed: false, ...resourcesGrammy }, + { collapsed: false, ...resourcesTelegram }, + ], + "/ru/hosting/": [ + { collapsed: false, ...hostingOverview }, + { collapsed: false, ...hostingTutorials }, + ], + }, + outline: { + level: [2, 6], + label: "На этой странице", + }, + editLink: { + text: "Отредактируйте эту страницу на GitHub", + pattern: + "https://github.com/grammyjs/website/edit/main/site/docs/:path", + }, + docFooter: { + prev: "Предыдущая страница", + next: "Следующая страница", + }, + lastUpdatedText: "Последнее обновление", + darkModeSwitchLabel: "Внешний вид", // only displayed in the mobile view. + sidebarMenuLabel: "Меню", // only displayed in the mobile view. + returnToTopLabel: "Вернуться наверх", // only displayed in the mobile view. + langMenuLabel: "Сменить язык", // Aria-label + socialLinks: [ + { + link: social.telegram.link, + icon: { + svg: social.telegram.icon, + }, + ariaLabel: "Чат grammY в Telegram", + }, + { + link: social.github.link, + icon: social.github.icon, + ariaLabel: "Ссылка на репозиторий grammY", + }, + ], + notFound: { + code: "404", + title: "Страница не найдена", + linkText: + "Вернуться домой (смотрите чтобы вас не загнали, а то вы больше не сможете погулять)", + linkLabel: "Вернуться домой", + messages: [ + "Они не смогли найти золото, Mased.", + "Запрашиваемая страница не найдена 🙄💅", + "Ничего для тебя, сорянчик.", + "Ошибка 404", + "Я знаю, где живёт любовь.\nВ каких укромных уголках твоей души её искать...", + ], + }, + }, + }, +}; + +export const searchRu: Record> = { + ru: { + placeholder: "Поиск", + translations: { + button: { + buttonText: "Поиск", + buttonAriaLabel: "Поиск", + }, + modal: { + searchBox: { + resetButtonTitle: "Очистить запрос", + resetButtonAriaLabel: "Очистить запрос", + cancelButtonText: "Отмена", + cancelButtonAriaLabel: "Отмена", + }, + startScreen: { + recentSearchesTitle: "Последнее", + noRecentSearchesText: "Нет последних запросов", + saveRecentSearchButtonTitle: "Сохранить этот запрос", + removeRecentSearchButtonTitle: "Убрать этот запрос из истории", + favoriteSearchesTitle: "Любимые запросы", + removeFavoriteSearchButtonTitle: + "Убрать этот запрос из списка любимых", + }, + errorScreen: { + titleText: "Невозможно получить результаты", + helpText: "Возможно, вам следует проверить подключение к сети.", + }, + footer: { + selectText: "чтобы выбрать", + selectKeyAriaLabel: "Введите ключ", + navigateText: "чтобы перемещаться", + navigateUpKeyAriaLabel: "Стрелка вверх", + navigateDownKeyAriaLabel: "Стрелка вниз", + closeText: "чтобы закрыть", + closeKeyAriaLabel: "Клавиша Escape", + searchByText: "Поиск сделан с помощью", + }, + noResultsScreen: { + noResultsText: "Нет результатов по запросу:", + suggestedQueryText: "Попробуйте поискать", + reportMissingResultsText: + "Как вы считаете, должен ли этот запрос возвращать результаты?", + reportMissingResultsLinkText: "Дайте нам знать.", + }, + }, + }, + }, +}; diff --git a/site/docs/ru/404.md b/site/docs/ru/404.md new file mode 100644 index 000000000..c53ce5f25 --- /dev/null +++ b/site/docs/ru/404.md @@ -0,0 +1,6 @@ +--- +layout: page +--- + + + diff --git a/site/docs/ru/README.md b/site/docs/ru/README.md new file mode 100644 index 000000000..9aab66454 --- /dev/null +++ b/site/docs/ru/README.md @@ -0,0 +1,106 @@ +--- +layout: home +titleTemplate: false + +hero: + name: grammY + text: Фреймворк для создания Telegram ботов. + taglines: + - только подумайте и всё уже готово. + - новая эра разработки ботов. + - быстрее гепарда. + - получайте удовольствие от создания ботов. + - на одно обновление впереди. + - может отжарить всё, кроме блюд. + - как два пальца об асфальт. + - обработал миллиарды и миллиарды. + - питаемый одержимостью. + image: + src: /images/Y.svg + alt: логотип grammY + actions: + - theme: brand + text: Начать + link: ./guide/getting-started + - theme: alt + text: Документация + link: ./guide/ + +features: + - icon: анимация пляжа + title: Простой в использовании + details: grammY делает создание Telegram ботов настолько простым, что вы уже знаете, как это сделать. + - icon: анимация палитры + title: Гибкий + details: grammY открыт и может быть расширен с помощью плагинов, чтобы точно соответствовать вашим потребностям. + - icon: анимация ракеты + title: Масштабируемый + details: grammY поможет вам, когда ваш бот станет популярным и трафик возрастёт. +--- + + + +## Быстрый старт + +Боты, написанные на [TypeScript](https://www.typescriptlang.org) или JavaScript, работающие на разных платформах, включая [Node.js](https://nodejs.org). + +Откройте терминал, напишите `npm install grammy` и вставьте следующий код в ваш файл запуска: + +::: code-group + +```ts [TypeScript] +import { Bot } from "grammy"; + +const bot = new Bot(""); // <-- Поместите сюда токен своего бота "" (https://t.me/BotFather) + +// Ответит "Привет!" на любое сообщение. +bot.on("message", (ctx) => ctx.reply("Привет!")); + +bot.start(); +``` + +```js [JavaScript] +const { Bot } = require("grammy"); + +const bot = new Bot(""); // <-- Поместите сюда токен своего бота "" (https://t.me/BotFather) + +// Ответит "Привет!" на любое сообщение. +bot.on("message", (ctx) => ctx.reply("Привет!")); + +bot.start(); +``` + +```ts [Deno] +import { Bot } from "https://deno.land/x/grammy/mod.ts"; + +const bot = new Bot(""); // <-- Поместите сюда токен своего бота "" (https://t.me/BotFather) + +// Ответит "Привет!" на любое сообщение. +bot.on("message", (ctx) => ctx.reply("Привет!")); + +bot.start(); +``` + +::: + +Работает! :tada: + +
+ +--- + + + + + +
+ +© 2021-2024 · grammY поддерживает Telegram Bot API 7.10, который был [выпущен](https://core.telegram.org/bots/api#september-6-2024) 6-го Сентября 2024 года +(Последнее изменение: Покупка платных медиа и раздача «Звёзд») + +
+
diff --git a/site/docs/ru/advanced/README.md b/site/docs/ru/advanced/README.md new file mode 100644 index 000000000..e2cff1662 --- /dev/null +++ b/site/docs/ru/advanced/README.md @@ -0,0 +1,23 @@ +--- +prev: false +--- + +# Обзор: Продвинутые темы + +Когда ваш бот станет более популярным, вы можете столкнуться с более сложными проблемами, чем просто заставить его работать. + +Этот раздел документации начнется с глубокого погружения в [middleware систему grammY](./middleware), которая позволит вам написать более сложную обработку сообщений, чем обычно требуется. + +В следующих четырех главах речь пойдет о масштабировании. +Читайте [Часть I](./structuring), если ваш код становится очень сложным. +Читайте [Часть II](./scaling), если вам нужно обрабатывать большое количество сообщений. +Читайте [Часть III](./reliability), если вы беспокоитесь о надежности вашего бота. +Читайте [Часть IV](./flood), если вы упираетесь в лимит запросов, т.е. получаете ошибку 429. + +Если вам нужно перехватывать и преобразовывать API-запросы на лету, grammY предлагает вам сделать это, установив [трансформирующие функции](./transformers). + +grammY так же имеет [поддержку прокси](./proxy). + +И последнее, но не менее важное: мы составили список нескольких вещей, которые вы должны иметь в виду при [развертывании](./deployment) вашего бота. +В нем нет ничего нового, это просто куча вещей о возможных проблемах, собранных в одном месте, чтобы вы могли ознакомиться с ними. +Возможно, это поможет вам спать по ночам с чистой совестью. diff --git a/site/docs/ru/advanced/business.md b/site/docs/ru/advanced/business.md new file mode 100644 index 000000000..f972e634f --- /dev/null +++ b/site/docs/ru/advanced/business.md @@ -0,0 +1,161 @@ +# Telegram Бизнес + +Telegram Бизнес позволяет управлять личным чатом с другим пользователем +(человеком, не ботом) с помощью вашего бота. Это включает в себя отправку и получение сообщений +от вашего имени. Как правило, это полезно, если вы ведете бизнес в Telegram, а +другой пользователь является вашим клиентом. + +> Если вы до сих пор не знакомы с Telegram Бизнес, посмотрите +> [официальную документацию](https://core.telegram.org/bots#manage-your-business) +> от Telegram, перед тем как продолжить. + +Естественно, grammY полностью поддерживает это. + +## Обработка бизнес-сообщений + +Бот может управлять личным чатом между двумя пользователями через Telegram +Бизнес --- аккаунт, который подписан на бизнес подписку Telegram. Управление +личными чатами осуществляется через объект _business connection_, который +выглядит [вот так](/ref/types/businessconnection). + +### Получение бизнес сообщений + +После установки бизнес подключения бот будет **получать сообщения** от _обоих +участников чата_. + +```ts +bot.on("business_message", async (ctx) => { + // Получаем объект сообщения. + const message = ctx.businessMessage; + // Сокращенные методы работают, как и ожидалось. + const msg = ctx.msg; +}); +``` + +Сейчас непонятно, кто из двух участников чата отправил сообщение. Это может быть +сообщение вашего клиента, а может быть сообщение, отправленное лично вами (не +вашим ботом)! + +Итак, нам нужно различить этих двух пользователей. Для этого нам нужно проверить +вышеупомянутый объект бизнес подключения. Он сообщает нам, кто является +пользователем бизнес аккаунта, то есть ваш идентификатор пользователя +(или одного из ваших сотрудников). + +```ts +bot.on("business_message", async (ctx) => { + // Получаем информацию о бизнес подключении. + const conn = await ctx.getBusinessConnection(); + const employee = conn.user; + // Проверяем, кто отправил это сообщение. + if (ctx.from.id === employee.id) { + // Это сообщение отправили вы. + } else { + // Это сообщение отправил ваш клиент. + } +}); +``` + +Вы также можете пропустить вызов `getBusinessConnection` для каждого обновления, +выполнив [это](#работа-с-бизнес-подключениями). + +### Отправка сообщений + +Ваш бот может **отправлять сообщения** в этот чат _не будучи участником чата_. +Это работает, как и ожидалось, с помощью `ctx.reply` и всех его вариантов. +grammY проверяет, доступен ли +[сокращенный метод контекста](../guide/context#краткая-запись) +`ctx.businessConnectionId`, чтобы отправить сообщение в управляемый бизнес-чат. + +```ts +bot.on("business_message").filter( + async (ctx) => { + const conn = await ctx.getBusinessConnection(); + return ctx.from.id !== conn.user.id; + }, + async (ctx) => { + // Автоматически отвечаем на все вопросы клиентов. + if (ctx.msg.text.endsWith("?")) { + await ctx.reply("Скоро."); + } + }, +); +``` + +Это будет выглядеть так, будто вы отправили сообщение лично. Ваш клиент не +сможет определить, было ли сообщение отправлено вручную или через бота. (Однако +вы увидите небольшой индикатор на этот счет.) (Но ваш бот, вероятно, отвечает +намного быстрее, чем вы. Приносим свои извинения.) + +## Продвинутые возможности + +Есть еще несколько вещей, которые следует учитывать при интеграции вашего бота с +Telegram Бизнес. Здесь мы кратко рассмотрим несколько аспектов. + +### Редактирование или удаление бизнес сообщений + +Когда вы или ваш клиент редактируете или удаляете сообщения в чате, ваш бот +будет уведомлен об этом. Точнее, вы будете получать обновления +`edited_business_message` или `deleted_business_messages`. Ваш бот может +обрабатывать их обычным способом, используя `bot.on` и его бесчисленными +[фильтрующими запросами](../guide/filter-queries). + +Однако ваш бот **НЕ** может редактировать или удалять сообщения в чате. +Аналогично, ваш бот **НЕ** может пересылать сообщения из чата или копировать их +куда-либо. Все эти возможности остаются за человеком. + +### Работа с бизнес подключениями + +Когда бот будет подключен к бизнес аккаунту, он получит обновление +`business_connection`. Это обновление также будет получено, когда бот будет +отключен или подключение будет отредактировано как-то иначе. + +Например, бот может или не может отправлять сообщения в чаты, которыми он +управляет. Вы можете узнать об этом с помощью части запроса `:can_reply`. + +```ts +bot.on("business_connection:can_reply", async (ctx) => { + // Подключение позволяет отправлять сообщения. +}); +``` + +Имеет смысл хранить объекты бизнес подключений в вашей базе данных. Так вы +сможете избежать вызова `ctx.getBusinessConnection()` при каждом обновлении +только для того, чтобы +[выяснить, кто отправил сообщение](#получение-бизнес-сообщении). + +Кроме того, обновление `business_connection` содержит `user_chat_id`. Этот +идентификатор чата может быть использован для инициирования разговора с +пользователем, который подключил бота. + +```ts +bot.on("business_connection:is_enabled", async (ctx) => { + const id = ctx.businessConnection.user_chat_id; + await ctx.api.sendMessage(id, "Спасибо, что подключили меня!"); +}); +``` + +Это будет работать, даже если пользователь ещё не запустил вашего бота. + +### Управление личными чатами + +Если вы подключите бота для управления своим аккаунтом, приложения Telegram +будут предлагать вам кнопку для управления этим ботом в каждом управляемом чате. +Эта кнопка отправляет боту команду `/start`. + +Эта команда запуска имеет специальные данные +[deep linking](../guide/commands#поддержка-deep-linking), определяемые Telegram. +Они имеют формат `bizChatXXXXX`, где `XXXXX` будет идентификатором чата, которым +вы управляете. + +```ts +bot.command("start", async (ctx) => { + const payload = ctx.match; + if (payload.startsWith("bizChat")) { + const id = payload.slice(7); // обрезаем `bizChat` + await ctx.reply(`Давайте управлять чатом #${id}!`); + } +}); +``` + +Это придает вашему боту важный контекст и позволяет ему управлять +личными бизнес чатами прямо из разговора с каждым клиентом. diff --git a/site/docs/ru/advanced/deployment.md b/site/docs/ru/advanced/deployment.md new file mode 100644 index 000000000..b9e192e82 --- /dev/null +++ b/site/docs/ru/advanced/deployment.md @@ -0,0 +1,61 @@ +# Советы по развертыванию + +Вот список вещей, которые следует помнить при размещении на хостинге большого бота. + +> Вас также могут заинтересовать наши инструкции по размещению ботов. +> Просмотрите **Хостинг / Туториалы** в верхней части страницы, чтобы увидеть некоторых хостинг-провайдеров, которые уже имеют специальные руководства. + +## Ошибки + +1. [Установите обработчик ошибок с помощью `bot.catch` для long polling или в вашем серверном фреймворке для вебхуков.](../guide/errors) +2. Используйте `await` для всех `Promise` и установите **линтинг** с правилами, которые гарантируют, что вы никогда об этом не забудете. + +## Отправка сообщений + +1. Отправляйте файлы, передавая путь или `Buffer` вместо потока (`stream`), или по крайней мере убедитесь, что вы [знаете о возможных подводных камнях](./transformers#случаи-использования-трансформирующих-функции). +2. Используйте `bot.on("callback_query:data")` в качестве резервного обработчика для [реагирования на все `callback_query`](../plugins/keyboard#ответ-на-нажатие-по-встроеннои-клавиатуре). +3. Используйте [плагин `auto-retry`](../plugins/auto-retry) для автоматической обработки ошибок, вызванных превышением лимита количества запросов, и повторной отправки таких запросов. + +## Масштабирование + +Это зависит от типа развертывания. + +### Long Polling + +1. [Используйте плагин `runner`](../plugins/runner). +2. [Используйте `sequentialize` с той же функцией для получения ключа сессии, что и в плагине сессий](./scaling#параллельность-это-сложно). +3. Просмотрите параметры конфигурации `run` ([документация API](/ref/runner/run)) и убедитесь, что они соответствуют вашим потребностям, или даже рассмотрите возможность собрать собственный runner со своим [источником обновлений](/ref/runner/updatesource) и их [поглотителем (sink)](/ref/runner/updatesink). + Главное, что нужно учитывать --- это максимальная нагрузка, которую вы хотите применить к вашему серверу, то есть сколько обновлений может быть обработано одновременно. +4. Подумайте о реализации [корректного завершения работы](./reliability#правильное-выключение), чтобы завершить обработку текущих обновлений прежде чем вы остановите бота, например, для его обновления. + +### Вебхуки + +1. Убедитесь, что вы не выполняете долговременные операции в middleware: например, передача больших файлов. + [Это приводит к ошибкам таймаута](../guide/deployment-types#своевременное-завершение-запросов-вебхуков) для вебхуков и дублированию обработки обновлений, поскольку Telegram будет повторно отправлять неподтвержденные обновления. + Вместо этого рассмотрите возможность использования очереди задач. +2. Ознакомьтесь с конфигурацией `webhookCallback` ([документация API](/ref/core/webhookcallback)). +3. Если вы изменяли параметр `getSessionKey` для плагина сессий, [используйте `sequentialize` с той же функцией для получения ключа сессии, что и в плагине сессий](./scaling#параллельность-это-сложно). +4. Если вы работаете на бессерверной платформе или платформе с автоматическим масштабированием, [установите информацию о боте](/ref/core/botconfig), чтобы предотвратить чрезмерное количество вызовов `getMe`. +5. Подумайте об использовании [ответов вебхука](../guide/deployment-types#ответ-вебхука). + +## Сессии + +1. Подумайте об использовании `lazySessions`, как описано [здесь](../plugins/session#ленивые-сессии). +2. Используйте опцию `storage` для настройки вашего адаптера хранения данных, иначе все данные будут потеряны, когда процесс бота остановится. + +## Тестирование + +Пишите тесты для своего бота. +Это можно сделать с помощью grammY следующим образом: + +1. Имитируйте исходящие запросы API с помощью [трансформирующих функций](./transformers). +2. Определите и отправьте образцы объектов обновления вашему боту с помощью `bot.handleUpdate` ([документация API](/ref/core/bot#handleupdate)). + Воспользуйтесь этими [объектами обновления](https://core.telegram.org/bots/webhooks#testing-your-bot-with-updates), предоставленными командой Telegram, для вдохновения. + +::: tip Предложите собственный плагин для тестирования +Хотя grammY предоставляет необходимые хуки для начала написания тестов, было бы очень полезно иметь плагин для тестирования ботов. +Это новая территория, т. к. таких плагинов для тестирования практически не существует. +Мы с нетерпением ждем вашего вклада! + +Пример того, как можно провести тестирование, [можно найти здесь](https://github.com/PavelPolyakov/grammy-with-tests). +::: diff --git a/site/docs/ru/advanced/flood.md b/site/docs/ru/advanced/flood.md new file mode 100644 index 000000000..b29f0e840 --- /dev/null +++ b/site/docs/ru/advanced/flood.md @@ -0,0 +1,127 @@ +# Масштабирование IV: Ограничения на флуд + +Telegram ограничивает количество сообщений, которые ваш бот может отправлять каждую секунду. +Это означает, что любой ваш API-запрос может завершиться ошибкой с кодом состояния 429 (Too Many Requests) и заголовком `retry_after`, как указано [здесь](https://core.telegram.org/bots/api#responseparameters). +Это может произойти в любой момент. + +Существует только один правильный способ разрешения таких ситуаций: + +1. Подождать указанное количество секунд. +2. Повторить запрос. + +К счастью, для этого существует [плагин auto-retry](../plugins/auto-retry). + +Этот плагин [очень простой](https://github.com/grammyjs/auto-retry/blob/main/src/mod.ts). +Он буквально просто ждёт и повторяет запрос. +Однако его использование имеет серьезные последствия: **любой запрос может быть медленным.** +Это означает, что когда вы запускаете бота на вебхуках, [вам технически придется использовать очередь](../guide/deployment-types#своевременное-завершение-запросов-вебхуков), что бы вы ни делали, или же вам нужно настроить плагин auto-retry таким образом, чтобы он никогда не занимал много времени - но тогда ваш бот может пропустить некоторые запросы. + +## Что такое точные пределы + +Они не определены. + +Смиритесь с этим. + +У нас есть несколько хороших идей о том, сколько запросов вы можете выполнить, но точные цифры неизвестны. +(Если кто-то говорит вам о реальных ограничениях, значит, он плохо информирован). +Ограничения --- это не просто жесткие пороги, которые вы можете узнать, экспериментируя с Bot API. +Скорее, это гибкие ограничения, которые меняются в зависимости от точных запросов вашего бота, количества пользователей и других факторов, не все из которых известны. + +Вот несколько заблуждений и ложных представлений об ограничениях запросов. + +- Мой бот слишком новый, чтобы получать ошибки ожидания флуда. +- Мой бот не получает достаточно трафика, чтобы получать ошибки ожидания флуда. +- Эта функция моего бота используется недостаточно, чтобы получать ошибки ожидания флуда. +- Мой бот оставляет достаточно времени между вызовами API, чтобы не получать ошибки ожидания флуда. +- Этот конкретный вызов метода не может получать ошибки ожидания флуда. +- `getMe` не может получать ошибки ожидания флуда. +- `getUpdates` не может получать ошибки ожидания флуда. + +Все это неверно. + +Давайте перейдем к тому, что мы _знаем_. + +## Безопасные предположения о лимитах запросов + +Из [Bot FAQ](https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this) мы знаем несколько пределов, которые нельзя превышать никогда. + +1. _"При отправке сообщений в конкретном чате не отправляйте более одного сообщения в секунду. Мы можем разрешить короткие всплески, превышающие этот лимит, но в конечном итоге вы начнете получать 429 ошибку."_ + + Здесь все должно быть предельно ясно. Плагин auto-retry делает это за вас. + +2. _"Если вы отправляете массовые уведомления нескольким пользователям, API не позволит отправлять более 30 сообщений в секунду или около того. Для достижения наилучших результатов следует распределять уведомления на большие интервалы по времени (8-12 часов)."_ + + **Это относится только к массовым уведомлениям**, т.е. если вы активно рассылаете сообщения многим пользователям. + Если вы просто отвечаете на сообщения пользователей, то не проблема отправить 1,000 или более сообщений в секунду. + + Когда в Bot FAQ говорится, что вам следует _"рассмотреть возможность распределения уведомлений по большим интервалам"_, это не означает, что вы должны добавлять какие-либо искусственные задержки. + Напротив, главный вывод заключается в том, что массовая рассылка уведомлений --- это процесс, который займет много часов. + Вы не можете рассчитывать на то, что все пользователи получат сообщения мгновенно и в одно и то же время. + +3. _"Также обратите внимание, что ваш бот не сможет отправлять более 20 сообщений в минуту одной и той же группе."_ + + Опять же, довольно ясно. + Совершенно не связано с массовыми уведомлениями или количеством сообщений, отправляемых в группе. + И опять же, плагин auto-retry позаботится об этом за вас. + +Есть еще несколько известных ограничений, о которых стало известно за пределами официальной документации Bot API. +Например, [известно](https://t.me/tdlibchat/146123), что боты могут редактировать не более 20 сообщений в минуту в одном групповом чате. +Однако это исключение, и мы также должны предположить, что эти ограничения могут быть изменены в будущем. +Таким образом, эта информация не влияет на то, как писать вашего бота. + +Например, троттлинг бота на основе этих цифр по-прежнему является плохой идеей: + +## Троттлинг + +Некоторые считают, что превышение лимита запросов --- это плохо. +Они предпочитают знать точные ограничения, чтобы иметь возможность троттлить своего бота. + +Это неверно. +Лимиты запросов --- это инструмент, полезный для контроля флуда, и если вы будете действовать соответствующим образом, они не окажут никакого негативного влияния на вашего бота. +Иными словами, превышение лимитов не приводит к банам. +А вот их игнорирование --- да. + +Более того, [согласно Telegram](https://t.me/tdlibchat/47285), знать точные лимиты "бесполезно и вредно". + +Это _бесполезно_, потому что даже если бы вы знали лимиты, вам все равно пришлось бы обрабатывать ошибки ожидания флуда. +Например, сервер Bot API возвращает 429, когда он выключается для перезагрузки во время технического обслуживания. + +Это _вредно_, потому что если вы будете искусственно задерживать некоторые запросы, чтобы не попасть в лимиты, производительность вашего бота будет далека от оптимальной. +Вот почему вы всегда должны делать свои запросы как можно быстрее, но уважать все ошибки ожидания флуда (используя плагин auto-retry). + +Но если троттлить запросы вредно, то как сделать трансляцию? + +## Как транслировать сообщения + +Трансляцию можно осуществлять, следуя очень простому подходу. + +1. Отправьте сообщение пользователю. +2. Если вы получите ответ 429, подождите и повторите попытку. +3. Повторите. + +Не добавляйте искусственных задержек. +(Они делают трансляцию медленнее). + +Не игнорируйте ошибки 429. +(Это может привести к бану). + +Не отправляйте много сообщений параллельно. +(Вы можете отправлять очень мало сообщений параллельно (может быть, 3 или около того), но это может быть сложным для реализации). + +Шаг 2 в приведенном выше списке выполняется автоматически плагином auto-retry, поэтому код будет выглядеть следующим образом: + +```ts +bot.api.config.use(autoRetry()); + +for (const [chatId, text] of broadcast) { + await bot.api.sendMessage(chatId, text); +} +``` + +Самое интересное здесь это то, что будет представлять собой `broadcast`. +Вам нужно, чтобы все ваши чаты хранились в некоторой базе данных, и вы должны иметь возможность медленно получать их все. + +В настоящее время вам придется реализовать эту логику самостоятельно. +В будущем мы хотим создать плагин для трансляции. +Мы будем рады принять ваш вклад! +Присоединяйтесь к нам [здесь](https://t.me/grammyjs). diff --git a/site/docs/ru/advanced/middleware.md b/site/docs/ru/advanced/middleware.md new file mode 100644 index 000000000..4b9797981 --- /dev/null +++ b/site/docs/ru/advanced/middleware.md @@ -0,0 +1,116 @@ +# Возможности Middleware + +В руководстве мы представили [middleware](../guide/middleware) как стек функций. +Хотя нет ничего плохого в том, что вы можете использовать middleware таким линейным образом (также в grammY), называть его просто стеком --- это упрощение. + +## Middleware в grammY + +Обычно вы видите следующую схему. + +```ts +const bot = new Bot(""); + +bot.use(/* ... */); +bot.use(/* ... */); + +bot.on(/* ... */); +bot.on(/* ... */); +bot.on(/* ... */); + +bot.start(); +``` + +Выглядит как стек, только за кулисами --- это дерево. +Сердцем этой функциональности является класс `Composer` ([ссылка](/ref/core/composer)), который строит это дерево. + +Прежде всего, каждый экземпляр `Bot` является экземпляром `Composer`. +Это просто подкласс, поэтому `class Bot extends Composer`. + +Также вы должны знать, что каждый метод `Composer` внутренне вызывает `use`. +Например, `filter` просто вызывает `use` с некоторым middleware для ветвления, а `on` просто вызывает `filter` снова с некоторой предикатной функцией, которая сопоставляет обновления с заданным [фильтрующими запросами](../guide/filter-queries). +Поэтому мы можем пока ограничиться рассмотрением `use`, а остальное будет дальше. + +Теперь нам предстоит немного углубиться в детали того, что `Composer` делает с вашими вызовами `use`, и чем он отличается от других систем middleware. +Разница может показаться тонкой, но дождитесь следующего подраздела, чтобы узнать, почему она имеет замечательные последствия. + +## Расширение `Composer` + +Вы можете установить дополнительный middleware на экземпляр `Composer` даже после установки самого `Composer` где-либо. + +```ts +const bot = new Bot(""); // подкласс `Composer` + +const composer = new Composer(); +bot.use(composer); + +// Они будут запущены: +composer.use(/* A */); +composer.use(/* B */); +composer.use(/* C */); +``` + +`A`, `B` и `C` будут запущены. +Все это говорит о том, что после установки экземпляра `Composer` вы все еще можете вызвать `use` в нём, и этот middleware всё равно будет запущен. +(В этом нет ничего выдающегося, но это уже основное отличие от популярных конкурирующих фреймворков, которые просто игнорируют последующие операции). + +Вам может быть интересно, где здесь древовидная структура. +Давайте посмотрим на этот фрагмент: + +```ts +const composer = new Composer(); + +composer.use(/* A */); +composer.use(/* B */).use(/* C */); +composer.use(/* D */).use(/* E */).use(/* F */).use(/* G */); +composer.use(/* H */).use(/* I */); +composer.use(/* J */).use(/* K */).use(/* L */); +``` + +Вы видите это? + +Как вы можете догадаться, все middleware будут запускаться в порядке от `A` до `L`. + +Другие библиотеки внутренне сократили бы этот код, чтобы он был эквивалентен `composer.use(/* A */).use(/* B */).use(/* C */).use(/* D */)...` и так далее. +Напротив, grammY сохраняет указанное вами дерево: один корневой узел (`composer`) имеет пять дочерних (`A`, `B`, `D`, `H`, `J`), а дочерний `B` имеет еще один дочерний, `C`, и т.д. +Это дерево будет обходить каждое обновление в порядке возрастания глубины, таким образом, эффективно проходя от `A` до `L` в линейном порядке, что очень похоже на то, что вы знаете из других систем. + +Это возможно благодаря созданию нового экземпляра `Composer` при каждом вызове `use`, который в свою очередь будет расширяться (как объяснялось выше). + +## Конкатенация вызовов `use` + +Если бы мы использовали только `use`, это было бы не слишком юзабельно (каламбур). +Все становится интереснее, как только в дело вступает, например, `filter`. + +Посмотрите на это: + +```ts +const composer = new Composer(); + +composer.filter(/* 1 */, /* A */).use(/* B */) + +composer.filter(/* 2 */).use(/* C */, /* D */) +``` + +В строке 3 мы регистрируем `A` за предикатной функцией `1`. +`A` будет оцениваться только для обновлений, которые проходят условие `1`. +Однако `filter` возвращает экземпляр `Composer`, который мы дополняем вызовом `use` в строке 3, поэтому `B` все еще охраняется `1`, даже если он установлен в совершенно другом вызове `use`. + +Строка 5 эквивалентна строке 3 в том отношении, что и `C`, и `D` будут запущены, только если выполняется `2`. + +Помните, как вызовы `bot.on()` можно было объединять в цепочку, чтобы конкатенировать запросы фильтрации с помощью AND? +Представьте себе следующее: + +```ts +const composer = new Composer(); + +composer.filter(/* 1 */).filter(/* 2 */).use(/* A */); +``` + +`2` будет проверяться только в том случае, если `1` выполняется, а `A` будет выполняться только в том случае, если выполняется `2` (и, следовательно, `1`). + +Пересмотрите раздел о [комбинировании фильтрующих запросов](../guide/filter-queries#комбинирование-нескольких-запросов) с новыми знаниями и почувствуйте свою новую силу. + +Особым случаем здесь является `fork`, поскольку он запускает два параллельных вычисления, т.е. чередующихся в цикле событий. +Вместо того чтобы возвращать экземпляр `Composer`, созданный базовым вызовом `use`, он возвращает `Composer`, отражающий развилку вычислений. +Это позволяет использовать лаконичные шаблоны типа `bot.fork().on(":text").use(/* A */)`. +Теперь `A` будет выполняться на ветке параллельных вычислений. diff --git a/site/docs/ru/advanced/proxy.md b/site/docs/ru/advanced/proxy.md new file mode 100644 index 000000000..a8bfd7f20 --- /dev/null +++ b/site/docs/ru/advanced/proxy.md @@ -0,0 +1,54 @@ +# Поддержка прокси + +grammY позволяет настраивать выполнение сетевых запросов. +Это включает в себя инъекцию пользовательского payload в каждый запрос, который может быть использована для установки прокси-агента. +Посмотрите `ApiClientOptions` в [документации grammY API](/ref/core/apiclientoptions). + +В Deno вот как можно использовать `http` прокси: + +```ts +import { Bot } from "https://deno.land/x/grammy/mod.ts"; + +const client = Deno.createHttpClient({ + proxy: { url: "http://host:port/" }, +}); +const bot = new Bot("", { + client: { + baseFetchConfig: { + // @ts-ignore + client, + }, + }, +}); +``` + +> Обратите внимание, что вам нужно запустить это с флагом `--unstable`. + +В Node.js вот как можно использовать прокси с пакетом `socks-proxy-agent` ([npm](https://www.npmjs.com/package/socks-proxy-agent)): + +```ts +import { Bot } from "grammy"; +import { SocksProxyAgent } from "socks-proxy-agent"; + +const socksAgent = new SocksProxyAgent({ + hostname: host, // введите хост прокси-сервера + port: port, // введите порт прокси-сервера +}); + +const bot = new Bot("", { + client: { + baseFetchConfig: { + agent: socksAgent, + compress: true, + }, + }, +}); +``` + +> Обратите внимание, что указание `compress: true` --- это необязательная оптимизация производительности. +> Она не имеет никакого отношения к поддержке прокси. +> Она является частью значения по умолчанию для `baseFetchConfig`, так что если вы все еще хотите ее получить, вам следует указать ее снова. + +Заставить прокси работать может быть непросто. +Свяжитесь с нами в [Telegram чате](https://t.me/grammyjs), если у вас возникнут проблемы, или если вам нужно, чтобы grammY поддерживал дополнительные параметры конфигурации. +У нас также есть [русскоязычный Telegram чат](https://t.me/grammyjs_ru). diff --git a/site/docs/ru/advanced/reliability.md b/site/docs/ru/advanced/reliability.md new file mode 100644 index 000000000..842eaee97 --- /dev/null +++ b/site/docs/ru/advanced/reliability.md @@ -0,0 +1,158 @@ +# Масштабирование III: Надежность + +Если вы убедились, что у вас есть правильная [обработка ошибок](../guide/errors) для вашего бота, то вы готовы к работе. +Все ошибки, которые должны произойти (неудачные вызовы API, неудачные сетевые запросы, неудачные запросы к базе данных, неудачные middleware и т.д.) --- будут обнаружены. + +Вы должны убедиться, что всегда `ожидаете` все promise, или, по крайней мере, вызываете `catch` для них. +Используйте правило линтинга, чтобы убедиться, что вы не сможете забыть об этом. + +## Правильное выключение + +Для ботов, использующих long polling, есть еще один момент, который следует учитывать. +Поскольку в какой-то момент вы снова собираетесь остановить свой экземпляр во время работы, вам следует подумать о том, чтобы перехватить события `SIGTERM` и `SIGINT` и вызвать `bot.stop` (встроенный long polling) или остановить своего бота через его [обработчик](/ref/runner/runnerhandle#stop) (grammY runner): + +### Простой Long Polling + +::: code-group + +```ts [TypeScript] +import { Bot } from "grammy"; + +const bot = new Bot(""); + +// Остановка бота при завершении процесса Node.js +// вот-вот будет завершён +process.once("SIGINT", () => bot.stop()); +process.once("SIGTERM", () => bot.stop()); + +await bot.start(); +``` + +```js [JavaScript] +const { Bot } = require("grammy"); + +const bot = new Bot(""); + +// Остановка бота при завершении процесса Node.js +// вот-вот будет завершён +process.once("SIGINT", () => bot.stop()); +process.once("SIGTERM", () => bot.stop()); + +await bot.start(); +``` + +```ts [Deno] +import { Bot } from "https://deno.land/x/grammy/mod.ts"; + +const bot = new Bot(""); + +// Остановка бота при завершении процесса Deno +// вот-вот будет завершён +Deno.addSignalListener("SIGINT", () => bot.stop()); +Deno.addSignalListener("SIGTERM", () => bot.stop()); + +await bot.start(); +``` + +::: + +### Использование grammY runner + +::: code-group + +```ts [TypeScript] +import { Bot } from "grammy"; +import { run } from "@grammyjs/runner"; + +const bot = new Bot(""); + +const runner = run(bot); + +// Остановка бота при завершении процесса Node.js +// вот-вот будет завершён +const stopRunner = () => runner.isRunning() && runner.stop(); +process.once("SIGINT", stopRunner); +process.once("SIGTERM", stopRunner); +``` + +```js [JavaScript] +const { Bot } = require("grammy"); +const { run } = require("@grammyjs/runner"); + +const bot = new Bot(""); + +const runner = run(bot); + +// Остановка бота при завершении процесса Node.js +// вот-вот будет завершён +const stopRunner = () => runner.isRunning() && runner.stop(); +process.once("SIGINT", stopRunner); +process.once("SIGTERM", stopRunner); +``` + +```ts [Deno] +import { Bot } from "https://deno.land/x/grammy/mod.ts"; +import { run } from "https://deno.land/x/grammy_runner/mod.ts"; + +const bot = new Bot(""); + +const runner = run(bot); + +// Остановка бота при завершении процесса Deno +// вот-вот будет завершён +const stopRunner = () => runner.isRunning() && runner.stop(); +Deno.addSignalListener("SIGINT", stopRunner); +Deno.addSignalListener("SIGTERM", stopRunner); +``` + +::: + +Это практически все, что нужно для надежности, и теперь ваш экземпляр должен:registered: никогда:tm: не падать. + +## Гарантии надежности + +Что делать, если ваш бот обрабатывает финансовые операции и вам необходимо рассмотреть [сценарий `kill -9`](https://stackoverflow.com/questions/43724467/what-is-the-difference-between-kill-and-kill-9) когда процессор физически ломается или в центре обработки данных отключается электричество? +Если по какой-то причине кто-то или что-то действительно жестко пресекает этот процесс, все становится немного сложнее. + +По сути, боты не могут гарантировать _точно однократное_ выполнение вашего middleware. +Прочтите это [обсуждение](https://github.com/tdlib/telegram-bot-api/issues/126) на GitHub, чтобы узнать больше о том, **почему** ваш бот может отправлять дублирующие сообщения (или вообще не отправлять их) в крайне редких случаях. +В оставшейся части этого раздела мы подробно расскажем о том, **как** grammY ведет себя в этих необычных обстоятельствах, и о том, как действовать в таких ситуациях. + +> Вы просто хотите создать бота для Telegram? [Пропустите остальную часть этой страницы.](./flood) + +### Вебхук + +Если вы используете вебхуки, сервер Bot API будет повторно доставлять обновления вашему боту, если он вовремя не ответит `OK`. +Это практически полностью определяет поведение системы --- если вам нужно предотвратить обработку дубликатов обновлений, вы должны создать свой собственный де-дубликатор, основанный на `update_id`. +grammY не делает этого за вас, но не стесняйтесь PR, если считаете, что кто-то еще может извлечь из этого пользу. + +### Long Polling + +[`Long Polling` или же `Длительный опрос`](https://learn.javascript.ru/long-polling) более интересен. +Long Polling, по сути, повторно запускает последнюю партию обновлений, которая была получена, но не смогла завершиться. + +> Обратите внимание, что если вы правильно остановите своего бота с помощью `bot.stop`, то [смещение обновления](https://core.telegram.org/bots/api#getting-updates) будет синхронизировано с серверами Telegram путем вызова `getUpdates` с правильным смещением, но без обработки данных обновления. + +Другими словами, вы никогда не потеряете ни одного обновления, однако может случиться так, что вы повторно обработаете до 100 обновлений, которые вы видели ранее. +Поскольку вызовы `sendMessage` не являются идемпотентными, пользователи могут получать дубликаты сообщений от вашего бота. +Однако _по крайней мере один раз_ обработка гарантирована. + +### grammY Runner + +Если вы используете [grammY runner](../plugins/runner) в параллельном режиме следующий вызов `getUpdates` потенциально может быть выполнен до того, как ваш middleware обработает первое обновление текущей партии. +Таким образом, смещение обновления будет [подтверждено](https://core.telegram.org/bots/api#getupdates) преждевременно. +Это плата за высокую параллельность, и, к сожалению, её невозможно избежать без снижения пропускной способности и скорости отклика. +В результате, если ваш экземпляр будет выключен в нужный (неправильный) момент, может случиться так, что до 100 обновлений не смогут быть получены снова, потому что Telegram считает их подтвержденными. +Это приводит к потере данных. + +Если очень важно предотвратить это, используйте источники и поглотители библиотеки grammY runner для создания собственного конвейера обновлений, который сначала пропускает все обновления через очередь сообщений. + +1. По сути, вам придется создать [поглотитель](/ref/runner/updatesink), который будет толкать в очередь, и запускать одного runner, который будет обслуживать только вашу очередь сообщений. +2. Затем нужно создать [источник](/ref/runner/updatesource), который снова будет тянуть из очереди сообщений. + В результате вы запустите два разных экземпляра grammY runner. + +Этот расплывчатый проект, описанный выше, был лишь наброском, но не реализован, насколько нам известно. +Пожалуйста, [свяжитесь с группой Telegram](https://t.me/grammyjs), если у вас есть какие-либо вопросы или если вы попробуете это сделать и сможете поделиться своими успехами. + +С другой стороны, если ваш бот сильно загружен и запрос обновлений замедляется из-за [автоматического ограничения нагрузки](../plugins/runner#поглотитель), возрастают шансы, что некоторые обновления будут получены повторно, что снова приведет к дублированию сообщений. +Таким образом, цена полной параллельности заключается в том, что обработка не может быть гарантирована ни _по крайней мере один раз_, ни _более одного раза_ diff --git a/site/docs/ru/advanced/scaling.md b/site/docs/ru/advanced/scaling.md new file mode 100644 index 000000000..471df64b3 --- /dev/null +++ b/site/docs/ru/advanced/scaling.md @@ -0,0 +1,153 @@ +# Масштабирование II: Высокая нагрузка + +Способность вашего бота выдерживать высокую нагрузку зависит от того, как вы запускаете бота [через long polling или через вебхуки](../guide/deployment-types). +В любом случае, вам следует ознакомиться с некоторыми подводными камнями [ниже](#параллельность-это-сложно). + +## Long Polling + +Большинству ботов никогда не требуется обрабатывать более нескольких сообщений в минуту (во время "пиковой нагрузки"). +Другими словами, масштабируемость не является для них проблемой. +Чтобы быть предсказуемым, grammY обрабатывает обновления последовательно. +Вот порядок операций: + +1. Получите до 100 обновлений через `getUpdates` ([ссылка на Telegram Bot API](https://core.telegram.org/bots/api#getupdates)) +2. Для каждого обновления `ожидайте` стек middleware для него + +Однако если ваш бот обрабатывает одно сообщение в секунду (или что-то в этом роде) во время пиков нагрузки, это может негативно сказаться на скорости отклика. +Например, сообщение Боба должно ждать, пока сообщение Алисы не будет обработано. + +Эту проблему можно решить, не дожидаясь окончания обработки сообщения Алисы, т.е. обрабатывая оба сообщения одновременно. +Для достижения максимальной скорости отклика мы также хотели бы получать новые сообщения, пока сообщения Боба и Алисы еще обрабатываются. +В идеале мы также хотели бы ограничить параллельность некоторым фиксированным числом, чтобы ограничить максимальную нагрузку на сервер. + +Параллельная обработка не поставляется из коробки. +Вместо этого можно использовать **плагин [grammY runner](../plugins/runner)** для запуска вашего бота. +Он поддерживает все вышеперечисленное из коробки и очень прост в использовании. + +```ts +// Раньше +bot.start(); + +// Используя grammY runner, который экспортирует `run`. +run(bot); +``` + +По умолчанию ограничение параллельности составляет 500. +Если вы хотите глубже изучить пакет, загляните на [эту](../plugins/runner) страницы. + +Параллельность --- это сложно, поэтому ознакомьтесь с [подразделом ниже](#параллельность-это-сложно), чтобы узнать, что следует иметь в виду при использовании grammY-runner. + +## Вебхуки + +Если вы запустите своего бота на вебхуках, он будет автоматически обрабатывать обновления одновременно, как только они будут получены. +Естественно, чтобы это хорошо работало при высокой нагрузке, вам следует ознакомиться с [использованием вебхуков](../guide/deployment-types#как-использовать-вебхуки). +Это означает, что вы все еще должны знать о некоторых последствиях параллельности, см. раздел [Параллельность - это сложно](#параллельность-это-сложно). + +Также [помните, что](../guide/deployment-types#своевременное-завершение-запросов-вебхуков) Telegram будет доставлять обновления из одного чата последовательно, а обновления из разных чатов --- параллельно. + +## Параллельность - это сложно + +Если ваш бот обрабатывает все обновления одновременно, это может вызвать ряд проблем, требующих особого внимания. +Например, если два сообщения из одного чата будут получены одним и тем же вызовом `getUpdates`, они будут обрабатываться одновременно. +Порядок сообщений внутри одного чата больше не может быть гарантирован. + +Основной момент, когда это может привести к конфликту --- использование [сессий](../plugins/session), которые могут столкнуться с опасностью записи после чтения. +Представьте себе следующую последовательность событий: + +1. Алиса отправляет сообщение A +2. Бот начинает обработку A +3. Бот считывает данные сессии для Алисы из базы данных +4. Алиса отправляет сообщение B +5. Бот начинает обработку B +6. Бот считывает данные сессии для Алисы из базы данных +7. Бот завершает обработку A и записывает новую сессию в базу данных +8. Бот завершает обработку B и записывает новую сессию в базу данных, тем самым перезаписывая изменения, сделанные при обработке A. + Потеря данных из-за конфлитка перезаписи! + +> Примечание: Вы можете попытаться использовать транзакции базы данных для своих сессий, но тогда вы сможете только обнаружить опасность, но не предотвратить ее. +> Попытка использовать блокировку вместо этого приведет к полному исключению параллельности. +> Гораздо проще изначально избежать опасности. + +Большинство других сессионных систем веб-фреймворков просто принимают риск возникновения условий гонки, поскольку они не слишком часто случаются в Интернете. +Однако мы этого не хотим, потому что боты Telegram с гораздо большей вероятностью столкнутся с параллельными запросами на один и тот же ключ сессии. +Следовательно, мы должны убедиться, что обновления, обращающиеся к одним и тем же данным сессии, обрабатываются последовательно, чтобы избежать этого опасного состояния гонки. + +В комплект поставки grammY runner входит middleware `sequentialize()`, который обеспечивает последовательную обработку обновлений, которые сталкиваются между собой. +Вы можете настроить его на ту же функцию, которая используется для определения ключа сессии. +Тогда она позволит избежать вышеупомянутого состояния гонки, замедляя те (и только те) обновления, которые могут вызвать столкновение. + +::: code-group + +```ts [TypeScript] +import { Bot, Context, session } from "grammy"; +import { run, sequentialize } from "@grammyjs/runner"; + +// Создайте бота. +const bot = new Bot(""); + +// Создайте уникальный идентификатор для объекта `Context`. +function getSessionKey(ctx: Context) { + return ctx.chat?.id.toString(); +} + +// Последовательность перед доступом к данным сессии! +bot.use(sequentialize(getSessionKey)); +bot.use(session({ getSessionKey })); + +// Добавьте обычный middleware, теперь с поддержкой безопасных сессий. +bot.on("message", (ctx) => ctx.reply("Получил ваше сообщение.")); + +// По-прежнему запускайте их одновременно! +run(bot); +``` + +```js [JavaScript] +const { Bot, Context, session } = require("grammy"); +const { run, sequentialize } = require("@grammyjs/runner"); + +// Создайте бота. +const bot = new Bot(""); + +// Создайте уникальный идентификатор для объекта `Context`. +function getSessionKey(ctx) { + return ctx.chat?.id.toString(); +} + +// Последовательность перед доступом к данным сессии! +bot.use(sequentialize(getSessionKey)); +bot.use(session({ getSessionKey })); + +// Добавьте обычный middleware, теперь с поддержкой безопасных сессий. +bot.on("message", (ctx) => ctx.reply("Получил ваше сообщение.")); + +// По-прежнему запускайте их одновременно! +run(bot); +``` + +```ts [Deno] +import { Bot, Context, session } from "https://deno.land/x/grammy/mod.ts"; +import { run, sequentialize } from "https://deno.land/x/grammy_runner/mod.ts"; + +// Создайте бота. +const bot = new Bot(""); + +// Создайте уникальный идентификатор для объекта `Context`. +function getSessionKey(ctx: Context) { + return ctx.chat?.id.toString(); +} + +// Последовательность перед доступом к данным сессии! +bot.use(sequentialize(getSessionKey)); +bot.use(session({ getSessionKey })); + +// Добавьте обычный middleware, теперь с поддержкой безопасных сессий. +bot.on("message", (ctx) => ctx.reply("Получил ваше сообщение.")); + +// По-прежнему запускайте их одновременно! +run(bot); +``` + +::: + +Не стесняйтесь присоединиться к [Telegram-чату](https://t.me/grammyjs), чтобы обсудить, как использовать grammY-runner с вашим ботом. +Мы всегда рады услышать людей, которые поддерживают больших ботов, чтобы мы могли улучшить grammY, основываясь на их опыте работы с пакетом. diff --git a/site/docs/ru/advanced/structuring.md b/site/docs/ru/advanced/structuring.md new file mode 100644 index 000000000..d6f8569b0 --- /dev/null +++ b/site/docs/ru/advanced/structuring.md @@ -0,0 +1,120 @@ +# Масштабирование I: Большая кодовая база + +Как только ваш бот возрастёт, вы столкнетесь с проблемой структурирования кода. +Естественно, вы можете разделить код по файлам. + +## Возможное решение + +> grammY еще довольно молод и пока не предоставляет официальных интеграций с DI-контейнерами. +> Подпишитесь на [@grammyjs_news](https://t.me/grammyjs_news), чтобы получить уведомление, как только мы начнем поддерживать это. + +Вы вольны структурировать свой код так, как вам нравится, и не существует универсального решения. +Тем не менее, простая и проверенная стратегия структурирования кода заключается в следующем. + +1. Группируйте вещи, которые семантически принадлежат друг другу, в одном файле (или, в зависимости от размера кода, директории). + Каждая из этих частей раскрывает middleware, который будет обрабатывать назначенные сообщения. +2. Централизованно создайте экземпляр бота, который объединит все middleware. +3. (Необязательно) Предварительно отфильтруйте обновления централизованно и отправляйте их только в нужном направлении. + Для этого вам может пригодиться `bot.route` ([ссылка на API](/ref/core/composer#route)) или, как вариант, плагин [router](../plugins/router). + +Выполняемый пример, реализующий описанную выше стратегию, можно найти в репозитории [бота для примера](https://github.com/grammyjs/examples/tree/main/scaling). + +## Пример структуры + +Для очень простого бота, управляющего списком TODO, можно представить такую структуру. + +```asciiart:no-line-numbers +src/ +├── bot.ts +└── todo/ + ├── item.ts + └── list.ts +``` + +`item.ts` просто определяет некоторые вещи об элементах TODO, и эти части кода используются в `list.ts`. + +В `list.ts`, вы можете сделать следующее: + +```ts +export const lists = new Composer(); + +// Зарегистрируйте здесь несколько обработчиков, которые будут работать с вашим middleware обычным способом. +lists.on("message", async (ctx) => {/* ... */}); +``` + +> Обратите внимание, что если вы используете TypeScript, то при создании Composer вам нужно передать ваш [пользовательский тип контекста](../guide/context#кастомизация-объекта-контекста). +> Например, вам нужно будет использовать `new Composer()`. + +Как вариант, вы можете использовать [границы ошибок](../guide/errors#границы-ошибок) для обработки всех ошибок, возникающих внутри вашего модуля. + +Теперь в `bot.ts` вы можете установить этот модуль следующим образом: + +```ts +import { lists } from "./todo/list"; + +const bot = new Bot(""); + +bot.use(lists); +// ... здесь может быть множество других модулей как todo + +bot.start(); +``` + +Как вариант, вы можете использовать [плагин router](../plugins/router) или [`bot.route`](/ref/core/composer#route) для объединения различных модулей, если вы можете заранее определить, какой middleware за это отвечает. + +Однако помните, что точный способ структурирования вашего бота очень сложно назвать в общем виде. +Как и всегда в программном обеспечении, делайте так, чтобы это имело наибольший смысл :wink: + +## Определения типов для извлеченного middleware + +Приведенная выше структура с использованием Composer работает хорошо. +Однако иногда вы можете оказаться в ситуации, когда вам нужно извлечь обработчик в функцию, а не создавать новый Composer и добавлять в него логику. +Это потребует от вас добавления правильных определений типов в обработчики, поскольку они больше не могут быть выведены через Composer. + +grammY экспортирует определения типов для всех **узких типов middleware**, таких как middleware, который вы можете передавать обработчикам команд. +Кроме того, он экспортирует определения типов для **узких контекстных объектов**, которые используются в этом middleware. +Оба типа настраиваются вашим [пользовательским контекстным объектом](../guide/context#кастомизация-объекта-контекста). +Таким образом, обработчик команд будет иметь тип `CommandMiddleware` и его контекстный объект `CommandContext`. +Их можно использовать следующим образом. + +::: code-group + +```ts [Node.js] +import { + type CallbackQueryMiddleware, + type CommandContext, + type NextFunction, +} from "grammy"; + +function commandMiddleware(ctx: CommandContext, next: NextFunction) { + // обработка команд +} +const callbackQueryMiddleware: CallbackQueryMiddleware = (ctx) => { + // обработка запросов обратного вызова +}; + +bot.command(["start", "help"], commandMiddleware); +bot.callbackQuery("query-data", callbackQueryMiddleware); +``` + +```ts [Deno] +import { + type CallbackQueryMiddleware, + type CommandContext, + type NextFunction, +} from "https://deno.land/x/grammy/mod.ts"; + +function commandMiddleware(ctx: CommandContext, next: NextFunction) { + // обработка команд +} +const callbackQueryMiddleware: CallbackQueryMiddleware = (ctx) => { + // обработка запросов обратного вызова +}; + +bot.command(["start", "help"], commandMiddleware); +bot.callbackQuery("query-data", callbackQueryMiddleware); +``` + +::: + +Ознакомьтесь со справочником [псевдонимов типов](/ref/core/#type-aliases), чтобы увидеть обзор всех псевдонимов типов, которые экспортирует grammY. diff --git a/site/docs/ru/advanced/transformers.md b/site/docs/ru/advanced/transformers.md new file mode 100644 index 000000000..546960725 --- /dev/null +++ b/site/docs/ru/advanced/transformers.md @@ -0,0 +1,112 @@ +# Трансформация Bot API + +Middleware --- это функция, которая обрабатывает объект контекста, т.е. входящие данные. + +grammY также предоставляет вам противоположность этому. +Функция _трансформатор_ --- это функция, которая обрабатывает исходящие данные, т.е. + +- название метода API бота, который необходимо вызвать, и +- объект `payload`, соответствующий методу + +Вместо того чтобы иметь `next` в качестве последнего аргумента для вызова нижележащего middleware, вы получаете `prev` в качестве первого аргумента для использования функций вышележащего трансформатора. +Взглянув на сигнатуру типа `Transformer` ([ссылка на grammY API](/ref/core/transformer)), мы можем увидеть, как она отражает это. +Обратите внимание, что `Payload` ссылается на `payload`, который должен соответствовать данному методу, а `ApiResponse> --- это тип возврата вызванного метода. + +Последняя вызываемая трансформирующая функция --- это встроенный вызывающий элемент, который выполняет такие действия, как сериализация определенных полей JSON и, в конечном счете, вызов `fetch`. + +Нет эквивалента класса `Composer` для трансформирующей функций, потому что это, вероятно, излишне, но если вам это нужно, вы можете написать свой собственный. +PR приветствуется! :wink: + +## Установка трансформирующей функции + +Трансформирующие функции могут быть установлены в `bot.api`. +Вот пример трансформирующей функции, которая ничего не делает: + +```ts +// Трансформирующая функция, которая ничего не делает +bot.api.config.use((prev, method, payload, signal) => + prev(method, payload, signal) +); + +// Сравнение с таким же middleware +bot.use((ctx, next) => next()); +``` + +Вот пример трансформирующей функции, которая предотвращает все вызовы API: + +```ts +// Некорректно возвращают undefined вместо соответствующих типов объектов. +bot.api.config.use((prev, method, payload) => undefined as any); +``` + +Вы также можете установить трансформирующие функции в API-объект контекстного объекта. +Тогда трансформирующая функциия будет временно использоваться только для API-запросов, которые выполняются для этого конкретного контекстного объекта. +Вызовы к `bot.api` остаются незатронутыми. +Вызовы через контекстные объекты параллельно запущенного middleware также остаются незатронутыми. +Как только соответствующий middleware завершает свою работу, трансформирующая функция отбрасывается. + +```ts +bot.on("message", async (ctx) => { + // Устанавливается на все контекстные объекты, обрабатывающие сообщения. + ctx.api.config.use((prev, method, payload, signal) => + prev(method, payload, signal) + ); +}); +``` + +> Параметр `signal` должен всегда передаваться в `prev`. +> Он позволяет отменять запросы и важен для работы `bot.stop`. + +Трансформирующие функции, установленные на `bot.api`, будут предустановлены в каждый объект `ctx.api`. +Таким образом, вызовы к `ctx.api` будут преобразованы как теми трансформаторами, которые находятся в `ctx.api`, так и теми трансформаторами, которые установлены в `bot.api`. + +## Случаи использования трансформирующих функций + +Трансформирующие функции так же гибки, как и middleware, и имеют столько же различных применений. + +Например, плагин [grammY menu](../plugins/menu) устанавливает трансформирующую функцию для преобразования исходящих экземпляров меню в правильный payload. +Вы также можете использовать их для + +- реализации [контроля флуда](../plugins/transformer-throttler), +- имитировать API-запросы во время тестирования, +- добавить [повторение запросов](../plugins/auto-retry), или +- многое другое. + +Обратите внимание, что повторный вызов API может иметь странные побочные эффекты: если вы вызовете `endDocument` и передадите экземпляр потока, пригодного для чтения, в `InputFile`, то поток будет прочитан при первой попытке запроса. +Если вы снова вызовете `prev`, поток может быть уже (частично) использован, что приведет к битым файлам. +Поэтому более надежным способом является передача путей к файлам в `InputFile`, чтобы grammY мог воссоздать поток по мере необходимости. + +## Расширитель API + +В grammY есть [расширители контекста](../guide/context#расширители-контекста), которые можно использовать для настройки типа контекста. +Сюда входят методы API как те, которые находятся непосредственно в объекте контекста, такие как `ctx.reply`, так и все методы в `ctx.api` и `ctx.api.raw`. +Однако вы не можете изменять типы `bot.api` и `bot.api.raw` с помощью контекстных расширителей. + +Именно поэтому grammY поддерживает _Расширители API_. +Они решают эту проблему: + +```ts +import { Api, Bot, Context } from "grammy"; +import { SomeApiFlavor, SomeContextFlavor, somePlugin } from "some-plugin"; + +// Расширитель контекста +type MyContext = Context & SomeContextFlavor; +// Расширитель API +type MyApi = Api & SomeApiFlavor; + +// Использование двух расширителей +const bot = new Bot(""); + +// Использование плагина +bot.api.config.use(somePlugin()); + +// Теперь вызовите `bot.api` с настроенными типами из расширителя API. +bot.api.somePluginMethod(); + +// Кроме того, используйте настроенный тип контекста из расширителя котекста. +bot.on("message", (ctx) => ctx.api.somePluginMethod()); +``` + +Расширители API работают точно так же, как и контекстные расширители. +Существуют как аддитивные, так и трансформируемые расширители API, и несколько расширителей API можно комбинировать так же, как это делается с контекстными расширителями. +Если вы не знаете, как это работает, вернитесь к [разделу о контекстных расширителях](../guide/context#расширители-контекста) в руководстве. diff --git a/site/docs/ru/demo/README.md b/site/docs/ru/demo/README.md new file mode 100644 index 000000000..5097e760b --- /dev/null +++ b/site/docs/ru/demo/README.md @@ -0,0 +1,3 @@ +# Живая демонстрация в браузере + +Скоро будет, возвращайтесь позднее. diff --git a/site/docs/ru/demo/examples.md b/site/docs/ru/demo/examples.md new file mode 100644 index 000000000..584e963ac --- /dev/null +++ b/site/docs/ru/demo/examples.md @@ -0,0 +1,3 @@ +# Примеры ботов + +Скоро будет, возвращайтесь позднее. diff --git a/site/docs/ru/guide/README.md b/site/docs/ru/guide/README.md new file mode 100644 index 000000000..e8d75e4cd --- /dev/null +++ b/site/docs/ru/guide/README.md @@ -0,0 +1,45 @@ + + +![grammY](/images/grammY.svg) + +# Начало + +Добро пожаловать в grammY! +Вы пришли в нужное место. + +## Что такое grammY? + +grammY --- это фреймворк для создания ботов в Telegram. +Он может быть использован для написания ботов на TypeScript и JavaScript и работает на Node.js, Deno и в браузере. + +Ознакомьтесь с [Введением](./introduction), если вы новичок в написании Telegram ботов, особенно если у вас мало опыта в программировании. + +Если вы знаете, как писать проект для Node.js или Deno, или уже создали бота Telegram с помощью другого фреймворка для ботов, начните работу в считанные минуты с помощью нашего руководства [Начало работы](./getting-started). + +## О документации + +Документация по ботам grammY разделена на три уровня. + +1. Высокоуровневая документация grammY _(этот сайт)_ +2. [Низкоуровневая документация grammY API](/ref/) _(автогенерируемая часть этого сайта)_ +3. [Документация API Raw HTTP от Telegram](https://core.telegram.org/bots/api) + +**Первая часть** (вы смотрите на нее!) объясняет, как работают боты и как использовать grammY. +Это то, что вы будете использовать чаще всего. +Раздел _Изучение_ всегда является хорошим началом, так как в нем объясняются все основные концепции. +Также ознакомьтесь с нашей великолепной коллекцией _Плагинов_ и посмотрите на _Примеры_. + +**Вторая часть** --- это [grammY API](/ref/), ссылка на который находится в верхней части страницы. +Это подробный обзор каждого кусочка кода, который предоставляет grammY. +Он автоматически генерируется из кода grammY и содержит все полезные пояснения к подсказкам, которые обычно можно найти, наведя курсор на любой элемент grammY в редакторе кода. +То же самое справедливо для каждого плагина в экосистеме grammY. +Помимо того, что плагины отображаются в [справочнике API](/ref/), их ссылки на API находятся в описании в нижней части страницы каждого плагина. + +**Третья часть** предоставлена Telegram и содержит список исходных методов [HTTP API](https://core.telegram.org/bots/api), к которым grammY будет обращаться под капотом. +Справочик на API grammY ссылается на него везде, где это имеет смысл. +Загляните туда, если вас интересуют подробные параметры, которые вы можете передавать в вызовы API. + +::: tip Присоединяйтесь к сообществу! +У нас есть дружественный [чат сообщества](https://t.me/grammyjs) в Telegram, который приветствует всех новых участников. (Вы можете найти русскоязычный чат [здесь](https://t.me/grammyjs_ru)). +Присоединяйтесь к нам, чтобы получить помощь, задать вопросы, узнать советы и хитрости для вашего следующего проекта бота. +::: diff --git a/site/docs/ru/guide/api.md b/site/docs/ru/guide/api.md new file mode 100644 index 000000000..6a347b9cb --- /dev/null +++ b/site/docs/ru/guide/api.md @@ -0,0 +1,285 @@ +# API Бота + +## Основная информация + +Telegram боты взаимодействуют с серверами Telegram посредством HTTP-запросов. +Telegram Bot API --- это спецификация этого интерфейса, то есть [длинный список](https://core.telegram.org/bots/api) методов и типов данных, обычно называемый документацией. +Он определяет все, что могут делать Telegram боты. +Вы можете найти его по ссылке на вкладке "Ресурсы" в разделе "Telegram". + +Настройку можно представить следующим образом: + +```asciiart:no-line-numbers +Ваш бот на grammY <———HTTP———> API Бота <———MTProto———> Telegram +``` + +Другими словами: когда ваш бот отправляет сообщение, оно будет отправлено в виде HTTP запроса на сервер _Bot API_. +Этот сервер расположен по адресу `api.telegram.org`. +Он преобразует запрос в родной протокол Telegram под названием MTProto и отправит запрос на бэкенд Telegram, который позаботится об отправке сообщения пользователю. + +Аналогично, когда пользователь отвечает, используется обратный путь. + +Когда вы запускаете бота, вам нужно решить, как отправлять обновления через HTTP-соединение. +Это можно сделать с помощью [long polling или вебхуков](./deployment-types). + +Вы также можете самостоятельно разместить сервер Bot API. +В основном это полезно для отправки больших файлов или для уменьшения задержки. + +## Вызов API бота + +API Бота --- это то, что определяет, что могут и чего не могут делать боты. +Каждый метод Bot API имеет эквивалент в grammY, и мы следим за тем, чтобы библиотека всегда была синхронизирована с последними и самыми лучшими функциями для ботов. +Пример: `sendMessage` в [документации Telegram Bot API](https://core.telegram.org/bots/api#sendmessage) и в [документации grammY API](/ref/core/api#sendmessage). + +### Вызов метода + +Вы можете вызывать методы API через `bot.api`, или [эквивалентно](./context#доступные-деиствия) через `ctx.api`: + +::: code-group + +```ts [TypeScript] +import { Api, Bot } from "grammy"; + +const bot = new Bot(""); + +async function sendHelloTo12345() { + // Отправляет сообщение по ID 12345 + await bot.api.sendMessage(12345, "Привет!"); + + // Отправьте сообщение и сохраните ответ, который содержит информацию об отправленном сообщении. + const sentMessage = await bot.api.sendMessage(12345, "Привет снова!"); + console.log(sentMessage.message_id); + + // Отправка сообщения без объекта `bot`. + const api = new Api(""); // <-- поместите токен вашего бота между "". + await api.sendMessage(12345, "Йоу!"); +} +``` + +```js [JavaScript] +const { Api, Bot } = require("grammy"); + +const bot = new Bot(""); + +async function sendHelloTo12345() { + // Отправляет сообщение по ID 12345 + await bot.api.sendMessage(12345, "Привет!"); + + // Отправьте сообщение и сохраните ответ, который содержит информацию об отправленном сообщении. + const sentMessage = await bot.api.sendMessage(12345, "Привет снова!"); + console.log(sentMessage.message_id); + + // Отправка сообщения без объекта `bot`. + const api = new Api(""); // <-- поместите токен вашего бота между "". + await api.sendMessage(12345, "Йоу!"); +} +``` + +```ts [Deno] +import { Api, Bot } from "https://deno.land/x/grammy/mod.ts"; + +const bot = new Bot(""); + +async function sendHelloTo12345() { + // Отправляет сообщение по ID 12345 + await bot.api.sendMessage(12345, "Привет!"); + + // Отправьте сообщение и сохраните ответ, который содержит информацию об отправленном сообщении. + const sentMessage = await bot.api.sendMessage(12345, "Привет снова!"); + console.log(sentMessage.message_id); + + // Отправка сообщения без объекта `bot`. + const api = new Api(""); // <-- поместите токен вашего бота между "". + await api.sendMessage(12345, "Йоу!"); +} +``` + +::: + +> Обратите внимание, что `bot.api` --- это просто экземпляр `Api`, который предварительно сконструирован для вас для удобства. +> Заметьте также, что если у вас есть доступ к объекту контекста (т.е. вы находитесь внутри обработчика сообщений), всегда предпочтительнее вызывать `ctx.api` или одно из [доступных действий](./context#доступные-деиствия). + +Хотя экземпляры `Api` охватывают весь Bot API, они иногда немного изменяют сигнатуры функций, чтобы сделать их более удобными для использования. +Строго говоря, все методы Bot API ожидают JSON-объект с рядом свойств. +Однако обратите внимание, что `sendMessage` в приведенном выше примере кода получает два аргумента - идентификатор чата и текст. +grammY знает, что эти два значения относятся к свойствам `chat_id` и `text` соответственно, и создаст для вас правильный JSON-объект. + +Как уже упоминалось [ранее](./basics#отправка-сообщении), вы можете указать другие параметры в третьем аргументе типа `Other`: + +```ts +async function sendHelloTo12345() { + await bot.api.sendMessage(12345, "Привет!", { + parse_mode: "HTML", + }); +} +``` + +Более того, grammY заботится о многочисленных технических деталях, чтобы упростить использование API. +Например, некоторые специфические свойства в некоторых специфических методах должны быть `JSON.stringify` перед отправкой. +Об этом легко забыть, сложно отладить, и это нарушает вывод типов. +grammY позволяет задавать объекты последовательно во всем API и гарантирует, что нужные свойства будут подстроены для API на лету перед отправкой. + +### Определения типов для Bot API + +grammY поставляется с полным покрытием типов для Bot API. +Репозиторий [`@grammyjs/types`](https://github.com/grammyjs/types) содержит определения типов, которые grammY использует внутри. +Эти определения типов также напрямую экспортируются из основного пакета `grammy`, так что вы можете использовать их в своем собственном коде. + +#### Определения типов в Deno + +В Deno вы можете просто импортировать определения типов из файла `types.ts`, который находится рядом с файлом `mod.ts`: + +```ts +import { type Chat } from "https://deno.land/x/grammy/types.ts"; +``` + +#### Определения типов в Node.js + +В Node.js все гораздо сложнее. +Вам нужно импортировать типы из `grammy/types`. +Например, вы получаете доступ к типу `Chat` следующим образом: + +```ts +import { type Chat } from "grammy/types"; +``` + +Однако официально Node.js поддерживает импорт из подпутей только начиная с Node.js 16. +Следовательно, TypeScript требует, чтобы `moduleResolution` был установлен на `node16` или `nodenext`. +Настройте свой `tsconfig.json` соответствующим образом и добавьте выделенную строку: + +```json{4} +{ + "compilerOptions": { + // ... + "moduleResolution": "node16" + // ... + } +} +``` + +В некоторых случаях это может работать и без внесения изменений в конфигурацию TypeScript. + +::: warning Неправильная автоподсказка на Node.js +Если вы не измените файл `tsconfig.json`, как описано выше, может случиться так, что ваш редактор кода предложит в автоподсказке импортировать типы из `grammy/out/client` или что-то в этом роде. +**Все пути, начинающиеся с `grammy/out`, являются внутренними. Не используйте их.** +Они могут быть произвольно изменены в любой момент времени, поэтому мы настоятельно рекомендуем вам импортировать типы из `grammy/types`. +::: + +### Выполнение необработанных вызовов API + +Бывают случаи, когда вы хотите использовать оригинальные сигнатуры функций, но при этом полагаться на удобство API grammY (например, авто форматирование JSON там, где это необходимо). +grammY поддерживает это через свойства `bot.api.raw` (или `ctx.api.raw`). + +Вы можете вызывать методы raw следующим образом: + +```ts +async function sendHelloTo12345() { + await bot.api.raw.sendMessage({ + chat_id: 12345, + text: "Привет!", + parse_mode: "HTML", + }); +} +``` + +По сути, все параметры сигнатуры функции объединяются с объектом `options`, когда вы используете необработанный API. + +## Выбор места расположения дата-центра + +> [Пропустите](./filter-queries) до конца страницы, если вы только начинаете. + +Если вы хотите уменьшить сетевую задержку вашего бота, важно, где вы его разместите. + +Сервер Bot API, расположенный за `api.telegram.org`, находится в Амстердаме, Нидерланды. +Поэтому лучшим местом для запуска бота является Амстердам. + +::: tip Сравнение хостингов +Возможно, вас заинтересует наше [сравнение хостинг-провайдеров](../hosting/comparison#таблицы-сравнения). +::: + +Однако может найтись еще более подходящее место для запуска бота, хотя это потребует гораздо больше усилий. + +[Помните](#основная-информация) что API сервер бота на самом деле не содержит вашего бота. +Он только ретранслирует запросы, переводит между HTTP и MTProto и так далее. +API сервер бота может находиться в Амстердаме, но серверы Telegram распределены по трем разным местам: + +- Амстердам, Нидерланды +- Майами, Флорида, США +- Сингапур + +Таким образом, когда API сервер бота отправляет запрос на сервера Telegram, ему может потребоваться отправить данные через полмира. +Произойдет это или нет, зависит от дата центра самого бота. +дата-центр бота --- это тот же дата-центр, что и у пользователя, создавшего бота. +дата-центр пользователя зависит от многих факторов, в том числе от его местонахождения. + +Вот что можно сделать, если вы хотите еще больше уменьшить задержку. + +1. Свяжитесь с [@where_is_my_dc_bot](https://t.me/where_is_my_dc_bot) и отправьте файл, который был загружен под вашей учетной записью. + В нем будет указано местоположение вашего аккаунта. + Это также местоположение вашего бота. +2. Если ваш дата-центр находится в Амстердаме, вам ничего не нужно делать. + В противном случае продолжайте читать. +3. Купите [VPS](../hosting/comparison#vps) в месте расположения вашего дата-центра. +4. [Запустите локальный Bot API-сервер](#запуск-локального-api-сервера-бота) на этом VPS. +5. Разместите своего бота в том же месте, что и ваш дата-центр. + +Таким образом, каждый запрос будет проходить только кратчайшее расстояние между Telegram и вашим ботом. + +## Запуск локального API сервера бота + +У запуска локального API сервера бота есть два основных преимущества. + +1. Ваш бот может отправлять и получать большие файлы. +2. У вашего бота может быть меньше задержек при работе в сети (см. [выше](#выбор-места-расположения-дата-центра)). + +> Другие незначительные преимущества перечислены [здесь](https://core.telegram.org/bots/api#using-a-local-bot-api-server). + +Вы должны запускать API сервер бота на VPS. +Если вы попытаетесь запустить его в другом месте, он будет падать или терять сообщения. + +Вы также должны скомпилировать API сервер бота с нуля. +Если у вас есть опыт компиляции больших проектов на C++, это будет полезно, но если его нет, то вы можете просто скопировать инструкции по сборке и надеяться, что они сработают. + +**Самый простой способ запустить API сервер бота - следовать [генератору инструкций по сборке](https://tdlib.github.io/telegram-bot-api/build.html?os=Linux), предоставленному Telegram.** + +> Дополнительные параметры можно найти [в репозитории API сервера бота](https://github.com/tdlib/telegram-bot-api#installation). + +Сборка сервера дает вам исполняемый файл, который можно запустить. + +Вы получили этот исполняемый файл? +Теперь вы можете перенести своего бота на локальный API сервер бота! + +### Выход из API сервера бота + +Сначала вам нужно выйти из API сервера бота. +Возьмите этот URL и вставьте его в браузер (не забудьте заменить `<токен>` на ваш токен бота): + +```text +https://api.telegram.org/bot<токен>/logOut +``` + +Вы должны увидеть `{"ok":true,"result":true}`. + +### Настройка grammY для использования локального API сервера бота + +Далее вы можете указать grammY использовать ваш локальный API сервер бота вместо `api.telegram.org`. +Допустим, ваш бот работает на `localhost` на порту 8081. +Тогда вам следует использовать следующую конфигурацию. + +```ts +const bot = new Bot("", { // <-- используйте тот же токен, что и раньше + client: { apiRoot: "http://localhost:8081" }, +}); +``` + +Теперь вы можете снова запустить своего бота. +Он будет использовать локальный API сервер. + +> Если что-то пошло не так, и вы не знаете, как это исправить, сколько бы вы ни гуглили, не стесняйтесь присоединяться к нашему [чату сообщества](https://t.me/grammyjs) и просить помощи! +> Мы знаем о вашей ошибке даже меньше, чем вы, но наверняка сможем ответить на ваши вопросы. + +Помните, что вам также придется настроить свой код для работы с локальными путями к файлам вместо URL, указывающих на ваши файлы. +Например, вызов `getFile` даст вам `file_path`, который указывает на ваш локальный диск, а не на файл, который сначала нужно загрузить из Telegram. +Аналогично, у плагина [files](../plugins/files) есть метод `getUrl`, который больше не будет возвращать URL, а вместо него - абсолютный путь к файлу. + +Если вы захотите снова изменить эту конфигурацию и перенести бота на другой сервер, обязательно прочитайте [этот раздел](https://github.com/tdlib/telegram-bot-api#moving-a-bot-to-a-local-server) в README репозитория API сервера бота. diff --git a/site/docs/ru/guide/basics.md b/site/docs/ru/guide/basics.md new file mode 100644 index 000000000..a3edd1784 --- /dev/null +++ b/site/docs/ru/guide/basics.md @@ -0,0 +1,164 @@ +# Отправка и получение сообщений + +Как только вы запустите своего бота с помощью `bot.start()`, grammY предоставит +вашим слушателям сообщения, которые пользователи отправляют вашему боту. grammY +также предоставляет методы, позволяющие легко отвечать на эти сообщения. + +## Получение сообщений + +Самый простой способ прослушивания сообщений --- через `bot.on("message")` + +```ts +bot.on("message", async (ctx) => { + const message = ctx.message; // объект сообщения +}); +``` + +Однако есть ряд и других вариантов. + +```ts +// Обработка команд, например /start +bot.command("start", async (ctx) => {/* ... */}); + +// Сопоставляет текст сообщения со строкой или регулярным выражением. +bot.hears(/echo *(.+)?/, async (ctx) => {/* ... */}); +``` + +Вы можете использовать автоподсказку в редакторе кода, чтобы увидеть все +доступные варианты, или посмотреть [все методы](/ref/core/composer) класса +`Composer`. + +> [Подробнее](./filter-queries) о фильтрации определенных типов сообщений с +> помощью `bot.on()`. + +## Отправка сообщений + +Все методы, которые могут использовать боты +(**[важный список](https://core.telegram.org/bots/api#available-methods)**), +доступны в объекте `bot.api`. + +```ts +// Отправить сообщению по ID пользователя 12345. +await bot.api.sendMessage(12345, "Привет!"); +// В качестве опций вы можете передать объект options. +await bot.api.sendMessage(12345, "Привет!", {/* больше опций */}); +// Просмотрите объект отправленного сообщения +const message = await bot.api.sendMessage(12345, "Привет!"); +console.log(message.message_id); + +// Получите информацию о самом боте. +const me = await bot.api.getMe(); + +// т.д. +``` + +Каждый метод принимает необязательный объект options типа `Other`, который +позволяет задать дополнительные параметры для ваших вызовов API. Эти объекты +опций в точности соответствуют опциям, которые вы можете найти в списке методов +по ссылке выше. Вы также можете использовать автоподсказку в вашем редакторе +кода, чтобы увидеть все доступные параметры, или посмотреть +[все методы](/ref/core/api) класса `Api`. На остальной части этой страницы +показаны некоторые примеры. + +Также посмотрите [следующий раздел](./context), чтобы узнать, как объект +контекста слушателя делает отправку сообщений легким делом! + +## Отправка сообщений с ответом на сообщение + +Вы можете использовать функцию Telegram reply-to, указав идентификатор +сообщения, на которое нужно ответить, с помощью `reply_parameters`. + +```ts +bot.hears("ping", async (ctx) => { + // `reply` - это псевдоним для `sendMessage` в том же чате (см. следующий раздел). + await ctx.reply("pong", { + // `reply_parameters` задает фактическую функцию ответа. + reply_parameters: { message_id: ctx.msg.message_id }, + }); +}); +``` + +> Обратите внимание, что отправка сообщения через `ctx.reply` не означает, что +> вы автоматически отвечаете на что-либо. Вместо этого вы должны указать +> `reply_parameters` для этого. Функция `ctx.reply` --- это всего лишь псевдоним +> для `ctx.api.sendMessage`, см. +> [следующий раздел](./context#доступные-деиствия). + +Параметры ответа также позволяют вам отвечать на сообщения в других чатах, а +также цитировать части сообщения - или даже и то, и другое одновременно! +Ознакомьтесь с документацией API бота +[документация параметров ответа](https://core.telegram.org/bots/api#replyparameters). + +## Отправка сообщения с форматированием + +> Ознакомьтесь с +> [разделом о параметрах форматирования](https://core.telegram.org/bots/api#formatting-options) +> в Telegram Bot API, написанным командой Telegram. + +Вы можете отправлять сообщения с **жирным** или _курсивным_ текстом, +использовать URL-адреса и многое другое. Есть два способа сделать это, как +описано в +[разделе о параметрах форматирования](https://core.telegram.org/bots/api#formatting-options), +а именно: Markdown и HTML. + +### Markdown + +> Смотрите + +Отправьте сообщение с пометкой markdown в тексте и укажите +`parse_mode: "MarkdownV2"`. + +```ts +await bot.api.sendMessage( + 12345, + "*Привет\\!* _Добро пожаловать_ в [grammY](https://grammy.dev)\\.", + { parse_mode: "MarkdownV2" }, +); +``` + +### HTML + +> Смотрите + +Отправьте сообщение с HTML-элементами в тексте и укажите `parse_mode: "HTML"`. + +```ts +await bot.api.sendMessage( + 12345, + 'Привет! Добро пожаловать в grammY.', + { parse_mode: "HTML" }, +); +``` + +## Отправка файлов + +Более подробно работа с файлами описана в +[следующем разделе](./files#отправка-фаилов). + +## Принудительный ответ​ + +> Это может быть полезно, если ваш бот работает в +> [режиме конфиденциальности](https://core.telegram.org/bots/features#privacy-mode) +> в групповых чатах. + +Когда вы отправляете сообщение, вы можете сделать так, чтобы клиент Telegram +пользователя автоматически указывал это сообщение как ответ. Это означает, что +пользователь будет отвечать на сообщение вашего бота автоматически (если только +он не удалит ответ вручную). В результате ваш бот будет получать сообщения +пользователей даже при работе в режиме +[конфиденциальности](https://core.telegram.org/bots/features#privacy-mode) в +групповых чатах. + +Принудительно ответить можно следующим образом: + +```ts +bot.command("start", async (ctx) => { + await ctx.reply( + "Привет! Я могу читать только те сообщения, в которых отвечают на мои сообщения!", + { + // Сделайте так, чтобы Telegram клиенты автоматически показывали пользователю интерфейс ответа. + reply_markup: { force_reply: true }, + }, + ); +}); +``` diff --git a/site/docs/ru/guide/commands.md b/site/docs/ru/guide/commands.md new file mode 100644 index 000000000..7e9ca519d --- /dev/null +++ b/site/docs/ru/guide/commands.md @@ -0,0 +1,75 @@ +# Команды + +Команды --- это специальные объекты в сообщениях Telegram, которые служат инструкциями для ботов. + +## Использование + +> Пересмотрите раздел "Команды" в статье [Функции Telegram Ботов](https://core.telegram.org/bots/features#commands), написанной командой Telegram. + +В grammY предусмотрена специальная обработка команд (например, `/start` и `/help`). +Вы можете напрямую зарегистрировать слушатели для определенных команд через `bot.command()`. + +```ts +// Ответить на команду /start. +bot.command("start" /* , ... */); +// Ответить на команду /help. +bot.command("help" /* , ... */); +// Ответить на команды /a, /b, /c, и /d. +bot.command(["a", "b", "c", "d"] /* , ... */); +``` + +Обратите внимание, что обрабатываются только те команды, которые находятся в начале сообщения, поэтому если пользователь отправит `"Пожалуйста, не отправляйте /start этому боту!"`, то ваш слушатель не будет вызван, даже если команда `/start` _содержится_ в сообщении. + +Telegram поддерживает отправку целевых команд ботам, то есть команд, которые заканчиваются на `@имя_вашего_бота`. +grammY делает это автоматически, поэтому `bot.command("start")` будет соответствовать сообщениям с `/start` и с `/start@имя_вашего_бота` в качестве команд. +Вы можете выбрать соответствие только целевым командам, указав `bot.command("start@имя_вашего_бота")`. + +::: tip Предлагать команды пользователям +Вы можете вызвать + +```ts +await bot.api.setMyCommands([ + { command: "start", description: "Запустить бота" }, + { command: "help", description: "Показать текст для справки" }, + { command: "settings", description: "Открыть настройки" }, +]); +``` + +чтобы клиенты Telegram отображали список предлагаемых команд в поле ввода текста. + +Также вы можете настроить это, обратившись к [@BotFather](https://t.me/BotFather). +::: + +## Аргументы + +Пользователи могут отправлять **аргументы** вместе со своими командами. +Вы можете получить доступ к строке аргументов через `ctx.match`. + +```ts +bot.command("add", async (ctx) => { + // `item` будет "яблочный пирог" если пользователь отправит "/add яблочный пирог". + const item = ctx.match; +}); +``` + +Обратите внимание, что вы всегда можете получить доступ ко всему тексту сообщения через `ctx.msg.text`. + +## Поддержка Deep Linking + +> Пересмотрите раздел о глубокой перелинковке в статье [Функции Telegram Ботов](https://core.telegram.org/bots/features#deep-linking), написанной командой Telegram. + +Когда пользователь заходит на `https://t.me/имя_вашего_бота?start=payload`, его Telegram-клиент покажет кнопку ЗАПУСТИТЬ, которая (при нажатии) отправит строку из URL-параметра вместе с сообщением, в данном примере текст сообщения будет `"/start payload"`. +Клиенты Telegram не покажет payload пользователю (он увидит только `"/start"` в пользовательском интерфейсе), однако ваш бот получит его. +grammY извлекает этот payload для вас и предоставляет её в `ctx.match`. +В нашем примере с приведенной выше ссылкой `ctx.match` будет содержать строку `"payload"`. + +Deep Linking полезен, если вы хотите построить реферальную систему или отследить, где пользователи обнаружили вашего бота. +Например, ваш бот может отправить сообщение на канал с кнопкой [встроенной клавиатуры](../plugins/keyboard#встроенные-клавиатуры). +Кнопка содержит URL-адрес, как показано выше, например `https://t.me/имя_вашего_бота?start=замечательный-пост-канала-12345`. +Когда пользователь нажмет на кнопку под постом, его клиент Telegram откроет чат с вашим ботом и отобразит кнопку ЗАПУСТИТЬ, как описано выше. +Таким образом, ваш бот сможет определить, откуда пришел пользователь, и что он нажал на кнопку под постом конкретного канала. + +Естественно, вы можете встраивать такие ссылки куда угодно: в интернет, в сообщения, в QR-коды и т.д. + +Посмотрите [этот](https://core.telegram.org/api/links#bot-links) раздел документации Telegram, чтобы увидеть полный список возможных форматов ссылок. +Кроме того, они позволяют предложить пользователям добавить вашего бота в группы или каналы, а также по желанию предоставить ему необходимые права администратора. diff --git a/site/docs/ru/guide/context.md b/site/docs/ru/guide/context.md new file mode 100644 index 000000000..27155f412 --- /dev/null +++ b/site/docs/ru/guide/context.md @@ -0,0 +1,654 @@ +# Контекст + +Объект `Context` ([ссылка на grammY API](/ref/core/context)) является важной +частью grammY. + +Всякий раз, когда вы регистрируете слушателя на объекте бота, этот слушатель +получает объект контекста. + +```ts +bot.on("message", async (ctx) => { + // `ctx` это объект `Context`. +}); +``` + +Вы можете использовать объект контекста, чтобы + +- [получить доступ к информации о сообщении](#доступная-информация) +- [выполнения действий в ответ на сообщение](#доступные-деиствия). + +Обратите внимание, что объекты контекста обычно называются `ctx`. + +## Доступная информация + +Когда пользователь отправляет сообщение вашему боту, вы можете получить доступ к +нему через `ctx.message`. Например, чтобы получить текст сообщения, вы можете +сделать следующее: + +```ts +bot.on("message", async (ctx) => { + // При обработке текстовых сообщений `txt` будет `строкой`. + // Оно будет `undefined`, если в полученном сообщении нет текста сообщения, + // например, фотографии, стикеры и другие сообщения. + const txt = ctx.message.text; +}); +``` + +Аналогичным образом вы можете получить доступ к другим свойствам объекта +сообщения, например, к `ctx.message.chat` для получения информации о чате, в +который было отправлено сообщение. Посмотрите +[часть о `Message` в документации Telegram Bot API](https://core.telegram.org/bots/api#message), +чтобы узнать, какие данные доступны. Кроме того, вы можете просто использовать +автоподсказку в редакторе кода, чтобы увидеть возможные варианты. + +Если вы зарегистрируете свой слушатель для других типов, `ctx` также предоставит +вам информацию о них. Пример: + +```ts +bot.on("edited_message", async (ctx) => { + // Получите новый, отредактированный текст сообщения. + const editedText = ctx.editedMessage.text; +}); +``` + +Более того, вы можете получить доступ к необработанному объекту `Update` +([документация Telegram Bot API](https://core.telegram.org/bots/api#update)), +который Telegram отправляет вашему боту. Этот объект обновления (`ctx.update`) +содержит все данные, которые являются источниками `ctx.message` и т. п. + +Объект контекста всегда содержит информацию о вашем боте, доступную через +`ctx.me`. + +### Краткая запись + +На контекстном объекте установлено несколько кратких записей. + +| Параметр | Описание | +| -------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `ctx.msg` | Получает объект сообщения, а также отредактированные | +| `ctx.msgId` | Получает идентификатор сообщения для сообщений или реакций | +| `ctx.chat` | Получает объект чата | +| `ctx.chatId` | Получает идентификатор чата из `ctx.chat` или из обновлений `business_connection`. | +| `ctx.senderChat` | Получает объект чата отправителя из `ctx.msg` (для анонимных сообщений канала/группы). | +| `ctx.from` | Получает автора сообщения, запроса обратного вызова или других вещей | +| `ctx.inlineMessageId` | Получает идентификатор сообщения для callback queries или выбранных inline результатов | +| `ctx.businessConnectionId` | Получает идентификатор бизнес-соединения для сообщений или обновлений бизнес-соединения | +| `ctx.entities` | Получает сущности сообщения и их текст, опционально отфильтрованный по типу сущности | +| `ctx.reactions` | Получает реакции от обновления в [удобном для работы виде](./reactions#проверка-того-как-изменилась-реакция) | + +Другими словами, вы также можете сделать это: + +```ts +bot.on("message", async (ctx) => { + // Получите текст сообщения. + const text = ctx.msg.text; +}); + +bot.on("edited_message", async (ctx) => { + // Получите новый, отредактированный текст сообщения. + const editedText = ctx.msg.text; +}); + +bot.on("message:entities", async (ctx) => { + // Получите все сущности. + const entities = ctx.entities(); + + // Получите текст первой сущности. + entities[0].text; + + // Получать сущности которые являются электронной почтой + const emails = ctx.entities("email"); + + // Получать сущности которые являются электронной почтой и номером телефона + const phonesAndEmails = ctx.entities(["email", "phone"]); +}); + +bot.on("message_reaction", (ctx) => { + const { emojiAdded } = ctx.reactions(); + if (emojiAdded.includes("🎉")) { + await ctx.reply("вечеринОчка :D"); + } +}); +``` + +> Перейдите к [Реакциям](./reactions), если они вас интересуют. + +Таким образом, если вы хотите, вы можете забыть о `ctx.message`, +`ctx.channelPost` и `ctx.editedMessage` и так далее, и просто всегда +использовать `ctx.msg` вместо этого. + +## Поиск информации с помощью проверок + +У объекта контекста есть несколько методов, которые позволяют исследовать +содержащиеся в нем данные на предмет определенных вещей. Например, вы можете +вызвать `ctx.hasCommand("start")`, чтобы узнать, содержит ли объект контекста +команду `/start`. Именно поэтому методы получили общее название _has checks_. + +::: tip Знайте, когда использовать has checks +Это точно такая же логика, которая +используется в `bot.command("start")`. Обратите внимание, что обычно следует +использовать [фильтрующие запросы](./filter-queries) и подобные методы. +Использование has checks лучше всего работает в плагине +[conversations](../plugins/conversations). +::: + +Проверки has checks сужают тип контекста. Это означает, что проверка наличия в +контексте данных запроса обратного вызова сообщит TypeScript, что в контексте +присутствует поле `ctx.callbackQuery.data`. + +```ts +if (ctx.hasCallbackQuery(/query-data-\d+/)) { + // Известно, что `ctx.callbackQuery.data` присутствует здесь + const data: string = ctx.callbackQuery.data; +} +``` + +То же самое относится и ко всем другим has checks. Посмотрите +[API объекта контекста](/ref/core/context#has), чтобы увидеть список всех +проверок has. Также ознакомьтесь со статическим свойством `Context.has` в +[документации API](/ref/core/context#has), которое позволяет создавать +эффективные предикатные функции для проверки большого количества объектов +контекста. + +## Доступные действия + +Если вы хотите ответить на сообщение пользователя, вы можете написать следующее: + +```ts +bot.on("message", async (ctx) => { + // Получите идентификатор чата. + const chatId = ctx.msg.chat.id; + // Текст для ответа + const text = "Я получил ваше сообщение!"; + // Отправить ответ. + await bot.api.sendMessage(chatId, text); +}); +``` + +Вы можете заметить две неоптимальные вещи: + +1. Мы должны иметь доступ к объекту `bot`. Это означает, что мы должны + передавать объект `bot` по всей нашей кодовой базе, чтобы получить ответ, что + раздражает, когда у вас несколько исходных файлов и вы определяете слушателя + в другом месте. +2. Нам приходится извлекать идентификатор чата из контекста и снова явно + передавать его в `sendMessage. Это тоже раздражает, потому что вы, скорее + всего, всегда хотите ответить тому же пользователю, который отправил + сообщение. Представьте, как часто вы будете набирать одно и то же сообщение + снова и снова! + +Что касается пункта 1. Объект контекста просто предоставляет вам доступ к тому +же объекту API, который вы найдете в `bot.api`, он называется `ctx.api`. Теперь +вы можете написать `ctx.api.sendMessage` вместо этого, и вам больше не придется +передавать объект `bot`. Легко. + +Однако настоящая сила заключается в исправлении пункта 2. Объект контекста +позволяет вам просто отправить ответ, например, так: + +```ts +bot.on("message", async (ctx) => { + await ctx.reply("Я получил ваше сообщение!"); +}); + +// Или даже короче: +bot.on("message", (ctx) => ctx.reply("Попался!")); +``` + +Отлично! :tada: + +Под капотом контекст уже знает идентификатор чата (а именно `ctx.msg.chat.id`), +поэтому он предоставляет вам метод `reply`, чтобы просто отправить сообщение +обратно в тот же чат. Внутри, `reply` снова вызывает `sendMessage` с +предварительно заполненным идентификатором чата. + +Следовательно, все методы на объекте контекста принимают объекты опций типа +`Other`, как объяснялось [ранее](./basics#отправка-сообщении). Это можно +использовать для передачи дополнительных настроек при каждом вызове API. + +::: tip Функция ответа в Telegram +Несмотря на то, что в grammY (и многих других +фреймворках) метод называется `ctx.reply`, он не использует функцию +[ответа в Telegram](https://telegram.org/blog/replies-mentions-hashtags#replies), +при которой происходит привязка к предыдущему сообщению. + +Если вы посмотрите, что может делать `sendMessage` в +[документации API бота](https://core.telegram.org/bots/api#sendmessage), вы +увидите ряд опций, таких как `parse_mode`, `link_preview_options` и +`reply_parameters`. Последняя может быть использована для превращения сообщения +в ответ: + +```ts +await ctx.reply("^ Это сообщение!", { + reply_parameters: { message_id: ctx.msg.message_id }, +}); +``` + +Один и тот же объект options может быть передан в `bot.api.sendMessage` и +`ctx.api.sendMessage`. Используйте автоподсказки, чтобы увидеть доступные +параметры прямо в редакторе кода. +::: + +Естественно, каждый другой метод в `ctx.api` имеет ярлык с правильными +предварительно заполненными значениями, например `ctx.replyWithPhoto` для ответа +с фотографией или `ctx.exportChatInviteLink` для получения ссылки на приглашение +в соответствующий чат. Если вы хотите получить представление о том, какие ярлыки +существуют, то автоподсказки - ваш друг, а также +[документация grammY API](/ref/core/context). + +Обратите внимание, что вы можете не захотеть всегда реагировать в одном и том же +чате. В этом случае вы можете просто вернуться к использованию методов `ctx.api` +и указать все опции при их вызове. Например, если вы получили сообщение от Алисы +и хотите отреагировать на него, отправив сообщение Бобу, то вы не можете +использовать `ctx.reply`, потому что он всегда будет отправлять сообщения в чат +с Алисой. Вместо этого вызовите `ctx.api.sendMessage` и укажите идентификатор +чата Боба. + +## Как создаются контекстные объекты + +Всякий раз, когда ваш бот получает новое сообщение от Telegram, оно +оборачивается в объект обновления. На самом деле, объекты обновлений могут +содержать не только новые сообщения, но и все остальные вещи, такие как +редактирование сообщений, ответы на опросы и +[многое другое](https://core.telegram.org/bots/api#update). + +Свежий объект контекста создается ровно один раз для каждого входящего +обновления. Контексты для разных обновлений являются совершенно несвязанными +объектами, они лишь ссылаются на одну и ту же информацию о боте через `ctx.me`. + +Один и тот же объект контекста для одного обновления будет общим для всех +установленных на боте промежуточных программ ([документация](./middleware)). + +## Кастомизация объекта контекста + +> Если вы новичок в работе с контекстными объектами, вам не нужно беспокоиться +> об остальной части этой страницы. + +При желании вы можете установить собственные свойства для контекстного объекта. + +### Через Middleware (Рекомендуется) + +Настройки можно легко выполнить в [middleware](./middleware). + +::: tip Middleчто? +Этот раздел требует понимания middleware, поэтому, если вы +еще не перешли к этому [разделу](./middleware), вот очень краткое описание. + +Все, что вам действительно нужно знать, это то, что несколько обработчиков могут +обрабатывать один и тот же объект контекста. Существуют специальные обработчики, +которые могут изменять `ctx` до запуска других обработчиков, и изменения первого +обработчика будут видны всем последующим обработчикам. +::: + +Идея заключается в том, чтобы установить middleware до того, как вы +зарегистрируете другие слушатели. Затем вы можете установить нужные вам свойства +внутри этих обработчиков. Если вы сделаете +`ctx.yourCustomPropertyName = вашеСобственноеЗначение` внутри такого обработчика, то +свойство `ctx.yourCustomPropertyName` будет доступно и в остальных обработчиках. + +Для наглядности предположим, что вы хотите установить свойство `ctx.config` для +объекта контекста. В этом примере мы будем использовать его для хранения +некоторой конфигурации о проекте, чтобы все обработчики имели к ней доступ. С +помощью конфигурации будет легче определить, используется ли бот его +разработчиком или обычными пользователями. + +Сразу после создания бота сделайте следующее: + +```ts +const BOT_DEVELOPER = 123456; // идентификатор чата разработчика бота + +bot.use(async (ctx, next) => { + // Измените здесь объект контекста, установив параметры для config. + ctx.config = { + botDeveloper: BOT_DEVELOPER, + isDeveloper: ctx.from?.id === BOT_DEVELOPER, + }; + // Запустите оставшиеся обработчики. + await next(); +}); +``` + +После этого вы можете использовать `ctx.config` в остальных обработчиках. + +```ts +bot.command("start", async (ctx) => { + // Работайте с измененным контекстом! + if (ctx.config.isDeveloper) await ctx.reply("Привет, мам!! <3"); + else await ctx.reply("Здравствуй, человек!"); +}); +``` + +Однако вы заметите, что TypeScript не знает о наличии `ctx.config`, хотя мы +правильно назначаем свойство. Поэтому, хотя код и работает во время выполнения, +он не компилируется. Чтобы исправить это, нам нужно изменить тип контекста и +добавить свойство. + +```ts +interface BotConfig { + botDeveloper: number; + isDeveloper: boolean; +} + +type MyContext = Context & { + config: BotConfig; +}; +``` + +Новый тип `MyContext` теперь точно описывает объекты контекста, с которыми на +самом деле работает наш бот. + +> Вам нужно будет убедиться, что типы синхронизированы со свойствами, которые вы +> инициализируете. + +Мы можем использовать новый тип, передав его конструктору `Bot`. + +```ts +const bot = new Bot(""); +``` + +В общем, настройка будет выглядеть следующим образом: + +::: code-group + +```ts [TypeScript] +const BOT_DEVELOPER = 123456; // идентификатор чата разработчика бота + +// Определите пользовательский тип контекста. +interface BotConfig { + botDeveloper: number; + isDeveloper: boolean; +} +type MyContext = Context & { + config: BotConfig; +}; + +const bot = new Bot(""); + +// Установка пользовательских свойств для объектов контекста. +bot.use(async (ctx, next) => { + ctx.config = { + botDeveloper: BOT_DEVELOPER, + isDeveloper: ctx.from?.id === BOT_DEVELOPER, + }; + await next(); +}); + +// Определите обработчики для объектов пользовательского контекста. +bot.command("start", async (ctx) => { + if (ctx.config.isDeveloper) await ctx.reply("Привет, мам!"); + else await ctx.reply("Добро пожаловать"); +}); +``` + +```js [JavaScript] +const BOT_DEVELOPER = 123456; // идентификатор чата разработчика бота + +const bot = new Bot(""); + +// Установка пользовательских свойств для объектов контекста. +bot.use(async (ctx, next) => { + ctx.config = { + botDeveloper: BOT_DEVELOPER, + isDeveloper: ctx.from?.id === BOT_DEVELOPER, + }; + await next(); +}); + +// Определите обработчики для объектов пользовательского контекста. +bot.command("start", async (ctx) => { + if (ctx.config.isDeveloper) await ctx.reply("Привет, мам!"); + else await ctx.reply("Добро пожаловать"); +}); +``` + +::: + +Естественно, пользовательский тип контекста можно передавать и другим вещам, +которые работают с middleware, например [Composer](/ref/core/composer). + +```ts +const composer = new Composer(); +``` + +Некоторые плагины также требуют передачи пользовательского типа контекста, +например, плагин [router](../plugins/router) или [menu](../plugins/menu). +Ознакомьтесь с их документацией, чтобы узнать, как они могут использовать +пользовательский тип контекста. Эти типы называются контекстными вкусами, как +описано [здесь внизу](#расширители-контекста). + +### Через наследование + +Помимо установки пользовательских свойств для объекта контекста, вы можете +подклассифицировать класс `Context`. + +```ts +class MyContext extends Context { + // т.д. +} +``` + +Однако мы рекомендуем настраивать контекстный объект +[через middle](#через-middleware-рекомендуется), потому что это намного гибче и +лучше работает, если вы хотите установить плагины. + +Сейчас мы рассмотрим, как использовать пользовательские классы для контекстных +объектов. + +При создании бота вы можете передать ему пользовательский конструктор контекста, +который будет использоваться для инстанцирования объектов контекста. Обратите +внимание, что ваш класс должен расширять `Context`. + +::: code-group + +```ts [TypeScript] +import { Bot, Context } from "grammy"; +import type { Update, UserFromGetMe } from "grammy/types"; + +// Определите класс пользовательского контекста. +class MyContext extends Context { + // Установите некоторые пользовательские свойства. + public readonly customProp: number; + + constructor(update: Update, api: Api, me: UserFromGetMe) { + super(update, api, me); + this.customProp = me.username.length * 42; + } +} + +// Передайте конструктор класса пользовательского контекста в качестве параметра. +const bot = new Bot("", { + ContextConstructor: MyContext, +}); + +bot.on("message", async (ctx) => { + // `ctx` теперь имеет тип `MyContext`. + const prop = ctx.customProp; +}); + +bot.start(); +``` + +```js [JavaScript] +const { Bot, Context } = require("grammy"); + +// Определите класс пользовательского контекста. +class MyContext extends Context { + // Установите некоторые пользовательские свойства. + public readonly customProp; + + constructor(update, api, me) { + super(update, api, me); + this.customProp = me.username.length * 42; + } +} + +// Передайте конструктор класса пользовательского контекста в качестве параметра. +const bot = new Bot("", { + ContextConstructor: MyContext, +}); + +bot.on("message", async (ctx) => { + // `ctx` теперь имеет тип `MyContext`. + const prop = ctx.customProp; +}); + +bot.start(); +``` + +```ts [Deno] +import { Bot, Context } from "https://deno.land/x/grammy/mod.ts"; +import type { + Update, + UserFromGetMe, +} from "https://deno.land/x/grammy/types.ts"; + +// Определите класс пользовательского контекста. +class MyContext extends Context { + // Установите некоторые пользовательские свойства. + public readonly customProp: number; + + constructor(update: Update, api: Api, me: UserFromGetMe) { + super(update, api, me); + this.customProp = me.username.length * 42; + } +} + +// Передайте конструктор класса пользовательского контекста в качестве параметра. +const bot = new Bot("", { + ContextConstructor: MyContext, +}); + +bot.on("message", async (ctx) => { + // `ctx` теперь имеет тип `MyContext`. + const prop = ctx.customProp; +}); + +bot.start(); +``` + +::: + +Обратите внимание, что при использовании подкласса пользовательский тип +контекста будет определяться автоматически. Вам больше не нужно писать +`Bot`, потому что вы уже указали конструктор вашего подкласса в +объекте options `new Bot()`. + +Однако это сильно затрудняет (если не делает невозможным) установку плагинов, т.к. +они часто требуют установки расширителей контекста. + +## Расширители контекста + +Контекстные расширители --- это способ сообщить TypeScript о новых свойствах +вашего контекстного объекта. Эти новые свойства могут поставляться в плагинах +или других модулях, а затем устанавливаться на вашего бота. + +Контекстные расширители также могут преобразовывать типы существующих свойств с +помощью автоматических процедур, которые определяются плагинами. + +### Дополнительные расширители контекста + +Как подразумевалось выше, существует два различных вида расширителей контекста. +Основной из них называется _дополнительным расширителем контекста_, и всякий +раз, когда мы говорим о расширителях контекста, мы имеем в виду только эту +основную форму. Давайте посмотрим, как это работает. + +Например, когда у вас есть [данные о сессии](../plugins/session), вы должны +зарегистрировать `ctx.session` в типе контекста. В противном случае, + +1. вы не сможете установить встроенный плагин сессий, и +2. у вас не будет доступа к `ctx.session` в ваших слушателях. + +> Несмотря на то, что мы будем использовать сессии в качестве примера, подобные +> вещи применимы и ко многим другим. На самом деле, большинство плагинов +> предоставляют вам контекст, который вы должны использовать. + +Расширитель контекста --- это просто небольшой новый тип, определяющий свойства, +которые должны быть добавлены к типу контекста. Давайте рассмотрим пример такого +типа. + +```ts +interface SessionFlavor { + session: S; +} +``` + +Тип `SessionFlavor` ([документация API](/ref/core/sessionflavor)) прост: он +определяет только свойство `session`. Он принимает параметр типа, который +определяет фактическую структуру данных сессии. + +Чем это полезно? Так вы можете расширять свой контекст данными сессии: + +```ts +import { Context, SessionFlavor } from "grammy"; + +// Объявите `ctx.session` типом `string`. +type MyContext = Context & SessionFlavor; +``` + +Теперь вы можете использовать плагин сессий, и у вас есть доступ к +`ctx.session`: + +```ts +bot.on("message", async (ctx) => { + // Теперь `str` имеет тип `string`. + const str = ctx.session; +}); +``` + +### Преобразованные расширители контекста + +Другая разновидность расширителей контекста более мощная. Вместо того чтобы +устанавливать их с помощью оператора `&`, их нужно устанавливать следующим +образом: + +```ts +import { Context } from "grammy"; +import { SomeFlavorA } from "my-plugin"; + +type MyContext = SomeFlavorA; +``` + +Все остальное работает точно так же. + +Каждый (официальный) плагин указывает в своей документации, должен ли он +использоваться через дополнительный или через преобразованных расширители +контекста. + +### Комбинирование разных расширителей контекста + +Если у вас есть разные +[дополнительных расширителей контекста](#дополнительные-расширители-контекста), +вы можете просто установить их следующим образом: + +```ts +type MyContext = Context & FlavorA & FlavorB & FlavorC; +``` + +Порядок следования расширителей контекста не имеет значения, вы можете +комбинировать их в любом порядке. + +Можно также комбинировать несколько +[преобразованных расширителей контекста](#преобразованные-расширители-контекста): + +```ts +type MyContext = FlavorX>>; +``` + +Здесь порядок может иметь значение, поскольку `FlavorZ` сначала преобразует +`Context`, затем `FlavorY`, а результат этого преобразования будет снова +преобразован `FlavorX`. + +Вы даже можете смешивать дополнительные и преобразованные расширители: + +```ts +type MyContext = FlavorX< + FlavorY< + FlavorZ< + Context & FlavorA & FlavorB & FlavorC + > + > +>; +``` + +Обязательно следуйте этому шаблону при установке нескольких плагинов. Существует +ряд ошибок типа, которые возникают из-за неправильного сочетания расширителей +контекста. diff --git a/site/docs/ru/guide/deployment-types.md b/site/docs/ru/guide/deployment-types.md new file mode 100644 index 000000000..80f42fd9a --- /dev/null +++ b/site/docs/ru/guide/deployment-types.md @@ -0,0 +1,434 @@ +--- +next: false +--- + +# Long Polling против Вебхуков + +Существует два способа, с помощью которых ваш бот может получать сообщения с серверов Telegram. +Они называются _long polling_ и _вебхуки_. +grammY поддерживает оба этих способа, при этом long polling используется по умолчанию. + +В этом разделе мы расскажем о том, что такое long polling и вебхуки, а также о преимуществах и недостатках использования того или иного способа развертывания. +Также будет рассказано о том, как использовать их в grammY. + +## Введение + +Вы можете рассматривать всю дискуссию о вебхуках против long polling как вопрос о том, какой _тип развертывания_ использовать. +Другими словами, есть два принципиально разных способа разместить бота (запустить его на каком-то сервере), и они отличаются тем, как сообщения доходят до бота и могут быть обработаны grammY. + +Этот выбор имеет большое значение, когда вам нужно решить, где разместить своего бота. +Например, некоторые провайдеры инфраструктуры поддерживают только один из двух типов развертывания. + +Ваш бот может либо получать их (long polling), либо сервера Telegram могут передавать их вашему боту (вебхуки). + +> Если вы уже знаете, как это работает, прокрутите страницу вниз, чтобы узнать, как использовать [long polling](#как-использовать-long-polling) или [вебхуки](#как-использовать-вебхуки) с помощью grammY. + +## Как работает long polling? + +_Представьте, что вы покупаете шарик мороженого в своем любимом магазине. +Вы подходите к сотруднику и спрашиваете свой любимый сорт мороженого. +К сожалению, он сообщает вам, что его нет в наличии._ + +_На следующий день вам снова захотелось вкусного мороженого, и вы снова идете в то же место и просите то же самое мороженое. +Хорошие новости! +За ночь они пополнили запасы, и вы можете насладиться мороженым с соленой карамелью уже сегодня! +Вкуснятина._ + +**Polling** означает, что grammY проактивно отправляет запрос в Telegram, запрашивая новые обновления (считайте: сообщения). +Если сообщений нет, Telegram вернет пустой список, указывающий на то, что с момента последнего запроса вашему боту не было отправлено ни одного нового сообщения. + +Когда grammY отправляет запрос в Telegram и за это время вашему боту были отправлены новые сообщения, Telegram вернет их в виде массива, включающего до 100 объектов update. + +```asciiart:no-line-numbers +______________ _____________ +| | | | +| | <--- Появились сообщения? --- | | +| | --- не-а. ---> | | +| | | | +| | <--- Появились сообщения? --- | | +| Telegram | --- не-а. ---> | Бот | +| | | | +| | <--- Появились сообщения? --- | | +| | --- да, вот, держите ---> | | +| | | | +|____________| |___________| +``` + +Сразу видно, что это имеет ряд недостатков. +Ваш бот получает новые сообщения только при каждом запросе, то есть каждые несколько секунд или около того. +Чтобы бот отвечал быстрее, можно просто посылать больше запросов и не ждать так долго между ними. +Например, мы можем запрашивать новые сообщения каждую миллисекунду! Что может пойти не так... + +Вместо того, чтобы решать спамить серверы Telegram, мы будем использовать _long polling_ вместо обычного (короткого) polling. + +**Long polling** означает, что grammY проактивно отправляет запрос в Telegram, запрашивая новые обновления. +Если сообщений нет, Telegram будет держать соединение открытым до тех пор, пока не появятся новые сообщения, а затем ответит на запрос этими новыми сообщениями. + +_Снова пора за мороженым! +Сотрудник уже приветствует вас по имени. +На вопрос о мороженом вашего любимого сорта сотрудник улыбается вам и замирает. +На самом деле, вы не получаете никакого ответа. +Поэтому вы решаете подождать, твердо улыбаясь в ответ. +И вы ждете. +И ждете._ + +_За несколько часов до следующего восхода солнца приезжает грузовик местной компании по доставке продуктов и заносит в подсобное помещение магазина несколько больших коробок. +Снаружи на них написано **мороженое**. +Наконец-то работник снова начинает двигаться. +"Конечно, у нас есть соленая карамель! +Две ложечки с посыпкой, как обычно?"_ + +_Как ни в чем не бывало, вы наслаждаетесь мороженым, покидая самый нереальный в мире магазин._ + +```asciiart:no-line-numbers +______________ _____________ +| | | | +| | <--- Появились сообщения? --- | | +| | . | | +| | . | | +| | . *Оба терпеливо ждут* | | +| Telegram | . | Бот | +| | . | | +| | . | | +| | --- да, вот, держите ---> | | +| | | | +|____________| |___________| +``` + +> Обратите внимание, что в реальности ни одно соединение не будет оставаться открытым часами. +> Длительные опросные запросы имеют тайм-аут по умолчанию 30 секунд (чтобы избежать ряда [технических проблем](https://datatracker.ietf.org/doc/html/rfc6202#section-5.5)). +> Если по истечении этого времени не будет получено новых сообщений, то запрос будет отменен и отправлен заново - но общая концепция остается прежней. + +Используя long polling, вам не нужно спамить сервера Telegram, и при этом вы сразу же получаете новые сообщения! +Замечательно. +Это то, что grammY делает по умолчанию, когда вы запускаете `bot.start()`. + +## Как работают вебхуки? + +_После этого ужасающего опыта (целая ночь без мороженого!) вы предпочитаете больше никого не спрашивать о мороженом. +Разве не было бы здорово, если бы мороженое само приходило к вам?_ + +Настройка **вебхука** означает, что вы предоставите Telegram URL-адрес, доступный из публичного интернета. +Всякий раз, когда вашему боту будет отправлено новое сообщение, Telegram (а не вы!) возьмет на себя инициативу и отправит запрос с объектом обновления на ваш сервер. +Мило, да? + +_Вы решаете в последний раз сходить в магазин. +Вы говорите своему другу за прилавком, где вы живете. +Он обещает лично приходить к вам в квартиру, когда там появится новое мороженое (потому что оно растает на почте). +Классный парень._ + +```asciiart:no-line-numbers +______________ _____________ +| | | | +| | | | +| | | | +| | *оба терпеливо ждут* | | +| | | | +| Telegram | | Бот | +| | | | +| | | | +| | --- Привет, новое сообщение ---> | | +| | <--- спасибо, чувак --- | | +|____________| |___________| +``` + +## Сравнение + +**Основное преимущество long polling перед вебхуками заключается в том, что он проще.** +Вам не нужен домен или публичный URL. +Вам не нужно возиться с настройкой SSL-сертификатов, если вы запускаете бота на VPS. +Используйте `bot.start()`, и все будет работать, не требуя дополнительной настройки. +При нагрузке вы полностью контролируете количество обрабатываемых сообщений. + +Места, где long polling работает хорошо: + +- Во время разработки локально. +- На большинстве серверов. +- На хостируемых "внутренних" экземплярах, т.е. машинах, на которых ваш бот активно работает 24 часа в сутки 7 дней в неделю. + +**Основное преимущество вебхуков перед long polling заключается в том, что они дешевле.** +Вы экономите тонну лишних запросов. +Вам не нужно постоянно держать открытым сетевое соединение. +Вы можете использовать сервисы, которые автоматически сводят инфраструктуру к нулю при отсутствии запросов. +При желании можно даже [сделать вызов API при ответе на запрос Telegram](#ответ-вебхука), хотя это и имеет ряд недостатков. +Ознакомьтесь с настройкой [здесь](/ref/core/apiclientoptions#canusewebhookreply). + +Места, где вебхуки работают хорошо: + +- На серверах с SSL-сертификатами. +- На размещенных "внешних" экземплярах, которые масштабируются в зависимости от нагрузки. +- На бессерверных платформах, таких как облачные функции или программируемые пограничные сети. + +## Я до сих пор не знаю, что использовать + +Тогда выбирайте long polling. +Если у вас нет веских причин использовать вебхуки, обратите внимание, что у long polling нет серьезных недостатков, и, согласно нашему опыту, вы потратите гораздо меньше времени на исправление ошибок. +Время от времени вебхуки могут быть немного неприятными (см. [ниже](#своевременное-завершение-запросов-вебхуков)). + +Что бы вы ни выбрали, если у вас возникнут серьезные проблемы, переключиться на другой тип развертывания будет несложно. +В случае с grammY вам придется изменить всего несколько строк кода. +Настройка вашего [middleware](./middleware) не изменится. + +## Как использовать long polling + +Вызовите + +```ts +bot.start(); +``` + +для запуска вашего бота с очень простой формой long polling. +Он обрабатывает все обновления последовательно. +Это делает вашего бота очень простым в отладке, а его поведение - очень предсказуемым, поскольку в нем нет параллелизма. + +Если вы хотите, чтобы ваши сообщения обрабатывались grammY одновременно, или вас беспокоит пропускная способность, ознакомьтесь с разделом о [grammY runner](../plugins/runner). + +## Как использовать вебхуки + +Если вы хотите запустить grammY с помощью вебхуков, вы можете интегрировать бота в веб-сервер. +Поэтому мы ожидаем, что вы сможете запустить простой веб-сервер с выбранным вами фреймворком. + +Каждый бот grammY может быть преобразован в middleware для ряда веб-фреймворков, включая `express`, `koa`/`oak` и другие. +Вы можете импортировать функцию `webhookCallback` ([документация API](/ref/core/webhookcallback)), чтобы создать middleware для соответствующего фреймворка. + +::: code-group + +```ts [TypeScript] +import express from "express"; + +const app = express(); // или то, что вы используете +app.use(express.json()); // спарсите тело JSON запроса + +// "express" также используется по умолчанию, если аргумент не указан. +app.use(webhookCallback(bot, "express")); +``` + +```js [JavaScript] +const express = require("express"); + +const app = express(); // или то, что вы используете +app.use(express.json()); // спарсите тело JSON запроса + +// "express" также используется по умолчанию, если аргумент не указан. +app.use(webhookCallback(bot, "express")); +``` + +```ts [Deno] +import { Application } from "https://deno.land/x/oak/mod.ts"; + +const app = new Application(); // или то, что вы используете + +// Обязательно укажите, какой фреймворк вы используете. +app.use(webhookCallback(bot, "oak")); +``` + +::: + +> Обратите внимание, что вы не должны вызывать `bot.start()` при использовании webhooks. + +Теперь ваше приложение прослушивает запросы вебхуков от Telegram. +Последнее, что вам нужно сделать, это указать Telegram, куда отправлять обновления. +Есть несколько способов сделать это, но в конечном итоге все они просто вызывают `setWebhook`, как описано [здесь](https://core.telegram.org/bots/api#setwebhook). + +Самый простой способ установить вебхук --- вставить следующий URL в адресную строку браузера, заменив `<токен>` на токен вашего бота, а `` на публичную конечную точку вашего сервера. + +```txt +https://api.telegram.org/bot<токен>/setWebhook?url= +``` + +Мы также создали соответствующий интерфейс для этого, если вы предпочитаете управлять вебхуком через веб-сайт. +Вы можете найти его здесь: + +Обратите внимание, что вы также можете установить свой вебхук из кода: + +```ts +const endpoint = ""; // <-- поместите сюда свой URL +await bot.api.setWebhook(endpoint); +``` + +Наконец, обязательно прочитайте [замечательное руководство Марвина по всем вещам, связанным с вебхуками](https://core.telegram.org/bots/webhooks), написанное командой Telegram, если вы рассматриваете [запуск бота на вебхуках на VPS](../hosting/vps#запуск-бота-на-вебхуках). + +### Адаптеры для веб-фреймворков + +Для того чтобы поддерживать множество различных веб-фреймворков, в grammY используется концепция **адаптеров**. +Каждый адаптер отвечает за передачу входных и выходных данных от веб-фреймворка к grammY и наоборот. +Второй параметр, передаваемый в `webhookCallback` ([документация API](/ref/core/webhookcallback)), определяет адаптер фреймворка, используемый для связи с веб-фреймворком. + +Из-за того, что этот подход работает, нам обычно нужен адаптер для каждого фреймворка, но, поскольку некоторые фреймворки имеют схожий интерфейс, существуют адаптеры, которые, как известно, работают с несколькими фреймворками. +Ниже приведена таблица с доступными на данный момент адаптерами, а также фреймворками, API или режимами выполнения, с которыми они работают. + +| Адаптер | Фреймворк/API/Среда выполнения | +| ------------------ | ------------------------------------------------------------------------------ | +| `aws-lambda` | AWS Lambda Functions | +| `aws-lambda-async` | AWS Lambda Functions с `async`/`await` | +| `azure` | Azure Functions | +| `bun` | `Bun.serve` | +| `cloudflare` | Cloudflare Workers | +| `cloudflare-mod` | Cloudflare Module Workers | +| `express` | Express, Google Cloud Functions | +| `fastify` | Fastify | +| `hono` | Hono | +| `http`, `https` | Node.js `http`/`https` modules, Vercel | +| `koa` | Koa | +| `next-js` | Next.js | +| `nhttp` | NHttp | +| `oak` | Oak | +| `serveHttp` | `Deno.serveHttp` | +| `std/http` | `Deno.serve`, `std/http`, `Deno.upgradeHttp`, `Fresh`, `Ultra`, `Rutt`, `Sift` | +| `sveltekit` | SvelteKit | +| `worktop` | Worktop | + +### Ответ вебхука + +При получении запроса на вебхук ваш бот может вызвать до одного метода в ответе. +Преимуществом является то, что это избавляет вашего бота от необходимости делать до одного HTTP-запроса на каждое обновление. +Однако у этого способа есть ряд недостатков: + +1. Вы не сможете обработать возможные ошибки соответствующего вызова API. + К ним относятся ошибки ограничения скорости, поэтому вы не сможете гарантировать, что ваш запрос будет иметь какой-либо эффект. +2. Что еще более важно, у вас также не будет доступа к объекту ответа. + Например, вызов `sendMessage не даст вам доступа к отправленному сообщению. +3. Кроме того, невозможно отменить запрос. + Сигнал `AbortSignal` будет проигнорирован. +4. Обратите внимание, что типы в grammY не отражают последствий выполненного обратного вызова вебхука! + Например, они указывают на то, что вы всегда получаете объект ответа, так что вы сами должны убедиться в том, что вы не облажаетесь при использовании этой незначительной оптимизации производительности. + +Если вы хотите использовать вебхук-ответы, вы можете указать опцию `canUseWebhookReply` в опции `client` вашего `BotConfig` ([документация API](/ref/core/botconfig)). +Передайте функцию, которая определяет, использовать или нет ответ вебхука для данного запроса, идентифицированного методом. + +```ts +const bot = new Bot("", { + client: { + // Мы принимаем недостаток ответов веб-хуков для ввода статуса. + canUseWebhookReply: (method) => method === "sendChatAction", + }, +}); +``` + +Вот как работают ответы на вебхуки под капотом. + +```asciiart:no-line-numbers +______________ _____________ +| | | | +| | | | +| | | | +| | *оба терпиливо ждут* | | +| | | | +| Telegram | | Бот | +| | | | +| | | | +| | --- новое сообщение ---> | | +| | <--- окей, sendChatAction --- | | +|____________| |___________| +``` + +### Своевременное завершение запросов вебхуков + +> Вы можете игнорировать остальную часть этой страницы, если все ваши middleware завершаются быстро, т.е. в течение нескольких секунд. +> Этот раздел предназначен в первую очередь для тех, кто хочет выполнять передачу файлов в ответ на сообщения или другие операции, требующие больше времени. + +Когда Telegram отправляет обновление из одного чата вашему боту, он будет ждать, пока вы завершите запрос, прежде чем доставить следующее обновление, относящееся к этому чату. +Другими словами, Telegram будет доставлять обновления из одного и того же чата последовательно, а обновления из разных чатов отправляются параллельно. +(Источник этой информации --- [здесь](https://github.com/tdlib/telegram-bot-api/issues/75#issuecomment-755436496)). + +Telegram старается сделать так, чтобы ваш бот получал все обновления. +Это означает, что если доставка обновлений в чат не удалась, последующие обновления будут стоять в очереди до тех пор, пока не удастся получить первое обновление. + +#### Почему не завершать запрос вебхука опасно + +У Telegram есть тайм-аут для каждого обновления, которое он отправляет на конечную точку вебхука. +Если вы не завершите запрос вебхука достаточно быстро, Telegram повторно отправит обновление, считая, что оно не было доставлено. +В результате ваш бот может неожиданно обработать одно и то же обновление несколько раз. +Это означает, что он будет выполнять всю обработку обновления, включая отправку любых ответных сообщений, несколько раз. + +```asciiart:no-line-numbers +______________ _____________ +| | | | +| | --- новое сообщение ---> | | +| | . | | +| | *бот обрабатывает* . | | +| | . | | +| Telegram | --- Я сказал сообщение!!! ---> | Бот | +| | .. | | +| | *бот обрабатывает дважды* .. | | +| | .. | | +| | --- АЛЛЛЛОООООО ---> | | +| | ... | | +| | *бот обрабатывает трижды* ... | | +|____________| ... |___________| +``` + +Именно поэтому в функции `webhookCallback` у grammY есть свой собственный, более короткий тайм-аут (по умолчанию: 10 секунд). +Если ваш middleware завершит работу раньше, функция `webhookCallback` ответит на вебхук автоматически. +В этом случае все в порядке. +Однако если ваш middleware не завершится до истечения тайм-аута, установленного grammY, `webhookCallback` выбросит ошибку. +Это означает, что вы можете обработать ошибку в своем веб-фреймворке. +Если у вас нет такой обработки ошибок, Telegram снова отправит то же самое обновление - но, по крайней мере, у вас теперь будут журналы ошибок, чтобы сообщить вам, что что-то не так. + +Когда Telegram отправит обновление вашему боту во второй раз, маловероятно, что ваша обработка будет быстрее, чем в первый раз. +В результате, скорее всего, снова произойдет тайм-аут, и Telegram снова отправит обновление. +Таким образом, ваш бот увидит обновление не просто два раза, а несколько десятков раз, пока Telegram не прекратит повторные попытки. +Вы можете заметить, что ваш бот начнет спамить пользователей, поскольку он пытается обработать все эти обновления (которые на самом деле каждый раз одни и те же). + +#### Почему досрочное завершение запроса вебхука также опасно + +Вы можете настроить `webhookCallback` так, чтобы он не выбрасывал ошибку по истечении тайм-аута, а завершал запрос вебхука досрочно, даже если ваш middleware все еще работает. +Это можно сделать, передав `"return"` в качестве третьего аргумента в `webhookCallback`, вместо значения по умолчанию `"throw"`. +Однако, несмотря на то, что такое поведение имеет несколько правильных вариантов использования, подобное решение обычно вызывает больше проблем, чем решает. + +Помните, что как только вы ответите на запрос вебхука, Telegram отправит следующее обновление для этого чата. +Однако, поскольку старое обновление все еще обрабатывается, два обновления, которые ранее обрабатывались последовательно, внезапно обрабатываются параллельно. +Это может привести к возникновению условий гонки. +Например, плагин сессии неизбежно сломается из-за опасности [WAR](https://en.wikipedia.org/wiki/Hazard_(computer_architecture)#Write_after_read_(WAR)). +**Это приводит к потере данных!** +Другие плагины и даже ваш собственный middleware тоже может сломаться. +Степень этого неизвестна и зависит от вашего бота. + +#### Как решить эту проблему + +Этот ответ легче сказать, чем сделать. +**Ваша работа заключается в том, чтобы убедиться, что ваш middleware завершается достаточно быстро.** +Не используйте долго выполняющиеся middleware. +Да, мы знаем, что вы, возможно, _хотите_ иметь долго выполняющиеся задачи. +Но все же. +Не делайте этого. +Только не в вашем middleware. + +Вместо этого используйте очередь (существует множество систем очередей, от очень простых до очень сложных). +Вместо того чтобы пытаться выполнить всю работу за небольшое окно тайм-аута вебхука, просто добавьте задачу в очередь для отдельной обработки и позвольте вашему middleware завершить работу. +Очередь может использовать столько времени, сколько захочет. +Когда она закончит, она может отправить сообщение обратно в чат. +Это несложно сделать, если вы используете простую очередь в памяти. +Это может быть немного сложнее, если вы используете отказоустойчивую внешнюю систему очередей, которая сохраняет состояние всех задач и может повторить выполнение, даже если ваш сервер внезапно умрет. + +```asciiart:no-line-numbers +______________ _____________ +| | | | +| | --- новое сообщение ---> | | +| | <--- спасибо, чувак ---. | | +| | . | | +| | . | | +| Telegram | *очередь бота работает* . | Бот | +| | . | | +| | . | | +| | <--- результат очереди --- | | +| | --- отличненько ---> | | +|____________| |___________| +``` + +#### Почему `"return"` в целом хуже, чем `"throw"` + +Вам может быть интересно, почему по умолчанию `webhookCallback` выбрасывает ошибку, а не завершает запрос успешно. +Такой выбор был сделан по следующим причинам. + +Условия гонки очень трудно воспроизвести, и они могут возникать крайне редко или периодически. +Решение этой проблемы заключается в том, чтобы _убедиться, что тайм-ауты не возникают_ в первую очередь. +Но если вы столкнетесь с ними, вы должны знать, что это происходит, чтобы вы могли исследовать и устранить проблему! +По этой причине вы хотите, чтобы ошибка появлялась в ваших логах. +Установка обработчика тайм-аута в `"return"`, то есть подавление тайм-аута и притворство, что ничего не произошло, прямо противоположно полезному поведению. + +Если вы сделаете это, то в некотором смысле будете использовать очередь обновлений в вебхуке доставки Telegram в качестве очереди задач. +Это плохая идея по всем причинам, описанным выше. +То, что grammY _может_ упускать ошибки, из-за которых вы можете потерять данные, не означает, что вы должны _указывать_ ему это делать. +Этот параметр конфигурации не следует использовать в случаях, когда вашему middleware просто требуется слишком много времени для завершения работы. +Потратьте время на то, чтобы правильно исправить эту проблему, и ваши будущие я (и пользователи) будут вам благодарны. diff --git a/site/docs/ru/guide/errors.md b/site/docs/ru/guide/errors.md new file mode 100644 index 000000000..1ce5215b4 --- /dev/null +++ b/site/docs/ru/guide/errors.md @@ -0,0 +1,169 @@ +# Обработка ошибок + +Каждая ошибка, вызванная вашим middleware, будет поймана grammY. +Для обработки ошибок следует установить собственный обработчик ошибок. + +Самое главное, что этот раздел научит вас [как ловить ошибки](#отлов-ошибок), которые могут быть получены. + +После этого мы рассмотрим все три типа ошибок, с которыми может столкнуться ваш бот. + +| Название | Значение | +| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | +| [`BotError`](#объект-boterror) | Объект Error, который оборачивает любую ошибку, возникающую в вашем middleware (например, две ошибки ниже). | +| [`GrammyError`](#объект-grammyerror) | Выбрасывается, если API сервер возвращает `ok: false`, указывая на то, что ваш запрос был недействительным и не прошел | +| [`HttpError`](#объект-httperror) | Выбрасывается, если не удалось достичь API сервера | + +Более продвинутый механизм обработки ошибок можно найти [внизу](#границы-ошибок). + +## Отлов ошибок + +То, как вы будете отлавливать ошибки, зависит от ваших настроек. + +### Long Polling + +Если вы запускаете бота через `bot.start()` или используете [grammY runner](../plugins/runner), то вам следует **установить обработчик ошибок через `bot.catch`.** + +В grammY по умолчанию установлен обработчик ошибок, который останавливает бота, если он был запущен с помощью `bot.start()`. +Затем он перебрасывает ошибку. +Что произойдет дальше, зависит от платформы. +Поэтому **вам следует установить обработчик ошибок через `bot.catch`.** + +Например: + +```ts +bot.catch((err) => { + const ctx = err.ctx; + console.error(`Ошибка при обработке обновления ${ctx.update.update_id}:`); + const e = err.error; + if (e instanceof GrammyError) { + console.error("Ошибка в запросе:", e.description); + } else if (e instanceof HttpError) { + console.error("Не удалось связаться с Telegram:", e); + } else { + console.error("Неизвестная ошибка:", e); + } +}); +``` + +### Вебхуки + +Если вы запускаете бота через вебхуки, grammY передаст ошибку в используемый вами веб-фреймворк, например, `express`. +Вы должны обрабатывать ошибки в соответствии с соглашениями этого фреймворка. + +## Объект `BotError` + +Объект `BotError` связывает полученную ошибку с соответствующим [объектом контекста](./context), который вызвал ее появление. +Это работает следующим образом. + +Какая бы ошибка ни возникла при обработке обновления, grammY перехватит ее для вас. +Часто бывает полезно получить доступ к объекту контекста, который вызвал ошибку. + +grammY никак не касается полученной ошибки, а вместо этого заворачивает ее в экземпляр `BotError`. +Учитывая, что этот объект имеет имя `err`, вы можете получить доступ к исходной ошибке через `err.error`. +К соответствующему объекту контекста можно обратиться через `err.ctx`. + +Посмотрите класс `BotError` в [документации grammY API](/ref/core/boterror). + +## Объект `GrammyError` + +Если метод API, например `sendMessage`, не сработает, grammY выбросит `GrammyError`. +Обратите внимание, что также экземпляры `GrammyError` будут обернуты в объекты `BotError`, если они будут выброшены в middleware. + +Полученный `GrammyError` указывает на то, что соответствующий API-запрос завершился неудачей. +Ошибка предоставляет доступ к коду ошибки, возвращаемому бэкендом Telegram, а также к ее описанию. + +Ознакомьтесь с классом `GrammyError` в [документации grammY API](/ref/core/grammyerror). + +## Объект `HttpError` + +При неудачном сетевом запросе возникает ошибка `HttpError`. +Это означает, что grammY не смог связаться с API сервером бота. +Объект ошибки содержит информацию о том, почему запрос не удался, которая доступна в свойстве `error`. + +Вы редко увидите такую ошибку, если только ваша сетевая инфраструктура нестабильна или API сервер вашего бота временно не работает. + +> Обратите внимание, что если с сервером Bot API можно связаться, но он возвращает `ok: false` для данного вызова метода, вместо него будет выброшен [`GrammyError`](./errors#объект-grammyerror). + +Посмотрите класс `HttpError` в [документации grammY API](/ref/core/httperror). + +## Границы ошибок + +> Это продвинутая тема, которая в основном полезна для больших ботов. +> Если вы относительно недавно знакомы с grammY, просто пропустите остаток этого раздела. + +Если вы разделили свою кодовую базу на разные части, то _границы ошибок_ позволят вам установить разные обработчики ошибок для разных частей middleware. +Они достигают этого, позволяя вам оградить ошибки в части вашего middleware. +Другими словами, если ошибка возникла в специально защищенной части middleware, она не сможет выйти за пределы этой части системы middleware. +Вместо этого будет вызван специальный обработчик ошибок, а окруженная часть middleware сделает вид, что успешно завершила работу. +Это особенность системы middleware grammY, поэтому границы ошибок не зависят от того, работаете ли вы с ботом с помощью вебхуков или long polling. + +Как вариант, вы можете вместо этого позволить middleware выполнить _возобновление_ после обработки ошибки, продолжая работу прямо за границей ошибки. +В этом случае огражденный middleware не только ведет себя так, как будто оно успешно завершилось, но и передает поток управления следующему middleware, которое было установлено после границы ошибки. +Таким образом, создается впечатление, что middleware, находящийся внутри границы ошибки, вызвал `next`. + +```ts +const bot = new Bot(""); + +bot.use(/* A */); +bot.use(/* B */); + +const composer = new Composer(); +composer.use(/* X */); +composer.use(/* Y */); +composer.use(/* Z */); +bot.errorBoundary(boundaryHandler /* , Q */).use(composer); + +bot.use(/* C */); +bot.use(/* D */); + +bot.catch(errorHandler); + +function boundaryHandler(err: BotError, next: NextFunction) { + console.error("Ошибка в Q, X, Y, или Z!", err); + /* + * Вы можете вызвать `next`, если хотите запустить + * middleware на C в случае ошибки: + */ + // await next() +} + +function errorHandler(err: BotError) { + console.error("Ошибка в A, B, C, или D!", err); +} +``` + +В приведенном выше примере `boundaryHandler` будет вызван для + +1. всех middleware, которые передаются в `bot.errorBoundary` после `boundaryHandler` (т.е. `Q`), и +2. всех middleware, установленных на последующих экземплярах composer (т.е. `X`, `Y` и `Z`). + +> Что касается пункта 2, вы можете перейти к [расширенному объяснению](../advanced/middleware) middleware, чтобы узнать, как работает цепочка в grammY. + +Вы также можете применить границу ошибки к Composer без вызова `bot.errorBoundary`: + +```ts +const composer = new Composer(); + +const protected = composer.errorBoundary(boundaryHandler); +protected.use(/* B */); + +bot.use(composer); +bot.use(/* C */); + +bot.catch(errorHandler); + +function boundaryHandler(err: BotError, next: NextFunction) { + console.error("Ошибка в B!", err); +} + +function errorHandler(err: BotError) { + console.error("Ошибка в C!", err); +} +``` + +Обработчик `boundaryHandler` из приведенного выше примера будет вызван для middleware, привязанных к `protected`. + +Если вы хотите, чтобы ошибка пересекала границу (то есть передавалась за ее пределы), вы можете повторно вызвать ошибку внутри своего обработчика ошибок. +Тогда ошибка будет передана на следующую окружающую границу. + +В некотором смысле, вы можете рассматривать обработчик ошибок, установленный через `bot.catch`, как самую внешнюю границу ошибки. diff --git a/site/docs/ru/guide/files.md b/site/docs/ru/guide/files.md new file mode 100644 index 000000000..e63a9d859 --- /dev/null +++ b/site/docs/ru/guide/files.md @@ -0,0 +1,260 @@ +# Обработка файлов + +Боты Telegram могут не только отправлять и получать текстовые сообщения, но и многие другие виды сообщений, например, фото и видео. +Это предполагает работу с файлами, которые прикрепляются к сообщениям. + +## Как работают файлы для ботов Telegram + +> Этот раздел объясняет, как работают файлы для ботов Telegram. +> Если вы хотите узнать, как можно работать с файлами в grammY, прокрутите страницу вниз, чтобы узнать о [скачивании](#получение-фаилов) и [загрузке](#отправка-фаилов) файлов. + +Файлы хранятся отдельно от сообщений. +Файл на серверах Telegram идентифицируется по `file_id`, который представляет собой длинную строку символов. +Например, она может выглядеть как `AgADBAADZRAxGyhM3FKSE4qKa-RODckQHxsoABDHe0BDC1GzpGACAAEC`. + +### Идентификаторы для полученных файлов + +> Боты получают только идентификаторы файлов. +> Если они хотят получить содержимое файла, они должны запросить его напрямую. + +Когда ваш бот **получает** сообщение с файлом, на самом деле он получает не все данные файла, а только `file_id`. +Если ваш бот действительно хочет загрузить файл, то он может сделать это, вызвав метод `getFile` ([документация Telegram Bot API](https://core.telegram.org/bots/api#getfile)). +Этот метод позволяет загрузить файл, сконструировав специальный временный URL. +Обратите внимание, что этот URL гарантированно будет действителен только в течение 60 минут, после чего его срок действия может истечь. В этом случае вы можете просто вызвать `getFile` снова. + +Файлы могут быть получены вот [так](#получение-фаилов). + +### Идентификаторы для отправленных файлов + +> Отправляя файлы, вы также получаете идентификатор файла. + +Всякий раз, когда ваш бот **отправляет** сообщение с файлом, он получает информацию об отправленном сообщении, включая `file_id` отправленного файла. +Это означает, что все файлы, которые видит бот, как при отправке, так и при получении, будут иметь `file_id`. +Если вы хотите работать с файлом после того, как ваш бот его увидит, вы всегда должны хранить его `file_id`. + +> Используйте идентификаторы файлов всегда, когда это возможно. +> Они очень эффективны. + +Когда бот отправляет сообщение, он может **указать `file_id`, который он уже видел**. +Это позволит ему отправить идентифицированный файл, без необходимости загружать данные для него. + +Вы можете использовать один и тот же `file_id` сколько угодно раз, так что вы можете отправить один и тот же файл в пять разных чатов, используя один и тот же `file_id`. +Однако вы должны убедиться, что используете правильный метод - например, вы не можете использовать `file_id`, который идентифицирует фотографию, при вызове [`sendVideo`](https://core.telegram.org/bots/api#sendvideo). + +Файлы могут быть отправлены вот [так](#отправка-фаилов). + +### Идентификаторы которые могут вас удивить + +> Идентификаторы файлов **работают только для вашего бота**. +> Если другой бот использует ваши идентификаторы файлов, он может случайно сработать, случайно разбиться и случайно убить невинных котят. +> :cat: → :skull: + +Каждый бот имеет свой собственный набор `file_id` для файлов, к которым он может получить доступ. +Вы не сможете надежно использовать `file_id` от бота вашего друга для доступа к файлу с помощью _вашего_ бота. +Каждый бот будет использовать разные идентификаторы для одного и того же файла. +Это означает, что вы не можете просто угадать `file_id` и получить доступ к файлу какого-то случайного человека, потому что Telegram отслеживает, какие `file_id` действительны для вашего бота. + +::: warning Использование других `file_id` +Обратите внимание, что в некоторых случаях технически возможно, что `file_id` от другого бота работает корректно. +**Однако**, использование чужих `file_id` опасно, так как они могут перестать работать в любой момент без предупреждения. +Поэтому всегда убеждайтесь, что все `file_id`, которые вы используете, были изначально предназначены для вашего бота. +::: + +> У файла может быть несколько идентификаторов. + +С другой стороны, возможно, что бот в конечном итоге видит один и тот же файл, идентифицированный разными `file_id`. +Это означает, что вы не можете полагаться на сравнение `file_id` для проверки того, являются ли два файла одним и тем же. +Если вам нужно идентифицировать один и тот же файл в течение долгого времени (или для нескольких ботов), вы должны использовать значение `file_unique_id`, которое ваш бот получает вместе с каждым `file_id`. + +Значение `file_unique_id` не может быть использовано для загрузки файлов, но оно будет одинаковым для любого файла у всех ботов. + +## Получение файлов + +Вы можете работать с файлами так же, как и с любыми другими сообщениями. +Например, если вы хотите прослушать голосовые сообщения, вы можете сделать следующее: + +```ts +bot.on("message:voice", async (ctx) => { + const voice = ctx.msg.voice; + + const duration = voice.duration; // в секундах + await ctx.reply(`Ваше сообщение длиной ${duration} секунд(ы).`); + + const fileId = voice.file_id; + await ctx.reply("Идентификатор файла вашего голосового сообщения: " + fileId); + + const file = await ctx.getFile(); // Действует не менее 1 часа + const path = file.file_path; // путь к файлу на API сервере бота + await ctx.reply("Скачайте свой собственный файл снова: " + path); +}); +``` + +::: tip Передача пользовательского идентификатора файла в getFile +На объекте контекста `getFile` является [краткой записью](./context#краткая-запись), и будет получать информацию о файле текущего сообщения. +Если вы хотите получить другой файл при работе с сообщением, используйте `ctx.api.getFile(file_id)` вместо этого. +::: + +> Посмотрите краткие записи [`:media` и `:file`](./filter-queries#сокращения) для фильтровую запросов, если вы хотите получить любой тип файла. + +Вызвав `getFile`, вы можете использовать возвращаемый `file_path` для загрузки файла по этому URL `https://api.telegram.org/file/bot<токен>/`, где `<токен>` должен быть заменен на ваш токен бота. + +Если вы [запускаете свой собственный API сервер](./api#запуск-локального-api-сервера-бота), то `file_path` будет представлять собой абсолютный путь к файлу, который указывает на файл на вашем локальном диске. +В этом случае вам больше не нужно ничего скачивать, так как сервер Bot API скачает файл за вас при вызове `getFile`. + +::: tip Плагин Files +grammY не поставляется в комплекте с собственным загрузчиком файлов, но вы можете установить [официальный плагин files](../plugins/files). +Это позволит вам загружать файлы через `await file.download()`, а также получать URL для их загрузки через `file.getUrl()`. +::: + +## Отправка файлов + +У ботов Telegram есть [три способа](https://core.telegram.org/bots/api#sending-files) отправки файлов: + +1. Через `file_id`, то есть отправляя файл по идентификатору, который уже известен боту. +2. Через URL, т.е. передав публичный URL файла, который Telegram скачает и отправит за вас. +3. Через загрузку собственного файла. + +Во всех случаях методы, которые вам нужно вызвать, называются одинаково. +В зависимости от того, какой из трех способов отправки файла вы выберете, параметры этих функций будут отличаться. +Например, чтобы отправить фотографию, вы можете использовать `ctx.replyWithPhoto` (или `sendPhoto`, если вы используете `ctx.api` или `bot.api`). + +Вы можете отправлять файлы других типов, просто переименовав метод и изменив тип передаваемых в него данных. +Чтобы отправить видео, вы можете использовать `ctx.replyWithVideo`. +То же самое касается и документа: `ctx.replyWithDocument`. +Вы поняли суть. + +Давайте разберемся, что представляют собой эти три способа отправки файла. + +### Через `file_id` или URL + +Первые два метода просты: вы просто передаете соответствующее значение в виде `строки`, и все готово. + +```ts +// Отправка через file_id. +await ctx.replyWithPhoto(existingFileId); + +// Отправка через URL. +await ctx.replyWithPhoto("https://grammy.dev/images/grammY.png"); + +// В качестве альтернативы можно использовать bot.api.sendPhoto() или ctx.api.sendPhoto(). +``` + +### Загрузка ваших собственных файлов + +В grammY есть хорошая поддержка загрузки собственных файлов. +Для этого нужно импортировать и использовать класс `InputFile` ([документация grammY API](/ref/core/inputfile)). + +```ts +// Отправка файла по локальному пути +await ctx.replyWithPhoto(new InputFile("/tmp/picture.jpg")); + +// В качестве альтернативы используйте bot.api.sendPhoto() или ctx.api.sendPhoto() +``` + +Конструктор `InputFile` принимает не только пути к файлам, но и потоки, объекты `Buffer`, асинхронные итераторы и, в зависимости от вашей платформы - многое другое, или функцию, которая создает любую из этих вещей. +Все, что вам нужно помнить, это: **создайте экземпляр `InputFile` и передайте его в любой метод для отправки файла**. +Экземпляры `InputFile` можно передавать всем методам, которые принимают отправку файлов путем загрузки. + +Вот несколько примеров того, как можно создать `InputFile`. + +#### Загрузка файлов с диска + +Если у вас уже есть файл, хранящийся на вашем компьютере, вы можете позволить grammY загрузить этот файл. + +::: code-group + +```ts [Node.js] +import { createReadStream } from "fs"; + +// Отправка локального файла. +new InputFile("/path/to/file"); + +// Отправка потоком. +new InputFile(createReadStream("/path/to/file")); +``` + +```ts [Deno] +// Отправка локального файла. +new InputFile("/path/to/file"); + +// Отправка из экземпляра `Deno.FsFile`. +new InputFile(await Deno.open("/path/to/file")); +``` + +::: + +#### Загрузка сырых бинарных данных + +Вы также можете отправить объект `Buffer` или итератор, который выдает объекты `Buffer`. +На Deno вы также можете отправлять объекты `Blob`. + +::: code-group + +```ts [Node.js] +// Отправьте буфер или массив байтов. +const buffer = Uint8Array.from([65, 66, 67]); +new InputFile(buffer); // "ABC" +// Отправьте итерируемый объект. +new InputFile(function* () { + // "ABCABCABCABC" + for (let i = 0; i < 4; i++) yield buffer; +}); +``` + +```ts [Deno] +// Send a blob. +const blob = new Blob("ABC", { type: "text/plain" }); +new InputFile(blob); +// Отправьте буфер или массив байтов. +const buffer = Uint8Array.from([65, 66, 67]); +new InputFile(buffer); // "ABC" +// Отправьте итерируемый объект. +new InputFile(function* () { + // "ABCABCABCABC" + for (let i = 0; i < 4; i++) yield buffer; +}); +``` + +::: + +#### Загрузка и повторая отправка файлов + +Вы даже можете заставить grammY загрузить файл из Интернета. +При этом файл не будет сохранен на диске. +Вместо этого grammY будет только передавать данные, сохраняя в памяти лишь небольшой фрагмент. +Это очень эффективно. + +> Обратите внимание, что Telegram поддерживает загрузку файла многими способами. +> Если возможно, вы должны предпочесть [отправить файл по URL](#через-file-id-или-url), а не использовать `InputFile` для передачи содержимого файла через ваш сервер. + +```ts +// Загрузите файл и передайте ответ в Telegram. +new InputFile(new URL("https://grammy.dev/images/grammY.png")); +new InputFile({ url: "https://grammy.dev/images/grammY.png" }); // равносильно +``` + +### Добавление подписи + +При отправке файлов вы можете указать дополнительные параметры в объекте options типа `Other`, точно так же, как объяснялось [ранее](./basics#отправка-сообщении). +Например, это позволяет отправлять подписи. + +```ts +// Отправьте фотографию из локального файла пользователю с ID 12345 с подписью "photo.jpg". +await bot.api.sendPhoto(12345, new InputFile("/path/to/photo.jpg"), { + caption: "photo.jpg", +}); +``` + +Как всегда, как и все остальные методы API, вы можете отправлять файлы через `ctx` (самый простой), `ctx.api` или `bot.api`. + +## Лимиты размеров файла + +Сам grammY может отправлять файлы без ограничений по размеру, однако Telegram ограничивает размер файлов, как описано [здесь](https://core.telegram.org/bots/api#sending-files). +Это означает, что ваш бот не сможет скачивать файлы размером более 20 МБ или загружать файлы размером более 50 МБ. +Некоторые комбинации имеют еще более строгие ограничения, например, фотографии, отправленные по URL (5 МБ). + +Как уже упоминалось в [предыдущем разделе](./api), ваш бот может работать с большими файлами, приложив некоторые дополнительные усилия. +Если вы хотите поддерживать загрузку файлов размером до 2000 МБ (максимальный размер файла в Telegram) и скачивание файлов любого размера ([4000 МБ с Telegram Премиум](https://t.me/premium/5)), вам необходимо [разместить собственный API сервер](./api#запуск-локального-api-сервера-бота) в дополнение к хостингу вашего бота. + +Хостинг собственного API сервера бота сам по себе не имеет никакого отношения к grammY. +Однако grammY поддерживает все методы, необходимые для настройки вашего бота на использование собственного API сервера. diff --git a/site/docs/ru/guide/filter-queries.md b/site/docs/ru/guide/filter-queries.md new file mode 100644 index 000000000..6f0136776 --- /dev/null +++ b/site/docs/ru/guide/filter-queries.md @@ -0,0 +1,370 @@ +# Фильтрующие запросы и `bot.on()` + +Первым аргументом `bot.on()` является строка _filter query_. + +## Введение + +Большинство (все?) других фреймворков позволяют выполнять примитивную форму фильтрации обновлений, например, только `on("message")` и тому подобное. +Остальная фильтрация сообщений остается на усмотрение разработчика, что часто приводит к бесконечным `if` утверждениям в коде. + +Напротив, **grammY поставляется с собственным языком запросов**, который вы можете использовать для **фильтрации именно тех сообщений**, которые вам нужны. + +Это позволяет использовать более 1150 различных фильтров, и мы можем добавить еще больше со временем. +Каждый правильный фильтр может быть автоматически заполнен в вашем редакторе кода. +Таким образом, вы можете просто набрать `bot.on("")`, открыть автоподсказки и выполнить поиск по всем запросам, набрав что-нибудь. + +![Поиск по фильтрам](/images/filter-query-search.png) + +Вывод типов в `bot.on()` будет понимать выбранный вами запрос фильтра. +Поэтому он подтягивает к контексту несколько типов, о которых известно. + +```ts +bot.on("message", async (ctx) => { + // Может быть undefined, если полученное сообщение не содержит текста. + const text: string | undefined = ctx.msg.text; +}); +bot.on("message:text", async (ctx) => { + // Текст всегда присутствует, потому что этот обработчик вызывается при получении текстового сообщения. + const text: string = ctx.msg.text; +}); +``` + +В некотором смысле, grammY реализует запросы фильтра как [во время выполнения](#производительность), так и [на уровне типов](#безопасность-типов). + +## Примеры запросов + +Вот несколько примеров запросов: + +### Регулярные запросы + +Простые фильтры для обновлений и подфильтры: + +```ts +bot.on("message"); // вызывается при получении любого сообщения +bot.on("message:text"); // только текстовых сообщений +bot.on("message:photo"); // только сообщений содержащих фотографии +``` + +### Фильтры для сущностей + +Подфильтры, которые позволяют углубиться на один уровень: + +```ts +bot.on("message:entities:url"); // сообщения содержащие URL +bot.on("message:entities:code"); // сообщения, содержащие блоки кода +bot.on("edited_message:entities"); // редактированные сообщения с любыми сущностями +``` + +### Пропущенные фильтры + +Вы можете опустить некоторые значения в фильтрующих запросах. +Тогда grammY будет перебирать различные значения, чтобы соответствовать вашему запросу. + +```ts +bot.on(":text"); // любые текстовые сообщения и любые текстовые сообщения каналов +bot.on("message::url"); // сообщения с URL адресом в тексте или подписи (фотографии и т.д.) +bot.on("::email"); // сообщения или посты в канале с электронной почтой в тексте или подписи +``` + +Если опустить _первое_ значение, то совпадут и сообщения, и посты в канале. +[Помните](./context#доступные-деиствия), что `ctx.msg` дает доступ как к сообщениям, так и к постам в канале, в зависимости от того, что соответствует запросу. + +Опуская _второе_ значение, можно подобрать как сущности, так и подпись. +Вы можете опустить и первую, и вторую часть одновременно. + +### Сокращения + +Механизм запросов grammY позволяет задавать аккуратные сокращения, которые группируют связанные запросы вместе. + +#### `msg` + +Сокращенная запись `msg` группирует новые сообщения и новые посты в канале. +Другими словами, использование `msg` всё равно, что прослушивание событий `"message"` и `"channel_post"`. + +```ts +bot.on("msg"); // любое сообщение или сообщение канала +bot.on("msg:text"); // точно так же, как `:text`. +``` + +#### `edit` + +Сокращённая запись `edit` группирует отредактированные сообщения и отредактированные посты канала. +Другими словами, использование `edit` эквивалентно прослушиванию событий `"edited_message"` и `"edited_channel_post"`. + +```ts +bot.on("edit"); // редактирование любого сообщения или сообщения канала +bot.on("edit:text"); // редактирование текстовых сообщений +bot.on("edit::url"); // редактирование сообщений с URL в тексте или подписи +bot.on("edit:location"); // обновленное местоположение +``` + +#### `:media` + +Сокращённая запись `:media` группирует фото и видео сообщения. +Другими словами, использование `:media` эквивалентно прослушиванию событий `":photo"` и `":video"`. + +```ts +bot.on("message:media"); // фото и видео сообщения +bot.on("edited_channel_post:media"); // редактирование сообщений канала с помощью медиа +bot.on(":media"); // медиа сообщения или посты в канале +``` + +#### `:file` + +Сокращённая запись `:file` группирует все сообщения в которых есть файлы. +Другими словами, использование `:file` эквивалентно прослушиванию событий `":photo"`, `":animation"`, `":audio"`, `":document"`, `":video"`, `":video_note"`, `":voice"`, и `":sticker"`. +Следовательно, вы можете быть уверены, что `await ctx.getFile()` передаст вам объект файла. + +```ts +bot.on(":file"); // файлы в сообщениях или сообщениях канала +bot.on("edit:file"); // редактирование сообщений с файлом или постах с файлом в канале +``` + +### Синтаксический сахар + +Есть два особых случая для частей запроса, которые делают фильтрацию для пользователей более удобной. +Вы можете обнаружить ботов в запросах с помощью части запроса `:is_bot`. +Синтаксический сахар `:me` можно использовать для ссылки на вашего бота внутри запроса, который будет сравнивать идентификаторы пользователей за вас. + +```ts +// Системное сообщение о боте, который присоединился к чату +bot.on("message:new_chat_members:is_bot"); +// Системное сообщение о том, что ваш бот удален +bot.on("message:left_chat_member:me"); +``` + +Обратите внимание, что хотя этот синтаксический сахар полезен для работы со служебными сообщениями, его не следует использовать для определения того, присоединяется ли кто-то к чату или покидает его. +Служебные сообщения --- это сообщения, которые информируют пользователей в чате, и некоторые из них будут видны не во всех случаях. +Например, в больших группах не будет никаких служебных сообщений о пользователях, которые присоединяются или покидают чат. +Следовательно, ваш бот может не заметить этого. +Вместо этого вы должны прослушивать [обновления участников чата](#обновления-участников-чата). + +## Комбинирование нескольких запросов + +Вы можете комбинировать любое количество фильтрующих запросов с помощью логических операций И и ИЛИ. + +### Сочетание с логическим ИЛИ + +Если вы хотите установить какую-то промежуточную программу за конкатенацией двух запросов ИЛИ, вы можете передать их оба в `bot.on()` в виде массива. + +```ts +// Выполняется, если обновление касается сообщения ИЛИ редактирования сообщения +bot.on(["message", "edited_message"] /* , ... */); +// Выполняется, если в тексте или подписи найден хэштег ИЛИ электронная почта ИЛИ упоминание +bot.on(["::hashtag", "::email", "::mention"] /* , ... */); +``` + +Middleware будет выполнен, если _любой из предоставленных запросов_ совпадет. +Порядок запросов не имеет значения. + +### Сочетание с логическим И + +Если вы хотите установить какую-то промежуточную программу за конкатенацией И двух запросов, вы можете составить цепочку вызовов `bot.on()`. + +```ts +// Поиск пересланных URL-адресов +bot.on("::url").on(":forward_origin" /* , ... */); +// Сопоставляет фотографии, содержащие хэштег в подписи +bot.on(":photo").on("::hashtag" /* , ... */); +``` + +Middleware будет выполнен, если _все предоставленные запросы_ совпадают. +Порядок запросов не имеет значения. + +### Построение сложных запросов + +Технически возможно объединять запросы фильтров в более сложные формулы, если они находятся в [CNF](https://en.wikipedia.org/wiki/Conjunctive_normal_form), хотя это вряд ли будет полезно. + +```ts +bot + // Сопоставляет все сообщения канала или пересланные сообщения ... + .on(["channel_post", ":forward_origin"]) + // ... которые содержат текст ... + .on(":text") + // ... с хотя бы с одним URL, хэштегом или кештегом. + .on(["::url", "::hashtag", "::cashtag"] /* , ... */); +``` + +Вывод типа `ctx` просканирует всю цепочку вызовов и проверит каждый элемент всех трех вызовов `.on`. +Например, он может определить, что `ctx.msg.text` является необходимым свойством для приведенного выше фрагмента кода. + +## Полезные советы + +Вот несколько менее известных возможностей фильтрующих запросов, которые могут пригодиться. +Некоторые из них немного продвинутые, поэтому не стесняйтесь переходить к [следующему разделу](./commands). + +### Обновления участников чата + +Вы можете использовать следующий запрос фильтра для получения обновлений состояния вашего бота. + +```ts +bot.on("my_chat_member"); // заблокировал, разблокировал, зашёл или вышел +``` + +В личных чатах это срабатывает, когда бот блокируется или разблокируется. +В группах это срабатывает, когда бот добавляется или удаляется. +Теперь вы можете проверить `ctx.myChatMember`, чтобы понять, что именно произошло. + +Не следует путать с + +```ts +bot.on("chat_member"); +``` + +который можно использовать для обнаружения изменений статуса других участников чата, например, когда люди присоединяются, получают повышение и так далее. + +> Обратите внимание, что обновления `chat_member` должны быть включены явно, указав `allowed_updates` при запуске вашего бота. + +### Комбинирование запросов с другими методами + +Вы можете комбинировать фильтрующие запросы с другими методами класса `Composer` ([документация API](/ref/core/composer)), такими как `command` или `filter`. +Это позволяет создавать мощные шаблоны обработки сообщений. + +```ts +bot.on(":forward_origin").command("help"); // пересланная команда /help + +// Отвечайте на команды только в личных чатах +const pm = bot.chatType("private"); +pm.command("start"); +pm.command("help"); +``` + +### Фильтрация по типу отправителя сообщения + +В Telegram существует пять различных типов авторов сообщений: + +1. Авторы постов в канале +2. Автоматические переадресации из связанных каналов в комментариях группы +3. Обычные аккаунты пользователей, включая ботов (т.е. "обычные" сообщения) +4. Администраторы, отправляющие сообщения от имени группы ([анонимные администраторы](https://telegram.org/blog/filters-anonymous-admins-comments#anonymous-group-admins)) +5. Пользователи, отправляющие сообщения в качестве одного из своих каналов. + +Вы можете комбинировать запросы фильтров с другими механизмами обработки обновлений, чтобы узнать тип автора сообщения. + +```ts +// Сообщения канала, отправленные `ctx.senderChat`. +bot.on("channel_post"); + +// Автоматическая пересылка из канала `ctx.senderChat`: +bot.on("message:is_automatic_forward"); +// Регулярные сообщения, отправляемые `ctx.from`. +bot.on("message").filter((ctx) => ctx.senderChat === undefined); +// Анонимный админ в `ctx.chat` +bot.on("message").filter((ctx) => ctx.senderChat?.id === ctx.chat.id); +// Пользователи, отправляющие сообщения от имени своего канала `ctx.senderChat`. +bot.on("message").filter((ctx) => + ctx.senderChat !== undefined && ctx.senderChat.id !== ctx.chat.id +); +``` + +### Фильтрация по статусу пользователя + +Если вы хотите отфильтровать по другим свойствам пользователя, вам нужно выполнить дополнительный запрос, например, `await ctx.getAuthor()` для автора сообщения. +Фильтрующие запросы не будут тайно выполнять за вас дополнительные запросы к API. +Тем не менее, выполнить такую фильтрацию довольно просто: + +```ts +bot.on("message").filter( + async (ctx) => { + const user = await ctx.getAuthor(); + return user.status === "creator" || user.status === "administrator"; + }, + (ctx) => { + // Обрабатывает сообщения от создателей и админов. + }, +); +``` + +### Повторное использование логики фильтрующих запросов + +Внутри `bot.on` полагается на функцию под названием `matchFilter`. +Она принимает запрос фильтра и компилирует его в функцию-предикат. +Предикат просто передается в `bot.filter` для фильтрации обновлений. + +Вы можете импортировать `matchFilter` напрямую, если хотите использовать его в своей собственной логике. +Например, вы можете решить пропускать все обновления, которые соответствуют определенному запросу: + +```ts +// Пропускайте все текстовые сообщений или посты в текстовых каналах. +bot.drop(matchFilter(":text")); +``` + +Аналогичным образом можно использовать типы фильтрующих запросов, которые grammY использует внутренне: + +### Повторное использование типов фильтрующих запросов + +Внутри `matchFilter` использует [сужение типов](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) TypeScript, чтобы сузить тип `ctx`. +Он берет тип `C extends Context` и `Q extends FilterQuery` и выдает `ctx is Filter`. +Другими словами, тип `Filter` --- это то, что вы фактически получаете для вашего `ctx` в middleware. + +Вы можете импортировать `Filter` напрямую, если хотите использовать его в своей собственной логике. +Например, вы можете определить функцию-обработчик, которая будет обрабатывать определенные объекты контекста, отфильтрованные с помощью запроса фильтра: + +```ts +function handler(ctx: Filter) { + // обработка суженного объекта контекста +} + +bot.on(":text", handler); +``` + +> Посмотрите ссылки на API для [`matchFilter`](/ref/core/matchfilter), [`Filter`](/ref/core/filter) и [`FilterQuery`](/ref/core/filterquery), чтобы прочитать дальше. + +## Язык запросов + +> Этот раздел предназначен для пользователей, которые хотят получить более глубокое представление о фильтрующих запросах в grammY, но он не содержит никаких знаний, необходимых для создания бота. + +### Структура запросов + +Каждый запрос состоит не более чем из трех частей запроса. +В зависимости от количества частей запроса мы различаем запросы L1, L2 и L3, такие как `"message"`, `"message:entities"` и `"message:entities:url"` соответственно. + +Части запроса разделяются двоеточиями (`:`). +Часть до первого двоеточия или конца строки запроса мы называем _L1-частью_ запроса. +Часть от первого двоеточия до второго двоеточия или до конца строки запроса мы называем _L2 частью_ запроса. +Часть от второго двоеточия до конца строки запроса мы называем _L3 частью_ запроса. + +Например: + +| Фильтрующий запрос | Часть L1 | Часть L2 | Часть L3 | +| ---------------------------- | ----------- | ------------ | ----------- | +| `"message"` | `"message"` | `undefined` | `undefined` | +| `"message:entities"` | `"message"` | `"entities"` | `undefined` | +| `"message:entities:mention"` | `"message"` | `"entities"` | `"mention"` | + +### Валидация запросов + +Несмотря на то, что система типов должна отлавливать все некорректные запросы фильтров во время компиляции, grammY также проверяет все переданные запросы фильтров во время выполнения во время установки. +Каждый переданный запрос фильтра сопоставляется со структурой проверки, которая проверяет, является ли он корректным. +Хорошо не только то, что ошибки в TypeScript приводят к серьезным проблемам со сложной системой вывода типов, которая обеспечивает работу запросов фильтра. +Если такое повторится в будущем, это позволит предотвратить проблемы, которые могли бы возникнуть в противном случае. +В этом случае вам будут выдаваться полезные сообщения об ошибках. + +### Производительность + +**grammY может проверять каждый запрос фильтра за (амортизированное) постоянное время на одно обновление**, независимо от структуры запроса или входящего обновления. + +Проверка фильтрующих запросов происходит только один раз, когда бот инициализируется и вызывается `bot.on()`. + +При запуске grammY извлекает предикатную функцию из фильтрующего запроса, разбивая его на части запроса. +Каждая часть будет сопоставлена с функцией, выполняющей одну проверку истинности свойства объекта, или две проверки, если часть опущена и необходимо проверить два значения. +Затем эти функции объединяются в предикат, который проверяет только столько значений, сколько необходимо для запроса, без итерации по ключам объекта `Update`. + +Эта система использует меньше операций, чем некоторые конкурирующие библиотеки, которым при маршрутизации обновлений необходимо выполнять проверку содержимого в массивах. +Система фильтрующих запросов grammY работает быстрее, несмотря на то, что она гораздо мощнее. + +### Безопасность типов + +Как упоминалось выше, запросы фильтрации автоматически сужают определенные свойства контекстного объекта. +Предикат, полученный из одного или нескольких запросов фильтра, представляет собой предикат типа TypeScript, который выполняет это сужение. +В целом, можно доверять тому, что вывод типов работает корректно. +Если предполагается, что свойство присутствует, вы можете смело полагаться на него. +Если предполагается, что свойство потенциально отсутствует, это означает, что есть определенные случаи его отсутствия. +Не стоит выполнять приведение типов с помощью оператора `!`. + +> Для вас может быть неочевидно, что это за случаи. +> Не стесняйтесь спрашивать в [групповом чате](https://t.me/grammyjs), если не можете разобраться. + +Вычисление этих типов - сложная задача. +В эту часть grammY вошло много знаний о API бота. +Если вы хотите больше узнать об основных подходах к вычислению этих типов, вы можете посмотреть [видео на YouTube](https://youtu.be/ZvT_xexjnMk). diff --git a/site/docs/ru/guide/games.md b/site/docs/ru/guide/games.md new file mode 100644 index 000000000..f55f2bb6c --- /dev/null +++ b/site/docs/ru/guide/games.md @@ -0,0 +1,101 @@ +# Игры + +## Введение + +Игры в Telegram --- это очень интересная функция, с которой очень весело работать. +Что вы можете сделать с ее помощью? +Ответ: все, что угодно. Любую HTML5-игру, которую вы разработали, вы можете предоставить пользователям Telegram с помощью этой функции. +(Да, это означает, что вам придется разработать настоящую игру на основе веб-сайта, которая находится в открытом доступе в интернете, прежде чем вы сможете интегрировать ее в своего Telegram-бота). + +## Настройка игр вашего бота через @BotFather + +Для простоты предположим, что к этому моменту вы уже настроили бота и игру, связанную с вашим ботом, написав [@BotFather](https://t.me/BotFather). +Если вы еще не сделали этого, ознакомьтесь с этой [статьей](https://core.telegram.org/bots/games) от команды Telegram. + +> Примечание: мы будем изучать только разработку бота. +> Разработка игры полностью зависит от разработчика. +> Все, что нам нужно, это ссылка на HTML5-игру, размещенную в интернете. + +## Отправка игр через бота + +Мы можем отправить игру в grammY с помощью метода `replyWithGame`, который принимает в качестве аргумента название игры, созданной вами в BotFather. +В качестве альтернативы можно использовать метод `api.sendGame` (grammY предоставляет все официальные методы [API бота](https://core.telegram.org/bots/api)). +Преимущество использования метода `api.sendGame` в том, что вы можете указать `chat.id` конкретного пользователя для отправки. + +1. Отправка игры через `replyWithGame` + + ```ts + // Мы будем использовать команду start для вызова метода ответа игрой + bot.command("start", async (ctx) => { + // Передайте имя игры, которую вы создали в BotFather, например "my_game". + await ctx.replyWithGame("my_game"); + }); + ``` + +2. Отправка игры через `api.sendGame` + + ```ts + bot.command("start", async (ctx) => { + // Вы можете получить идентификатор чата пользователя, которому нужно отправить игру, с помощью `ctx.from.id`. + // который дает вам идентификатор чата пользователя, вызвавшего команду start. + const chatId = ctx.from.id; + await ctx.api.sendGame(chatid, "my_game"); + }); + ``` + +> [Помните](./basics#отправка-сообщении) что вы можете указать дополнительные параметры при отправке сообщений, используя объект options типа `Other`. + +Вы также можете указать пользовательскую [встроенную клавиатуру](../plugins/keyboard#встроенные-клавиатуры) для отображения кнопок в игре. +По умолчанию будет отправлена кнопка с именем `Play my_game`, где _my_game_ - название вашей игры. + +```ts +// Определите новую встроенную клавиатуру. Вы можете написать любой текст, который будет отображаться +// на кнопках, но убедитесь, что первой кнопкой всегда должна +// быть кнопкая запуска! + +const keyboard = new InlineKeyboard().game("Играть в my_game"); + +// Обратите внимание, что мы использовали game(), в отличие от обычной встроенной клавиатуры. +// где мы используем url() или text() + +// Через метод `replyWithGame`. +await ctx.replyWithGame("my_game", { reply_markup: keyboard }); + +// Через метод `api.sendGame`. +await ctx.api.sendGame(chatId, "my_game", { reply_markup: keyboard }); +``` + +## Прослушивание callback-функции нашей кнопки в игре + +Для обеспечения логики при нажатии кнопки, а также для перенаправления пользователей в нашу игру и многого другого мы слушаем событие `callback_query:game_short_name`, которое сообщает нам, что пользователь нажал игровую кнопку. +Все, что нам нужно сделать, это: + +```ts +// Передайте сюда url вашей игры, которая уже должна быть размещена в Интернете. + +bot.on("callback_query:game_short_name", async (ctx) => { + await ctx.answerCallbackQuery({ url: "ваша_ссылка_на_игру" }); +}); +``` + +--- + +### Наш окончательный код должен выглядеть примерно так + +```ts +bot.on("callback_query:game_short_name", async (ctx) => { + await ctx.answerCallbackQuery({ url: "ваша_ссылка_на_игру" }); +}); + +bot.command("start", async (ctx) => { + await ctx.replyWithGame("my_game", { + reply_markup: keyboard, + // Или вы можете использовать метод api здесь, в зависимости от ваших потребностей. + }); +}); +``` + +> Не забудьте добавить правильную [обработку ошибок](./errors) в вашего бота перед запуском. + +Возможно, в будущем мы расширим эту статью дополнительными разделами и FAQ, но это все, что вам нужно, чтобы начать создание своей игры в Telegram. +Приятной игры! :space_invader: diff --git a/site/docs/ru/guide/getting-started.md b/site/docs/ru/guide/getting-started.md new file mode 100644 index 000000000..7177bf37c --- /dev/null +++ b/site/docs/ru/guide/getting-started.md @@ -0,0 +1,234 @@ +# Начало работы + +Создайте своего первого бота за несколько минут (прокрутите +[вниз](#приступаите-к-работе-с-deno) для руководства по Deno). + +## Приступайте к работе на Node.js + +> Это руководство предполагает, что у вас установлен +> [Node.js](https://nodejs.org), и `npm` должен поставляться вместе с ним. Если +> вы не знаете, что это за вещи, ознакомьтесь с нашим +> [Введением](./introduction)! + +Создайте новый проект TypeScript и установите пакет `grammy`. Для этого откройте +терминал и введите: + +::: code-group + +```sh [npm] +# Создайте новую директорию и перейдите в неё. +mkdir my-bot +cd my-bot + +# Установите Typescript (пропустите если вы используете JavaScript). +npm install -D typescript +npx tsc --init + +# Установите grammY. +npm install grammy +``` + +```sh [Yarn] +# Создайте новую директорию и перейдите в неё. +mkdir my-bot +cd my-bot + +# Установите Typescript (пропустите если вы используете JavaScript). +yarn add typescript -D +npx tsc --init + +# Установите grammY. +yarn add grammy +``` + +```sh [pnpm] +# Создайте новую директорию и перейдите в неё. +mkdir my-bot +cd my-bot + +# Установите Typescript (пропустите если вы используете JavaScript). +pnpm add -D typescript +npx tsc --init + +# Установите grammY. +pnpm add grammy +``` + +::: + +Создайте новый пустой текстовый файл, например, `bot.ts`. Теперь структура ваших +папок должна выглядеть следующим образом: + +```asciiart:no-line-numbers +. +├── bot.ts +├── node_modules/ +├── package.json +├── package-lock.json +└── tsconfig.json +``` + +Теперь пришло время открыть Telegram, чтобы создать аккаунт бота и получить +токен для его работы. Для этого напишите [@BotFather](https://t.me/BotFather). +Токен выглядит как `123456:aBcDeF_gHiJkLmNoP-q`. Он используется для +аутентификации вашего бота. + +Получили токен? Теперь вы можете написать код вашего бота в файле `bot.ts`. Вы +можете скопировать следующий пример бота в этот файл и передать свой токен в +конструктор `Bot`: + +::: code-group + +```ts [TypeScript] +import { Bot } from "grammy"; + +// Создайте экземпляр класса `Bot` и передайте ему токен вашего бота. +const bot = new Bot(""); // <-- поместите токен вашего бота между "". + +// Теперь вы можете зарегистрировать слушателей на объекте вашего бота `bot`. +// grammY будет вызывать слушателей, когда пользователи будут отправлять сообщения вашему боту. + +// Обработайте команду /start. +bot.command( + "start", + (ctx) => ctx.reply("Добро пожаловать. Запущен и работает!"), +); +// Обработайте другие сообщения. +bot.on("message", (ctx) => ctx.reply("Получил другое сообщение!")); + +// Теперь, когда вы указали, как обрабатывать сообщения, вы можете запустить своего бота. +// Он подключится к серверам Telegram и будет ждать сообщений. + +// Запустите бота. +bot.start(); +``` + +```js [JavaScript] +const { Bot } = require("grammy"); + +// Создайте экземпляр класса `Bot` и передайте ему токен вашего бота. +const bot = new Bot(""); // <-- поместите токен вашего бота между "". + +// Теперь вы можете зарегистрировать слушателей на объекте вашего бота `bot`. +// grammY будет вызывать слушателей, когда пользователи будут отправлять сообщения вашему боту. + +// Обработайте команду /start. +bot.command( + "start", + (ctx) => ctx.reply("Добро пожаловать. Запущен и работает!"), +); +// Обработайте другие сообщения. +bot.on("message", (ctx) => ctx.reply("Получил другое сообщение!")); + +// Теперь, когда вы указали, как обрабатывать сообщения, вы можете запустить своего бота. +// Он подключится к серверам Telegram и будет ждать сообщений. + +// Запустите бота. +bot.start(); +``` + +::: + +Скомпилируйте код, выполнив команду + +```sh +npx tsc +``` + +в вашем терминале. В результате будет создан JavaScript файл `bot.js`. + +Теперь вы можете запустить бота, выполнив команду + +```sh +node bot.js +``` + +в вашем терминале. Готово! :tada: + +Перейдите в Telegram, чтобы посмотреть, как ваш бот отвечает на сообщения! + +::: tip Включение логов +Вы можете включить базовое ведение логов, выполнив +команду + +```sh +export DEBUG="grammy*" +``` + +в терминале перед выполнением `node bot.js`. Это облегчает отладку вашего бота. +::: + +## Приступайте к работе с Deno + +> Это руководство предполагает, что у вас установлен [Deno](https://deno.com). + +Создайте новую директорию и создайте в ней новый пустой текстовый файл, +например, с именем `bot.ts`. + +```sh +mkdir ./my-bot +cd ./my-bot +touch bot.ts +``` + +Теперь пришло время открыть Telegram, чтобы создать аккаунт бота и получить +токен для его работы. Для этого напишите [@BotFather](https://t.me/BotFather). +Токен выглядит как `123456:aBcDeF_gHiJkLmNoP-q`. Он используется для +аутентификации вашего бота. + +Получили токен? Теперь вы можете написать код вашего бота в файле `bot.ts`. Вы +можете скопировать следующий пример бота в этот файл и передать свой токен в +конструктор `Bot`: + +```ts +import { Bot } from "https://deno.land/x/grammy/mod.ts"; + +// Создайте экземпляр класса `Bot` и передайте ему токен вашего бота. +const bot = new Bot(""); // <-- поместите токен вашего бота между "". + +// Теперь вы можете зарегистрировать слушателей на объекте вашего бота `bot`. +// grammY будет вызывать слушателей, когда пользователи будут отправлять сообщения вашему боту. + +// Обработайте команду /start. +bot.command( + "start", + (ctx) => ctx.reply("Добро пожаловать. Запущен и работает!"), +); +// Обработайте другие сообщения. +bot.on("message", (ctx) => ctx.reply("Получил другое сообщение!")); + +// Теперь, когда вы указали, как обрабатывать сообщения, вы можете запустить своего бота. +// Он подключится к серверам Telegram и будет ждать сообщений. + +// Запустите бота. +bot.start(); +``` + +Теперь вы можете запустить бота, выполнив команду + +```sh +deno run --allow-net bot.ts +``` + +в вашем терминале. Готово! :tada: + +Перейдите в Telegram, чтобы посмотреть, как ваш бот отвечает на сообщения! + +::: tip Включение логов +Вы можете включить базовое ведение логов, выполнив +команду + +```sh +export DEBUG="grammy*" +``` + +в терминале перед выполнением `node bot.js`. Это облегчает отладку вашего бота + +Теперь вам нужно запустить бота, используя + +```sh +deno run --allow-net --allow-env bot.ts +``` + +чтобы grammY мог определить, что `DEBUG` установлен. +::: diff --git a/site/docs/ru/guide/introduction.md b/site/docs/ru/guide/introduction.md new file mode 100644 index 000000000..84112013a --- /dev/null +++ b/site/docs/ru/guide/introduction.md @@ -0,0 +1,208 @@ +# Введение + +Telegram Бот --- это специальный аккаунт, который автоматизируется за счет работы с API. +Создать Telegram бота может каждый, единственное условие - немного разбираться в коде. + +> Если вы уже знаете, как создавать ботов, перейдите по ссылке [Начало работы](./getting-started)! + +grammY --- это библиотека, с помощью которой написать такого бота очень просто. + +## Как написать бота + +Прежде чем приступить к созданию бота, ознакомьтесь с тем, что могут и чего не могут боты Telegram. +Ознакомьтесь с [Введением для разработчиков](https://core.telegram.org/bots) от команды Telegram. + +При создании бота Telegram вы создадите текстовый файл с исходным кодом вашего бота. +(Вы также можете скопировать один из наших файлов-примеров). +Он определяет _что на самом деле делает ваш бот_, т.е. "когда пользователь отправляет это сообщение, ответить таким образом" и так далее. + +Затем вы можете запустить этот исходный файл. +Теперь ваш бот будет работать, пока вы не прекратите его работу. + +Теперь вы вроде как закончили... + +## Как поддерживать бота в рабочем состоянии + +...за исключением случаев, когда вы серьезно относитесь к своему проекту бота. +Если вы остановите бота (или выключите компьютер), он станет неактивным и больше не будет реагировать на сообщения. + +> Пропустите этот раздел, если вы хотите только поиграть с ботами, и [продолжите здесь с необходимыми условиями](#необходимые-условия-для-начала-работы) для начала работы. + +Проще говоря, если вы хотите, чтобы бот постоянно находился в сети, вам придется держать компьютер работающим 24 часа в сутки. +Поскольку вы, скорее всего, не захотите делать это со своим компьютером, вам следует загрузить свой код на _хостинг-провайдер_ (другими словами, на чужой компьютер, также известный как _сервер_), и позволить этим людям запускать его для вас. + +Существует бесчисленное множество компаний, которые позволят вам запустить своего Telegram бота бесплатно. +В этой документации рассматривается ряд различных хостинг-провайдеров, которые, как мы знаем, хорошо работают с grammY (см. раздел [Хостинг](../hosting/comparison)). +Однако в конечном итоге выбор провайдера остается за вами. +Помните, что размещение вашего кода в другом месте означает, что тот, кто владеет этим "местом", имеет доступ ко всем вашим сообщениям и данным ваших пользователей, поэтому вам следует выбрать провайдера, которому вы можете доверять. + +Вот (упрощенная) схема того, как в итоге будет выглядеть настройка, когда Алиса свяжется с вашим ботом: + +```asciiart:no-line-numbers +_________ отправляет ____________ ___________ +| Алиса | —> Telegram сообщение —> | Telegram | —> HTTP запрос —> | ваш бот | +————————— вашему бота ———————————— ——————————— + + телефон сервера Telegram ваш компьютер, + лучше: сервер + + + |_____________________________________| |___________| + | | + Ответственность Telegram ваша ответственность +``` + +Аналогичным образом ваш бот может делать HTTP-запросы к серверам Telegram, чтобы отправлять сообщения Алисе. +(Если вы никогда не слышали о HTTP, то можете считать, что это пакеты данных, которые передаются через интернет). + +## Что grammY делает для вас + +Боты взаимодействуют с Telegram посредством HTTP-запросов. +Каждый раз, когда ваш бот отправляет или получает сообщения, HTTP-запросы идут туда и обратно между серверами Telegram и вашим сервером/компьютером. + +По своей сути, grammY реализует все эти коммуникации за вас, так что вы можете просто ввести `sendMessage` в свой код, и сообщение будет отправлено. +Кроме того, есть множество других полезных вещей, которые grammY делает, чтобы упростить создание бота. +Вы познакомитесь с ними по ходу работы. + +## Необходимые условия для начала работы + +> Пропустите остальную часть этой страницы, если вы уже знаете, как разрабатывать приложения Deno или Node.js, и [приступайте к работе](./getting-started). + +Вот несколько интересных вещей о программировании, но которые редко объясняются, потому что большинство разработчиков считают их очевидными. + +В следующем разделе вы создадите бота, написав текстовый файл, содержащий исходный код на языке программирования [TypeScript](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html). +Документация по grammY не научит вас программировать, поэтому мы ожидаем, что вы научитесь сами. +Однако помните: создание бота Telegram с помощью grammY --- это хороший способ научиться программированию! :rocket: + +::: tip Как научиться кодить +Вы можете начать изучение TypeScript с [официального учебника](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html), написанного командой TypeScript, а затем двигаться дальше. +Не тратьте больше 30 минут на чтение информации в интернете, затем вернитесь сюда, (прочитайте остальную часть раздела) и [приступайте к работе](./getting-started). + +Если вы видите незнакомый синтаксис в документации или получаете сообщение об ошибке, которое вам непонятно, погуглите - объяснение уже есть в интернете (например, на Stack Overflow). +::: + +::: danger Не учитесь кодить +Сэкономьте свое время, посмотрев это [34-секундное видео](https://youtu.be/8RtGlWmXGhA). +::: + +Выбрав grammY, вы уже определились с языком программирования, а именно TypeScript. +Но что произойдет после того, как вы создадите свой код на TypeScript, как он начнет выполняться? +Для этого вам нужно установить программное обеспечение, которое сможет _исполнить_ ваш код. +Такое программное обеспечение называется _средой выполнения_. +Она принимает файлы вашего исходного кода и фактически выполняет все, что в них запрограммировано. + +Для нас есть две среды выполнения на выбор: [Deno](https://deno.com) и [Node.js](https://nodejs.org). +(Если вы видите, что люди называют ее _Node_, значит, им просто лень набирать ".js", но означают они одно и то же). + +> Остальная часть этого раздела поможет вам определиться с выбором между этими двумя платформами. +> Если вы уже знаете, что хотите использовать, перейдите к разделу [необходимые условия для Node.js](#необходимые-условия-для-node-js) или [необходимые условия для Deno](#необходимые-условия-для-deno). + +Node.js --- это более старая, более зрелая технология. +Если вам нужно подключиться к нестандартной базе данных или сделать другие низкоуровневые системные вещи, вероятность того, что вы сможете сделать это с помощью Node.js, очень высока. +Deno - относительно новая технология, поэтому иногда ей все еще не хватает поддержки некоторых продвинутых вещей. +Сегодня большинство серверов используют Node.js. + +С другой стороны, Deno значительно проще в освоении и использовании. +Если у вас еще нет большого опыта в программировании, **имеет смысл начать с Deno.** + +Даже если вы уже писали код для Node.js, вам стоит попробовать Deno. +Многие вещи, которые сложно сделать в Node.js, не требуют особых усилий в Deno. + +Deno + +- гораздо проще в установке. +- не требует настройки проекта. +- использует намного меньше места на диске. +- имеет превосходные встроенные средства разработки и отличную интеграцию с редакторами. +- более безопасен. +- имеет множество других преимуществ, которые здесь не уместны. + +Разрабатывать код под Deno также намного интереснее. +По крайней мере, мы так считаем. + +Однако если у вас есть причины использовать Node.js, например, потому что вы уже хорошо его знаете, то это совершенно нормально! +Мы следим за тем, чтобы grammY одинаково хорошо работал на обеих платформах, и не срезаем углы. +Пожалуйста, выбирайте то, что вы считаете лучшим для себя. + +### Необходимые условия для Deno + +Прежде чем приступить к созданию бота, давайте потратим несколько минут на правильную настройку для разработки программного обеспечения. +Это означает установку нескольких инструментов. + +#### Подготовка вашей машины к разработке + +[Установите Deno](https://docs.deno.com/runtime/fundamentals/installation) если вы ещё не установили его. + +Вам также понадобится текстовый редактор, который хорошо подходит для программирования. +Лучшим из них для Deno является Visual Studio Code, часто называемый просто VS Code. +[Установите его.](https://code.visualstudio.com) + +Далее вам нужно соединить VS Code и Deno. +Это очень просто: Существует расширение для VS Code, которое делает все автоматически. +Вы можете установить его [как описано здесь](https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno). + +Теперь ваша система готова к разработке бота! :tada: + +#### Разработка бота + +Создайте новую директорию. +Она будет содержать проект с вашим ботом. +Откройте эту новую директорию в VS Code. + +```sh +mkdir ./my-bot +cd ./my-bot +code . +``` + +> Если вы находитесь на macOS и команда `code` недоступна, просто откройте VS Code, нажмите `Cmd+Shift+P`, введите "shell command" и нажмите Enter. + +В VS Code превратите пустую директорию в проект Deno. +Нажмите `Ctrl+Shift+P`, введите "deno init" и нажмите Enter. +В правом нижнем углу вашего редактора должна отобразиться версия Deno, установленная в вашей системе. + +Ваша среда разработки Deno готова. +Теперь вы можете приступить к написанию бота. +Об этом мы расскажем на следующей странице. + +И последнее: +После создания бота, например, в файле с именем `bot.ts`, вы можете запустить его, набрав в терминале команду `deno run --allow-net bot.ts`. +(Да, написание программ подразумевает частое использование терминала, привыкайте к этому). +Остановить бота снова можно с помощью `Ctrl+C`. + +Готовы? +[Приступайте к работе](./getting-started#приступаите-к-работе-с-deno)! :robot: + +### Необходимые условия для Node.js + +Вы собираетесь написать своего бота на TypeScript, но, в отличие от Deno, Node.js не может выполнять TypeScript. +Вместо этого, когда у вас есть исходный файл (например, `bot.ts`), вы собираетесь _компилировать_ его в JavaScript. +После этого у вас будет два файла: ваш исходный `bot.ts` и сгенерированный `bot.js`, который, в свою очередь, может быть запущен Node.js. +Точные команды для всего этого будут представлены в следующем разделе, когда вы действительно создадите бота, но важно знать, что эти шаги необходимы. + +Чтобы запустить файл `bot.js`, у вас должен быть установлен [Node.js](https://nodejs.org/en/). + +В общем, вот что вам нужно сделать для Node.js: + +1. Создайте исходный файл `bot.ts` с кодом TypeScript, например, с помощью [VS Code](https://code.visualstudio.com/) или любого другого редактора кода. +2. Скомпилируйте код, выполнив команду в терминале. В результате будет создан файл `bot.js`. +3. Запустите `bot.js` с помощью Node.js, опять же из терминала. + +Каждый раз, когда вы изменяете код в `bot.ts`, вам нужно перезапускать процесс Node.js. +Нажмите `Ctrl+C` в терминале, чтобы остановить процесс. +Это приведет к остановке бота. +Затем вам нужно повторить шаги 2 и 3. + +::: tip Подождите, что? +Установка Node.js и настройка всего необходимого занимает много времени. +Если вы никогда не занимались этим раньше, то можете столкнуться с множеством непонятных проблем, которые будет сложно решить. + +Именно поэтому мы предполагаем, что вы знаете, как настроить свою систему, или способны научиться сами. +(Установка Node.js _правильным способом_ настолько сложна, что для этой страницы она не подходит). + +Если на этом этапе вы чувствуете себя потерянным, вам следует оставить Node.js и использовать вместо него [Deno](#необходимые-условия-для-deno). +::: + +Все еще уверены в себе? +Отлично! +[Приступайте к работе](./getting-started#приступаите-к-работе-на-node-js)! :robot: diff --git a/site/docs/ru/guide/middleware.md b/site/docs/ru/guide/middleware.md new file mode 100644 index 000000000..50a0f1272 --- /dev/null +++ b/site/docs/ru/guide/middleware.md @@ -0,0 +1,257 @@ +# Middleware + +Функции-слушатели, которые передаются в `bot.on()`, `bot.command()` и им подобные, называются _middleware_. +Хотя неправильно говорить, что они слушают обновления, называть их "слушателями" --- это упрощение. + +> В этом разделе объясняется, что такое middleware, и на примере grammY показано, как его можно использовать. +> Если вы ищете конкретную документацию о том, что делает реализацию middleware в grammY особенной, посмотрите [Возможности Middleware](../advanced/middleware) в расширенном разделе документации. + +## Стек Middleware + +Предположим, вы пишете бота следующего вида: + +```ts{8} +const bot = new Bot(""); + +bot.use(session()); + +bot.command("start", (ctx) => ctx.reply("Запущен!")); +bot.command("help", (ctx) => ctx.reply("Текст помощи")); + +bot.on(":text", (ctx) => ctx.reply("Текст!")); // (*) +bot.on(":photo", (ctx) => ctx.reply("Фото!")); + +bot.start(); +``` + +При поступлении обновления с обычным текстовым сообщением будут выполнены следующие действия: + +1. Вы отправляете боту сообщение `"Привет!". +2. Middleware получает обновление и выполняет свои session действия. +3. Обновление будет проверено на наличие команды `/start`, которая не содержится +4. Обновление будет проверено на наличие команды `/help`, которая не содержится +5. Обновление будет проверено на наличие текста в сообщении (или сообщении канала), которое успешно. +6. Будет вызван middleware по адресу `(*)`, который обработает обновление, ответив `"Текст!". + +Обновление **не** проверяется на наличие фотоконтента, потому что middleware по адресу `(*)` уже обработало обновление. + +Итак, как это работает? +Давайте выясним. + +Мы можем просмотреть тип `Middleware` в документации grammY [здесь](/ref/core/middleware#type): + +```ts +// Для краткости опустите некоторые параметры типа. +type Middleware = MiddlewareFn | MiddlewareObj; +``` + +Ага! +Middleware может быть функцией или объектом. +Мы использовали только функции (`(ctx) => { ... }`), поэтому пока проигнорируем объекты middleware и углубимся в тип `MiddlewareFn` ([ссылка](/ref/core/middlewarefn)): + +```ts +// Снова опущены параметры типа. +type MiddlewareFn = (ctx: Context, next: NextFunction) => MaybePromise; +// с +type NextFunction = () => Promise; +``` + +Так, middleware принимает два параметра! +До сих пор мы использовали только один, объект контекста `ctx`. +Мы [уже знаем](./context), что такое `ctx`, но мы также видим функцию с именем `next`. +Чтобы понять, что такое `next`, нам нужно посмотреть на все middleware, которое вы устанавливаете на объект бота в целом. + +Вы можете рассматривать все установленные функции middleware как несколько слоев, которые накладываются друг на друга. +Первое middleware (`session` в нашем примере) является самым верхним слоем, поэтому оно первым получает каждое обновление. +Затем он может решить, хочет ли он обработать обновление или передать его следующему слою (обработчику команды `/start`). +Функция `next` может использоваться для вызова последующего middleware, часто называемого _нижележащий middleware_. +Это также означает, что если вы не вызовете функцию `next` в своем middleware, то нижележащие уровни middleware не будут вызваны. + +Этот стек функций является _стеком middleware_. + +```asciiart:no-line-numbers +(ctx, next) => ... | +(ctx, next) => ... |—————вышележащий middleware X +(ctx, next) => ... | +(ctx, next) => ... <— middleware X. Вызовите `next` чтобы пропустить обновления ниже +(ctx, next) => ... | +(ctx, next) => ... |—————нижележащий middleware X +(ctx, next) => ... | +``` + +Вспомнив наш предыдущий пример, мы теперь знаем, почему `bot.on(":photo")` даже не был проверен: middleware в `bot.on(":text", (ctx) => { ... })` уже обработал обновление, и оно не вызывало `next`. +На самом деле, он даже не указал `next` в качестве параметра. +Он просто проигнорировала `next`, а значит, не передал обновление. + +Давайте попробуем что-нибудь еще с нашими новыми знаниями! + +```ts +const bot = new Bot(""); + +bot.on(":text", (ctx) => ctx.reply("Текст!")); +bot.command("start", (ctx) => ctx.reply("Команда!")); + +bot.start(); +``` + +Если вы запустите вышеупомянутого бота и отправите `/start`, вы никогда не увидите ответ, говорящий `Команда!`. +Давайте проверим, что происходит: + +1. Вы посылаете боту команду `"/start"`. +2. Middleware `":text"` получает обновление и проверяет его на наличие текста, что удается, поскольку команды --- это текстовые сообщения. + Обновление немедленно обрабатывается первым middleware, и ваш бот отвечает "Текст!". + +Сообщение даже не проверяется на наличие в нем команды `/start`! +Порядок регистрации middleware имеет значение, потому что он определяет порядок слоев в стеке middleware. +Проблему можно решить, изменив порядок строк 3 и 4. +Если бы вы вызвали `next` в строке 3, было бы отправлено два ответа. + +**Функция `bot.use()` просто регистрирует middleware, который получает все обновления**. +Именно поэтому `session()` устанавливается через `bot.use()` - мы хотим, чтобы плагин работал со всеми обновлениями, независимо от того, какие данные в них содержатся. + +Наличие стека middleware - чрезвычайно мощное свойство любого веб-фреймворка, и этот паттерн широко популярен (не только для ботов Telegram). + +Давайте напишем свой собственный кусочек middleware, чтобы лучше проиллюстрировать, как это работает. + +## Writing Custom Middleware + +Мы проиллюстрируем концепцию middleware, написав простую middleware функцию, которая может измерять время ответа вашего бота, то есть время, которое требуется вашему боту для обработки сообщения. + +Вот сигнатура функции для нашего middleware. +Вы можете сравнить ее с типом middleware, приведенным выше, и убедить себя в том, что у нас действительно есть middleware. + +::: code-group + +```ts [TypeScript] +/** Измеряет время отклика бота и записывает его в `console` */ +async function responseTime( + ctx: Context, + next: NextFunction, // аналог для: () => Promise +): Promise { + // TODO: реализовать +} +``` + +```js [JavaScript] +/** Измеряет время отклика бота и записывает его в `console` */ +async function responseTime(ctx, next) { + // TODO: реализовать +} +``` + +::: + +Мы можем установить его в наш экземпляр `bot` с помощью `bot.use()`: + +```ts +bot.use(responseTime); +``` + +Давайте приступим к его реализации. +Вот что мы хотим сделать: + +1. Как только приходит обновление, мы сохраняем `Date.now()` в переменную. +2. Мы вызываем middleware, и, таким образом, позволяем обрабатывать все сообщения. + Это включает в себя подбор команды, ответ и все остальное, что делает ваш бот. +3. Мы снова берем `Date.now()`, сравниваем его со старым значением и `console.log` выводит разницу во времени. + +Важно установить наш middleware `responseTime` первым на бота (в верхней части стека middleware), чтобы убедиться, что все операции будут включены в измерение. + +::: code-group + +```ts [TypeScript] +/** Измеряет время отклика бота и записывает его в `console` */ +async function responseTime( + ctx: Context, + next: NextFunction, // аналог для: () => Promise +): Promise { + // сохраните начальное + const before = Date.now(); // миллисекунды + // вызовите нижележащий middleware + await next(); // убедитесь что вы ждёте отработки + // сохраните конечное время + const after = Date.now(); // миллисекунды + // выведите разницу + console.log(`Время ответа: ${after - before} мс`); +} + +bot.use(responseTime); +``` + +```js [JavaScript] +/** Измеряет время отклика бота и записывает его в `console` */ +async function responseTime(ctx, next) { + // сохраните начальное + const before = Date.now(); // миллисекунды + // вызовите нижележащий middleware + await next(); // убедитесь что вы ждёте отработки + // сохраните конечное время + const after = Date.now(); // миллисекунды + // выведите разницу + console.log(`Время ответа: ${after - before} мс`); +} + +bot.use(responseTime); +``` + +::: + +Готовый и работает! :heavy_check_mark: + +Не стесняйтесь использовать этот middleware в своем объекте бота, регистрировать больше слушателей и играть с примером. +Это поможет вам полностью понять, что такое middleware. + +::: danger ОПАСНО: Всегда следите за тем, чтобы вы ожидали отработки! +Если вы вызовете `next()` без ключевого слова `await`, это приведет к нескольким поломкам: + +- :x: Ваш стек middleware будет выполняться в неправильном порядке. +- :x: Возможна потеря данных. +- :x: Некоторые сообщения могут быть не отправлены. +- :x: Ваш бот может случайно упасть в результате трудно воспроизводимых действий. +- :x: Если произойдет ошибка, ваш обработчик ошибок не будет вызван для нее. + Вместо этого вы увидите, что возникнет `UnhandledPromiseRejectionWarning`, что может привести к краху процесса вашего бота. +- :x: Сломается механизм обратного давления в [grammY runner](../plugins/runner), который защищает ваш сервер от чрезмерно высокой нагрузки, например, во время скачков нагрузки. +- :skull: Иногда он также убивает всех ваших невинных котят :cry_cat_face: + +::: + +Правило, согласно которому вы должны использовать `await`, особенно важно для `next()`, но на самом деле оно применимо к любому выражению, возвращающему `Promise`. +Сюда относятся `bot.api.sendMessage`, `ctx.reply` и все остальные сетевые вызовы. +Если ваш проект важен для вас, то вы используете инструменты линтинга, которые предупредят вас, если вы забудете использовать `await` для `Promise`. + +::: tip Enable no-floating-promises +Рассмотрите возможность использования [ESLint](https://eslint.org/) и настройте его на использование правила [no-floating-promises](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/no-floating-promises.mdx). +Это позволит вам никогда не забывать использовать `await` (при этом крича на вас). +::: + +## Свойства Middleware в grammY + +В grammY, middleware может возвращать `Promise` (который будет `ожидаться`), но оно также может быть синхронным. + +В отличие от других систем middleware (например, от `express`), вы не можете передавать значения ошибок в `next`. +`next` не принимает никаких аргументов. +Если вы хотите получить ошибку, вы можете просто `вызвать` ошибку. +Еще одно отличие заключается в том, что не имеет значения, сколько аргументов принимает ваш middleware: `() => {}` будет обработано точно так же, как `(ctx) => {}`, или как `(ctx, next) => {}`. + +Существует два типа middleware: функции и объекты. +Объекты middleware --- это просто обертка для функций middleware. +В основном они используются внутри системы, но иногда могут помогать сторонним библиотекам или использоваться в расширенных сценариях, например, в [Composer](/ref/core/composer): + +```ts +const bot = new Bot(""); + +bot.use(/*...*/); +bot.use(/*...*/); + +const composer = new Composer(); +composer.use(/*...*/); +composer.use(/*...*/); +composer.use(/*...*/); +bot.use(composer); // composer это объект middleware! + +bot.use(/*...*/); +bot.use(/*...*/); +// ... +``` + +Если вы хотите глубже изучить, как grammY middleware, ознакомьтесь с [возможностями middleware](../advanced/middleware) в расширенном разделе документации. diff --git a/site/docs/ru/guide/reactions.md b/site/docs/ru/guide/reactions.md new file mode 100644 index 000000000..e6fab534f --- /dev/null +++ b/site/docs/ru/guide/reactions.md @@ -0,0 +1,208 @@ +# Реакции + +Боты могут работать с реакциями на сообщения. +Существует два типа реакций: реакции на эмодзи и пользовательские реакции на эмодзи. + +## Реакция на сообщения + +Боты могут добавить к сообщению одну реакцию в виде эмодзи. + +В тех же случаях боты могут реагировать и пользовательскими эмодзи (хотя боты не могут иметь [Telegram Премиум](https://telegram.org/faq_premium)). +Когда премиум пользователь добавляет к сообщению пользовательскую реакцию с помощью эмодзи, боты впоследствии могут добавить такую же реакцию к этому сообщению. +Кроме того, если администратор чата явно разрешает использовать пользовательские эмодзи, их могут использовать и боты в этом чате. + +Вот как можно реагировать на сообщения. + +```ts +// Используйте `ctx.react` для реакции на текущее сообщение. +bot.command("start", (ctx) => ctx.react("😍")); +bot.on("message", (ctx) => ctx.react("👍")); + +// Используйте `ctx.api.setMessageReaction` для реакций в другом месте. +bot.on("message", async (ctx) => { + await ctx.api.setMessageReaction(chat_id, message_id, "🎉"); +}); + +// Используйте `bot.api.setMessageReaction` вне обработчиков. +await bot.api.setMessageReaction(chat_id, message_id, "💯"); +``` + +Как обычно, TypeScript предоставит автоподсказку для эмодзи, которые вы можете использовать. +Список доступных эмодзи-реакций можно найти [здесь](https://core.telegram.org/bots/api#reactiontypeemoji). + +::: tip Плагин Emoji +Программировать с помощью эмодзи может быть некрасиво. +Не все системы могут правильно отображать исходный код. +Кроме того, надоедает постоянно копировать их из разных мест. + +Пусть [плагин emoji](../plugins/emoji#полезные-данные-для-реакции) поможет вам! +::: + +Теперь, когда вы знаете, как ваш бот может реагировать на сообщения, давайте посмотрим, как мы можем обрабатывать реакцию ваших пользователей. + +## Получение обновлений о реакциях + +Существует несколько различных способов обработки обновлений о реакциях. +В личных чатах и групповых чатах ваш бот будет получать обновление `message_reaction`, если пользователь изменил свою реакцию на сообщение. +В каналах (или автоматически пересылаемых сообщениях каналов в группах) ваш бот будет получать обновление `message_reaction_count`, которое показывает только общее количество реакций, но не раскрывает, кто отреагировал. + +Оба типа реакций должны быть включены, прежде чем вы сможете их получать. +Например, при polling вы можете включить их следующим образом: + +```ts +bot.start({ + allowed_updates: ["message", "message_reaction", "message_reaction_count"], +}); +``` + +::: tip Включение всех типов обновлений +Возможно, вы захотите импортировать `API_CONSTANTS` из grammY и затем указать + +```ts +allowed_updates: API_CONSTANTS.ALL_UPDATE_TYPES; +``` + +чтобы получать все обновления. +Обязательно ознакомьтесь с [API документацией](/ref/core/apiconstants#all-update-types). +::: + +[grammY runner](../plugins/runner#расширенные-настроики) и `setWebhook` имеют схожие способы указания `allowed_updates`. + +Теперь, когда ваш бот может получать обновления реакции, давайте посмотрим, как он может их обрабатывать! + +### Обработка новых реакций + +Очень просто обрабатывать вновь добавленные реакции. +В grammY для этого есть специальная поддержка через `bot.reaction`. + +```ts +bot.reaction("🎉", (ctx) => ctx.reply("блюп блюп")); +bot.reaction(["👍", "👎"], (ctx) => ctx.reply("Красивый палец")); +``` + +Эти обработчики будут срабатывать каждый раз, когда пользователь добавляет в сообщение новую реакцию. + +Естественно, если ваш бот обрабатывает пользовательские реакции от премиум-пользователей - вы можете прослушать и их. + +```ts +bot.reaction( + { type: "custom_emoji", custom_emoji_id: "identifier-string" }, + async (ctx) => {/* ... */}, +); +``` + +Для этого необходимо заранее знать идентификатор пользовательского эмодзи. + +Наконец, когда пользователь ставит реакцию звезды, вы можете обработать это обновление следующим образом. + +```ts +bot.reaction({ type: "paid" }, (ctx) => ctx.reply("Спасибо!")); +``` + +### Обработка произвольных изменений в реакциях + +Хотя это не видно в пользовательском интерфейсе официального клиента Telegram, на самом деле пользователи могут изменять несколько реакций одновременно. +Именно поэтому при обновлении реакций вы получаете два списка - старых и новых реакций. +Это позволяет вашему боту обрабатывать произвольные изменения в списке реакций. + +```ts +bot.on("message_reaction", async (ctx) => { + const reaction = ctx.messageReaction; + // Мы получаем только идентификатор сообщения, но не его содержимое. + const message = reaction.message_id; + // Разница между этими двумя списками описывает изменения. + const old = reaction.old_reaction; // предыдущие + const now = reaction.new_reaction; // текущие +}); +``` + +grammY позволяет еще больше отфильтровать обновления с помощью специальных [фильтрующих запросов](./filter-queries) для типа реакции. + +```ts +// Обновления, в которых текущая реакция содержит хотя бы один эмодзи. +bot.on("message_reaction:new_reaction:emoji", (ctx) => {/* ... */}); +// Обновления, в которых предыдущая реакция содержала хотя бы один пользовательский эмодзи. +bot.on("message_reaction:old_reaction:custom_emoji", (ctx) => {/* ... */}); +// Обновление, когда текущая реакция содержит реакцию звезды. +bot.on("message_reaction:new_reaction:paid", (ctx) => {/* ... */}); +``` + +Хотя эти два массива объектов [`ReactionType`](https://core.telegram.org/bots/api#reactiontype) технически дают вам всю информацию, необходимую для работы с обновлениями реакций, они все же могут быть немного громоздкими для работы. +Поэтому grammY может вычислять из обновлений более полезные вещи. + +### Проверка того, как изменилась реакция + +Существует [краткая запись](./context#краткая-запись) под названием `ctx.reactions`, которая позволяет увидеть, как именно изменилась реакция. + +Вот как можно использовать `ctx.reactions`, чтобы определить, удаляет ли пользователь свой палец вверх (но простить его, если он сохраняет свою реакцию окей). + +```ts +bot.on("message_reaction", async (ctx) => { + const { emoji, emojiAdded, emojiRemoved } = ctx.reactions(); + if (emojiRemoved.includes("👍")) { + // Палец вверх был удален! Неприемлемо. + if (emoji.includes("👌")) { + // Все в порядке, не наказывайте. + await ctx.reply("Я прощаю тебя"); + } else { + // Как они смеют. + await ctx.banAuthor(); + } + } +}); +``` + +Есть четыре массива, возвращаемые `ctx.reaction`: добавленные эмодзи, удаленные эмодзи, сохраненные эмодзи и список, который говорит вам о результате изменения. +Кроме того, есть еще четыре массива для пользовательских эмодзи с аналогичной информацией. +Наконец, есть два булевых флага для платных реакций. + +```ts +const { + /** Эмодзи, присутствующие в данный момент в реакции этого пользователя */ + emoji, + /** Эмодзи, недавно добавленные в реакцию этого пользователя */ + emojiAdded, + /** Эмодзи, не измененные обновлением реакции этого пользователя */ + emojiKept, + /** Эмодзи, удаленные из реакции этого пользователя */ + emojiRemoved, + /** Пользовательские эмодзи, присутствующие в данный момент в реакции этого пользователя */ + customEmoji, + /** Пользовательский эмодзи, недавно добавленный в реакцию этого пользователя */ + customEmojiAdded, + /** Пользовательские эмодзи, не измененные обновлением реакции этого пользователя */ + customEmojiKept, + /** Пользовательские эмодзи удалены из реакции этого пользователя */ + customEmojiRemoved, + /** Указывает, присутствует ли платная реакция в реакциях этого пользователя. */ + paid, + /** Указывает, была ли платная реакция недавно добавлена к реакции этого пользователя */ + paidAdded, +} = ctx.reactions(); +``` + +Многое было сказано о работе с обновлениями в личных чатах и групповых чатах. +Давайте рассмотрим каналы. + +### Обработка обновлений счетчика реакций + +В личных чатах, группах и супергруппах известно, кто и как реагирует на то или иное сообщение. +Однако для сообщений в канале у нас есть только список анонимных реакций. +Невозможно получить список пользователей, которые отреагировали на определенное сообщение. +То же самое справедливо и для сообщений канала, которые автоматически пересылаются в связанный с каналом чат. + +В обоих случаях ваш бот получит обновление `message_reaction_count`. + +Вы можете поступить с ним следующим образом. + +```ts +bot.on("message_reaction_count", async (ctx) => { + const counts = ctx.messageReactionCount; + // И снова мы видим только идентификатор сообщения. + const message = counts.message_id; + // Вот список реакций с подсчетом. + const { reactions } = counts; +}); +``` + +Обязательно следите за [спецификацией](https://core.telegram.org/bots/api#messagereactioncountupdated) для обновления количества реакций на сообщения. diff --git a/site/docs/ru/hosting/cloudflare-workers-nodejs.md b/site/docs/ru/hosting/cloudflare-workers-nodejs.md new file mode 100644 index 000000000..26003a5fb --- /dev/null +++ b/site/docs/ru/hosting/cloudflare-workers-nodejs.md @@ -0,0 +1,467 @@ +--- +prev: false +next: false +--- + +# Хостинг: Cloudflare Workers (Node.js) + +[Cloudflare Workers](https://workers.cloudflare.com) --- это публичная платформа для бессерверных вычислений, которая предлагает удобное и простое решение для выполнения JavaScript на [edge](https://en.wikipedia.org/wiki/Edge_computing). +Благодаря возможности обрабатывать HTTP-трафик и использованию [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), создание ботов Telegram становится простым делом. +Кроме того, вы можете даже разрабатывать [web приложения](https://core.telegram.org/bots/webapps) на edge, и все это бесплатно в пределах определенных квот. + +В этом руководстве мы расскажем вам о том, как разместить ботов Telegram на Cloudflare Workers. + +::: tip Ищете версию Deno? +В этом руководстве объясняется, как развернуть бота Telegram на Cloudflare Workers с помощью Node.js. +Если вы ищете версию Deno, пожалуйста, ознакомьтесь с [этой страницей](./cloudflare-workers). +::: + +## Необходимые условия + +1. Аккаунт [Cloudflare](https://dash.cloudflare.com/login) с [настроенным](https://dash.cloudflare.com/?account=workers) и рабочим поддоменом. +2. Рабочая среда [Node.js](https://nodejs.org/) с установленным `npm`. + +## Настройка + +Сначала создайте новый проект: + +```sh +npm create cloudflare@latest +``` + +Затем вам будет предложено ввести имя worker'а: + +```ansi{6} +using create-cloudflare version 2.17.1 + +╭ Create an application with Cloudflare Step 1 of 3 +│ +╰ In which directory do you want to create your application? also used as application name // [!code focus] +  ./grammybot // [!code focus] +``` + +Здесь мы создаем проект с именем `grammybot`, вы можете выбрать свое собственное, это будет имя вашего worker'а, а также часть URL запроса. + +::: tip +Вы можете изменить имя worker'а в файле `wrangler.toml` позже. +::: + +Далее вам будет предложено выбрать тип рабочего, здесь мы выбрали `"Hello World" Worker`: + +```ansi{8} +using create-cloudflare version 2.17.1 + +╭ Create an application with Cloudflare Step 1 of 3 +│ +├ In which directory do you want to create your application? +│ dir ./grammybot +│ +╰ What type of application do you want to create? // [!code focus] +  ● "Hello World" Worker // [!code focus] +  ○ "Hello World" Worker (Python) // [!code focus] +  ○ "Hello World" Durable Object // [!code focus] +  ○ Website or web app // [!code focus] +  ○ Example router & proxy Worker // [!code focus] +  ○ Scheduled Worker (Cron Trigger) // [!code focus] +  ○ Queue consumer & producer Worker // [!code focus] +  ○ API starter (OpenAPI compliant) // [!code focus] +  ○ Worker built from a template hosted in a git repository // [!code focus] +``` + +Далее вам будет предложено выбрать, хотите ли вы использовать TypeScript, если вы хотите использовать JavaScript, выберите `No`. +Здесь мы выбираем `Yes`: + +```ansi{11} +using create-cloudflare version 2.17.1 + +╭ Create an application with Cloudflare Step 1 of 3 +│ +├ In which directory do you want to create your application? +│ dir ./grammybot +│ +├ What type of application do you want to create? +│ type "Hello World" Worker +│ +╰ Do you want to use TypeScript? // [!code focus] +  Yes / No // [!code focus] +``` + +Ваш проект будет настроен через несколько минут. +После этого вас спросят, хотите ли вы использовать git для контроля версий, выберите `Yes`, если вы хотите, чтобы репозиторий инициализировался автоматически, или `No`, если вы хотите инициализировать его самостоятельно позже. + +Здесь мы выбираем `Yes`: + +```ansi{36} +using create-cloudflare version 2.17.1 + +╭ Create an application with Cloudflare Step 1 of 3 +│ +├ In which directory do you want to create your application? +│ dir ./grammybot +│ +├ What type of application do you want to create? +│ type "Hello World" Worker +│ +├ Do you want to use TypeScript? +│ yes typescript +│ +├ Copying template files +│ files copied to project directory +│ +├ Updating name in `package.json` +│ updated `package.json` +│ +├ Installing dependencies +│ installed via `npm install` +│ +╰ Application created + +╭ Configuring your application for Cloudflare Step 2 of 3 +│ +├ Installing @cloudflare/workers-types +│ installed via npm +│ +├ Adding latest types to `tsconfig.json` +│ added @cloudflare/workers-types/2023-07-01 +│ +├ Retrieving current workerd compatibility date +│ compatibility date 2024-04-05 +│ +╰ Do you want to use git for version control? // [!code focus] +  Yes / No // [!code focus] +``` + +Наконец, вас спросят, хотите ли вы развернуть worker'а, выберите `No`, поскольку мы собираемся развернуть его, когда у нас будет рабочий Telegram бот: + +```ansi{49} +using create-cloudflare version 2.17.1 + +╭ Create an application with Cloudflare Step 1 of 3 +│ +├ In which directory do you want to create your application? +│ dir ./grammybot +│ +├ What type of application do you want to create? +│ type "Hello World" Worker +│ +├ Do you want to use TypeScript? +│ yes typescript +│ +├ Copying template files +│ files copied to project directory +│ +├ Updating name in `package.json` +│ updated `package.json` +│ +├ Installing dependencies +│ installed via `npm install` +│ +╰ Application created + +╭ Configuring your application for Cloudflare Step 2 of 3 +│ +├ Installing @cloudflare/workers-types +│ installed via npm +│ +├ Adding latest types to `tsconfig.json` +│ added @cloudflare/workers-types/2023-07-01 +│ +├ Retrieving current workerd compatibility date +│ compatibility date 2024-04-05 +│ +├ Do you want to use git for version control? +│ yes git +│ +├ Initializing git repo +│ initialized git +│ +├ Committing new files +│ git commit +│ +╰ Application configured + +╭ Deploy with Cloudflare Step 3 of 3 +│ +╰ Do you want to deploy your application? // [!code focus] +  Yes / No // [!code focus] +``` + +## Установка зависимостей + +`cd` в `grammybot` (замените это имя на имя вашего worker'а, которое вы задали выше), установите `grammy` и другие пакеты, которые могут вам понадобиться: + +```sh +npm install grammy +``` + +## Создание бота + +Отредактируйте `src/index.js` или `src/index.ts` и напишите этот код внутри: + +```ts{11,28-29,38,40-42,44} +/** + * Welcome to Cloudflare Workers! This is your first worker. + * + * - Run `npm run dev` in your terminal to start a development server + * - Open a browser tab at http://localhost:8787/ to see your worker in action + * - Run `npm run deploy` to publish your worker + * + * Learn more at https://developers.cloudflare.com/workers/ + */ + +import { Bot, Context, webhookCallback } from "grammy"; + +export interface Env { + // Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/ + // MY_KV_NAMESPACE: KVNamespace; + // + // Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/ + // MY_DURABLE_OBJECT: DurableObjectNamespace; + // + // Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/ + // MY_BUCKET: R2Bucket; + // + // Example binding to a Service. Learn more at https://developers.cloudflare.com/workers/runtime-apis/service-bindings/ + // MY_SERVICE: Fetcher; + // + // Example binding to a Queue. Learn more at https://developers.cloudflare.com/queues/javascript-apis/ + // MY_QUEUE: Queue; + BOT_INFO: string; + BOT_TOKEN: string; +} + +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext, + ): Promise { + const bot = new Bot(env.BOT_TOKEN, { botInfo: JSON.parse(env.BOT_INFO) }); + + bot.command("start", async (ctx: Context) => { + await ctx.reply("Привет, мир!"); + }); + + return webhookCallback(bot, "cloudflare-mod")(request); + }, +}; +``` + +Здесь мы сначала импортируем `Bot`, `Context` и `webhookCallback` из `grammy`. + +Внутри интерфейса `Env` мы добавляем переменную `BOT_INFO`, это переменная окружения, которая хранит информацию о вашем боте, вы можете получить информацию о боте, вызвав Telegram Bot API с помощью метода `getMe`. +Откройте эту ссылку в своем браузере: + +```ansi:no-line-numbers +https://api.telegram.org/bot<токен>/getMe +``` + +Замените `<токен>` на токен вашего бота. +В случае успеха вы увидите ответ в формате JSON, похожий на этот: + +```json{3-12} +{ + "ok": true, + "result": { + "id": 1234567890, + "is_bot": true, + "first_name": "mybot", + "username": "MyBot", + "can_join_groups": true, + "can_read_all_group_messages": false, + "supports_inline_queries": true, + "can_connect_to_business": false + } +} +``` + +Теперь откройте файл `wrangler.toml` в корне вашего проекта и добавьте переменную окружения `BOT_INFO` в разделе `[vars]` со значением из объекта `result`, которое вы получили выше, следующим образом: + +```toml +[vars] +BOT_INFO = """{ + "id": 1234567890, + "is_bot": true, + "first_name": "mybot", + "username": "MyBot", + "can_join_groups": true, + "can_read_all_group_messages": false, + "supports_inline_queries": true, + "can_connect_to_business": false +}""" +``` + +Замените информацию о боте на ту, которую вы получаете из браузера. +Обратите внимание на три двойные кавычки `""` в начале и в конце. + +В дополнение к `BOT_INFO`, мы также добавляем переменную `BOT_TOKEN`, это переменная окружения, которая хранит ваш токен бота, используемый для создания бота. + +Вы можете заметить, что мы только определили переменную `BOT_TOKEN`, но еще не присвоили ее. +Обычно нужно хранить переменную окружения в файле `wrangler.toml`, однако в нашем случае это небезопасно, поскольку токен бота должен храниться в секрете. +Cloudflare Workers предоставляет нам безопасный способ хранения конфиденциальной информации, такой как API-ключи и auth-токены, в переменной окружения: [secrets](https://developers.cloudflare.com/workers/configuration/secrets/#secrets-on-deployed-workers)! + +::: tip +Секретные значения не видны в Wrangler или в Cloudflare Dashboard после того, как вы их определили. +::: + +Вы можете добавить секрет в свой проект с помощью следующей команды: + +```sh +npx wrangler secret put BOT_TOKEN +``` + +Следуйте инструкциям и введите свой токен бота, который будет загружен и зашифрован. + +::: tip +Вы можете изменить название переменных окружения на любое другое, но помните, что в следующих шагах вы будете делать то же самое. +::: + +Внутри функции `fetch()` мы создаем бота с `BOT_TOKEN`, который отвечает "Привет, мир!", когда получает `/start`. + +## Развертывание вашего бота + +Теперь вы можете развернуть своего бота с помощью следующей команды: + +```sh +npm run deploy +``` + +## Настройка вебхука + +Нам нужно указать Telegram, куда отправлять обновления. +Откройте браузер и перейдите по этой ссылке: + +```ansi:no-line-numbers +https://api.telegram.org/bot<токен>/setWebhook?url=https://<мой_бот>.<мой_поддомен>.workers.dev/ +``` + +Замените `<токен>` на токен вашего бота, замените `<мой_бот>` на имя вашего рабочего, замените `<мой_поддомен>` на ваш рабочий поддомен, настроенный в Cloudflare. + +Если настройка прошла успешно, вы увидите JSON следующего вида: + +```json +{ + "ok": true, + "result": true, + "description": "Webhook was set" +} +``` + +## Тестирование вашего бота + +Откройте приложение Telegram и запустите своего бота. +Если он ответит, значит, все готово! + +## Отладка вашего бота + +Для тестирования и отладки вы можете запустить локальный или удаленный сервер разработки, прежде чем развертывать бота. + +В среде разработки ваш бот не имеет доступа к секретным переменным окружения. +Поэтому, [согласно Cloudflare](https://developers.cloudflare.com/workers/configuration/secrets/#local-development-with-secrets), вы можете создать файл `.dev.vars` в корне вашего проекта для определения секретов: + +```env +BOT_TOKEN=<токен_вашего_бота> # <- заменит это на свой токен. +``` + +Не забудьте также добавить `BOT_INFO` для разработки. +Щелкните [здесь](https://developers.cloudflare.com/workers/configuration/environment-variables/) и [здесь](https://developers.cloudflare.com/workers/configuration/secrets/) для получения более подробной информации о переменных окружения и секретах. + +Замените `BOT_INFO` и `BOT_TOKEN` своим значением, если вы изменили имя переменной окружения на предыдущем шаге. + +::: tip +Вы можете использовать другой токен бота для разработки, чтобы он не влиял на работу основного. +::: + +Теперь вы можете выполнить следующую команду, чтобы запустить сервер разработки: + +```sh +npm run dev +``` + +После запуска сервера разработки вы можете протестировать своего бота, отправив ему примеры обновлений с помощью таких инструментов, как `curl`, [Insomnia](https://insomnia.rest) или [Postman](https://postman.com). +Примеры обновлений см. в [здесь](https://core.telegram.org/bots/webhooks#testing-your-bot-with-updates), а более подробную информацию о структуре обновлений --- [здесь](https://core.telegram.org/bots/api#update). + +Если вы не хотите конструировать обновление или хотите протестировать его на реальном обновлении, вы можете получить обновление из Telegram Bot API с помощью метода `getUpdates`. +Для этого вам нужно будет сначала удалить вебхук. +Откройте веб-браузер и перейдите по этой ссылке: + +```ansi:no-line-numbers +https://api.telegram.org/bot<токен>/deleteWebhook +``` + +Замените `<токен>` на токен вашего бота, и вы увидите JSON, похожий на этот: + +```json +{ + "ok": true, + "result": true, + "description": "Webhook was deleted" +} +``` + +Затем откройте клиент Telegram и отправьте что-нибудь боту, например, отправьте `/start`. + +Теперь перейдите по этой ссылке в браузере, чтобы получать обновления: + +```ansi:no-line-numbers +https://api.telegram.org/bot<токен>/getUpdates +``` + +Снова замените `<токен>` на токен вашего бота, в случае успеха вы увидите JSON, похожий на этот: + +```json{4-29} +{ + "ok": true, + "result": [ + { + "update_id": 123456789, + "message": { + "message_id": 123, + "from": { + "id": 987654321, + "is_bot": false, + "first_name": "", + "language_code": "en" + }, + "chat": { + "id": 987654321, + "first_name": "", + "type": "private" + }, + "date": 1712803046, + "text": "/start", + "entities": [ + { + "offset": 0, + "length": 6, + "type": "bot_command" + } + ] + } + } + ] +} +``` + +`result` --- это массив объектов обновлений (выше содержится только один объект обновлений), вам следует скопировать только один объект и протестировать бота, разместив этот объект на сервере разработки с помощью вышеупомянутых инструментов. + +Если вы хотите игнорировать устаревшие обновления (например, игнорировать все обновления во время разработки перед развертыванием в производственной среде), вы можете добавить параметр `offset` в метод `getUpdates`, как показано ниже: + +```ansi:no-line-numbers +https://api.telegram.org/bot<токен>/getUpdates?offset= +``` + +Замените `<токен>` на ваш токен бота, а `` на `update_id` последнего полученного обновления (с наибольшим номером), после чего вы будете получать только обновления, вышедшие позже этого обновления, и никогда не сможете получить обновления, вышедшие раньше. + +Теперь вы можете тестировать своего бота с реальными объектами обновлений в своей локальной среде разработки! + +Вы также можете вывести свой локальный сервер разработки в публичный интернет, используя некоторые сервисы обратного прокси, например [Ngrok](https://ngrok.com/), и установить вебхук на URL, который вы получите от них, или вы можете настроить свой собственный обратный прокси, если у вас есть публичный IP-адрес, доменное имя и SSL сертификат, но это выходит за рамки данного руководства. +Дополнительную информацию о настройке обратного прокси см. в документации к используемому вами программному обеспечению. + +::: warning +Использование стороннего обратного прокси может привести к утечке информации! +::: + +::: tip +Не забудьте [установить вебхук обратно](#настроика-вебхука) при развертывании в prod. +::: diff --git a/site/docs/ru/hosting/cloudflare-workers.md b/site/docs/ru/hosting/cloudflare-workers.md new file mode 100644 index 000000000..9a903162d --- /dev/null +++ b/site/docs/ru/hosting/cloudflare-workers.md @@ -0,0 +1,163 @@ +--- +prev: false +next: false +--- + +# Хостинг: Cloudflare Workers (Deno) + +[Cloudflare Workers](https://workers.cloudflare.com) --- это публичная платформа +для бессерверных вычислений, которая предлагает удобное и простое решение для +выполнения небольших рабочих нагрузок на +[edge](https://en.wikipedia.org/wiki/Edge_computing). + +Это руководство проведет вас через процесс размещения вашего бота на Cloudflare +Workers. + +::: tip Ищете версию для Node.js? В этом руководстве объясняется, как развернуть +бота Telegram на Cloudflare Workers с помощью Deno. Если вы ищете версию для +Node.js, пожалуйста, ознакомьтесь с +[этим руководством](./cloudflare-workers-nodejs). +::: + +## Необходимые условия + +Чтобы следовать дальше, убедитесь, что у вас есть +[учетная запись Cloudflare](https://dash.cloudflare.com/login) с рабочим, +[настроенным](https://dash.cloudflare.com/?account=workers) поддоменом. + +## Настройка + +Убедитесь, что у вас установлены [Deno](https://deno.com) и +[Denoflare](https://denoflare.dev). + +Создайте новый каталог и создайте новый файл `.denoflare` в этом каталоге. +Поместите в этот файл следующее содержимое: + +> Примечание: Ключ «$schema» в следующем JSON-коде указывает на фиксированную +> версию в своем URL («v0.5.12»). На момент написания статьи это была последняя +> доступная версия. Вам следует обновить их до +> [самой новой версии](https://github.com/skymethod/denoflare/releases). + +```json{2,9,17-18} +{ + "$schema": "https://raw.githubusercontent.com/skymethod/denoflare/v0.5.12/common/config.schema.json", + "scripts": { + "my-bot": { + "path": "bot.ts", + "localPort": 3030, + "bindings": { + "BOT_TOKEN": { + "value": "ВАШ_ТОКЕН_БОТА" + } + }, + "workersDev": true + } + }, + "profiles": { + "account1": { + "accountId": "ВАШ_АККАУНТ_ID", + "apiToken": "ВАШ_API_ТОКЕН" + } + } +} +``` + +Обязательно замените `ВАШ_АККАУНТ_ID`, `ВАШ_API_ТОКЕН` и `ВАШ_ТОКЕН_БОТА` +соответствующим образом. При создании API-токена вы можете выбрать предустановку +`Edit Cloudflare Workers` из предварительно настроенных разрешений. + +## Создание бота + +Создайте новый файл с именем `bot.ts` и поместите в него следующее содержимое: + +```ts +import { Bot, webhookCallback } from "https://deno.land/x/grammy/mod.ts"; +import { UserFromGetMe } from "https://deno.land/x/grammy/types.ts"; + +interface Environment { + BOT_TOKEN: string; +} + +let botInfo: UserFromGetMe | undefined = undefined; + +export default { + async fetch(request: Request, env: Environment) { + try { + const bot = new Bot(env.BOT_TOKEN, { botInfo }); + + if (botInfo === undefined) { + await bot.init(); + botInfo = bot.botInfo; + } + + bot.command( + "start", + (ctx) => ctx.reply("Добро пожаловать! Запущен и работаю."), + ); + bot.on("message", (ctx) => ctx.reply("Получил сообщение!")); + + const cb = webhookCallback(bot, "cloudflare-mod"); + + return await cb(request); + } catch (e) { + return new Response(e.message); + } + }, +}; +``` + +## Развертывание вашего бота + +Это так же просто, как бегать: + +```sh +denoflare push my-bot +``` + +В результате выполнения этой команды вы получите информацию о хосте, на котором +запущен worker. Обратите внимание на строку, содержащую что-то вроде +`<мой_бот>.<мой_поддомен>.workers.dev`. Это хост, на котором ваш бот ожидает +вызова. + +## Настройка вебхука + +Нам нужно указать Telegram, куда отправлять обновления. Откройте браузер и +перейдите по этому URL-адресу: + +```text +https://api.telegram.org/bot<токен>/setWebhook?url=https://<мой_бот>.<мой_поддомен>.workers.dev/ +``` + +Замените `<токен>`, `<мой_бот>` и `<мой_поддомен>` своими значениями. Если +настройка прошла успешно, вы увидите JSON-ответ следующего вида: + +```json +{ + "ok": true, + "result": true, + "description": "Webhook was set" +} +``` + +## Тестирование вашего бота + +Откройте приложение Telegram и запустите своего бота. Если он ответит, значит, +все готово! + +## Отладка вашего бота + +Для тестирования и отладки вы можете запустить локальный или удаленный сервер +разработки перед развертыванием бота в производстве. Просто выполните следующую +команду: + +```sh +denoflare serve my-bot +``` + +После запуска сервера разработки вы можете протестировать своего бота, отправив +ему примеры обновлений с помощью таких инструментов, как `curl`, +[Insomnia](https://insomnia.rest) или [Postman](https://postman.com). Примеры +обновлений см. на +[здесь](https://core.telegram.org/bots/webhooks#testing-your-bot-with-updates), +а более подробную информацию о структуре обновлений +[здесь](https://core.telegram.org/bots/api#update). diff --git a/site/docs/ru/hosting/comparison.md b/site/docs/ru/hosting/comparison.md new file mode 100644 index 000000000..e18e1fee4 --- /dev/null +++ b/site/docs/ru/hosting/comparison.md @@ -0,0 +1,124 @@ +--- +prev: false +next: false +--- + +# Сравнение хостинг-провайдеров + +Существует множество различных хостинг-провайдеров, которые позволяют запускать +бота. Иногда бывает трудно уследить за тем, сколько они стоят и насколько хороша +их работа. Поэтому сообщество grammY собирает свой опыт на этой странице. + +## Что такое хостинг-провайдер? + +Для того чтобы поддерживать бота в сети 24 часа в сутки, вам нужно, чтобы +компьютер работал 24 часа в сутки. Как +[упоминалось во введении](../guide/introduction#как-поддерживать-бота-в-рабочем-состоянии), +вы, скорее всего, не захотите делать это с помощью своего ноутбука или домашнего +компьютера. Вместо этого вы можете попросить компанию запустить бота в облаке. + +Другими словами, вы просто запускаете его на чужом компьютере. + +## Таблицы сравнения + +> Пожалуйста, нажмите на кнопку редактирования внизу страницы, чтобы добавить +> новых провайдеров или отредактировать существующих! + +У нас есть две сравнительные таблицы: одна для +[бессерверного хостинга и PaaS](#бессерверные-и-paas) и одна для [VPS](#vps). + +### Бессерверные и PaaS + +Бессерверность означает, что вы не контролируете одну машину, на которой запущен +ваш бот. Вместо этого такие хостинг-провайдеры позволят вам загружать код, а +затем запускать и останавливать разные машины по мере необходимости, чтобы ваш +бот всегда работал. + +Главное, что нужно знать о них --- это то, что на бессерверных инфраструктурах +вы должны использовать [вебхуки](../guide/deployment-types). Большинство из +перечисленных ниже провайдеров будут иметь проблемы, если вы попытаетесь +запустить на них бота с long polling (`bot.start()` или +[grammY runner](../plugins/runner)). + +С другой стороны, PaaS (Platform as a Service) предоставляет аналогичное, но +более контролируемое решение. Вы можете выбрать, сколько экземпляров машин будут +обслуживать вашего бота и когда они будут запущены. Использование +[long polling](../guide/deployment-types) также возможно в PaaS, если выбранный +вами провайдер позволяет постоянно держать запущенным только один экземпляр. + +Недостатком бессерверных и PaaS является то, что по умолчанию они не +предоставляют постоянного хранилища, например локальной файловой системы. Вместо +этого вам часто придется иметь отдельную базу данных и подключаться к ней, если +вам нужно хранить данные постоянно. + +| Название | Мин. цена | Цены | Лимиты | Node.js | Deno | Web | Заметки | +| ---------------------- | --------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Deta | Бесплатно | Нет тарифных планов | Не указано лимитов | ✅ | ✅ | ✅ | Deno поддерживается [пользовательским приложением](https://deta.space/docs/en/build/quick-starts/custom) ([пример](https://github.com/deta/starters/tree/main/deno-app)). | +| Deno Deploy | Бесплатно | $20/мес подписка за 5M зап и 100 ГБ; $2/1М зап, $0.5/ГБ трафика | [1M зап/мес, 100 ГБ/мес, 10 мс CPU-time лимит](https://deno.com/deploy/pricing) | ❌ | ✅ | ❌ | | +| Fly | Бесплатно | $1.94/мес подписка за shared-cpu-1x и 256 МБ ОЗУ, $0.02/ГБ трафика | [3 shared-cpu-1x 256mb VMs, 160ГБ/мес, 3ГБ хранилище](https://fly.io/docs/about/pricing/) | ✅ | ✅ | ❓ | | +| DigitalOcean Functions | Бесплатно | $1.85/100K ГБ-С | [90K ГБ-С/мес](https://docs.digitalocean.com/products/functions/details/pricing/) | ✅ | ❌ | ❓ | | +| Cloudflare Workers | Бесплатно | $5/10M зап | [100K зап/день, 10 мс CPU-time лимит](https://workers.cloudflare.com/) | ❌ | [✅](https://denoflare.dev/) | ✅ | | +| Vercel | Бесплатно | $20/мес подписка | [Неограниченные зап, 100 ГБ-h, 10 сек. лимит](https://vercel.com/pricing) | [✅](https://vercel.com/docs/functions/runtimes/node-js) | [✅](https://github.com/vercel-community/deno) | [✅](https://vercel.com/docs/frameworks) | | +| Scaleway Functions | Бесплатно | €0.15/1M зап, €1.2/100K ГБ-С | [1M зап, 400K ГБ-С/мес](https://www.scaleway.com/en/pricing/serverless/#serverless-functions) | ❓ | ❓ | ❓ | | +| Scaleway Containers | Бесплатно | €0.10/100K ГБ-С, €1.0/100K vCPU-s | [400K ГБ-С, 200K vCPU-s/мес](https://www.scaleway.com/en/pricing/serverless/#serverless-containers) | ❓ | ❓ | ❓ | | +| Vercel Edge Functions | Бесплатно | $20/мес подписка за 500K | [100K зап/день](https://vercel.com/pricing) | [✅](https://vercel.com/docs/functions/runtimes/edge-runtime#compatible-node.js-modules) | ❓ | [✅](https://vercel.com/templates/edge-functions) | | +| serverless.com | Бесплатно | | | ❓ | ❓ | ❓ | | +| Heroku | $5 | $5 за 1,000 [dyno часов](https://devcenter.heroku.com/articles/usage-and-billing#dyno-usage-and-costs)/мес | [512МБ ОЗУ, отключается после 30 минут бездействия](https://www.heroku.com/pricing) | ✅ | ✅ | ❓ | Deno поддерживается [сторонним билдпаком](https://github.com/chibat/heroku-buildpack-deno). | +| DigitalOcean Apps | $5 | | | ❓ | ❓ | ❓ | Не тестировано | +| Fastly Compute@Edge | | | | ❓ | ❓ | ❓ | | +| Zeabur | $5 | $5/мес подписка | 2ГБ ОЗУ, Неограниченные зап | ✅ | ✅ | ✅ | | + +### VPS + +Виртуальный частный сервер (VPS) --- это виртуальная машина, над которой вы +имеете полный контроль. Обычно доступ к ней осуществляется через +[SSH](https://en.wikipedia.org/wiki/Secure_Shell). Вы можете установить на нее +любое программное обеспечение, а также нести ответственность за обновление +системы и так далее. + +На VPS вы можете запускать ботов, используя как long polling, так и веб-хуки. + +Посмотрите [туториал](./vps) о том, как разместить ботов grammY на VPS. + +| Название | Мин. цена | Пинг бота | Самый дешевый вариант | +| ------------- | --------- | ----------------------------------------- | ---------------------------------- | +| Hostinger | $14 | | 1 vCPU, 4 ГБ ОЗУ, 50 ГБ SSD, 1 ТБ | +| Contabo | | 15 мс :de: Нюрнберг | | +| DigitalOcean | $5 | 1-15 мс :netherlands: AMS, 19 мс :de: FRA | 1 vCPU, 1 ГБ ОЗУ, 25 ГБ SSD, 1 ТБ | +| Hetzner Cloud | €4.15 | ~42 мс :de: | 1 vCPU, 2 ГБ ОЗУ, 20 ГБ SSD, 20 ТБ | +| IONOS VPS | €1 или $2 | 15 мс :de: Баден-Баден | 1 vCPU, 0.5 ГБ ОЗУ, 8 ГБ SSD | +| Scaleway | €~7 | | 2 cores, 2 ГБ ОЗУ, 20 ГБ SSD | +| MVPS | €4 | 6-9 мс :de: Германия | 1 core, 2 ГБ ОЗУ, 25 ГБ SSD, 2 ТБ | + +## Пояснения к сокращениям + +### Базовые сокращения + +| Сокращение | Расшифровка | Объяснение | +| ---------- | -------------- | ------------------------------------------------------------------------------------------------ | +| K | тысяча | 1,000 чего-нибудь. | +| M | миллион | 1,000,000 чего-нибудь. | +| € | Евро | Валюта EUR. | +| $ | Доллар | Валюта USD. | +| зап | запрос | Количество HTTP запросов. | +| vCPU | виртуальный ЦП | Вычислительная мощность одного виртуального процессора, являющегося частью реального процессора. | +| мс | миллисекунда | 0.001 секунды. | +| с | секунда | Одна секунда (Единица СИ для обозначения времени). | +| мин | минута | Одна минута, 60 секунд. | +| час | час | Один час, 60 минут. | +| день | день | Один день, 24 часов. | +| мес | мясяц | Один месяц, примерно 30 дней. | +| ГБ | гигабайт | 1,000,000,000 байт хранилища. | + +### Примеры комбинаций + +| Единица измерения | Обозначение | Расшифровка | Объяснение | +| ----------------- | ---------------------------- | --------------------------------------------- | --------------------------------------------------------- | +| $/мес | цена | Долларов в месяц | Цена в месяц. | +| €/M зап | цена | Евро на миллион запросов | Стоимость обработки одного миллиона запросов. | +| зап/мин | пропускная способность | запросов в минуту | Количество запросов, обработанных за одну минуту. | +| ГБ/с | пропускная способность | гигабайт в секунду | Количество гигабайт, передаваемых за одну секунду. | +| ГБ-с | использование памяти | гигабайт в секунду | Один гигабайт, используемый за одну секунду. | +| ГБ-ч | использование памяти | гигабайт в час | Один гигабайт, используемый в течение часа. | +| ч/мес | временной интервал | часов в месяц | Количество часов в месяц. | +| K vCPU-с/мес | временной интервал обработки | тысяч виртуальных процессорных секунд в месяц | Секунд в месяц обработки с одним виртуальным процессором. | diff --git a/site/docs/ru/hosting/deno-deploy.md b/site/docs/ru/hosting/deno-deploy.md new file mode 100644 index 000000000..5cc94dd47 --- /dev/null +++ b/site/docs/ru/hosting/deno-deploy.md @@ -0,0 +1,103 @@ +--- +prev: false +next: false +--- + +# Хостинг: Deno Deploy + +В этом руководстве рассказывается о том, как вы можете разместить своих ботов grammY на [Deno Deploy](https://deno.com/deploy). + +Обратите внимание, что это руководство предназначено только для пользователей Deno, и для создания учетной записи [Deno Deploy](https://deno.com/deploy) вам необходимо иметь аккаунт [GitHub](https://github.com). + +Deno Deploy идеально подходит для большинства простых ботов, но следует учитывать, что не все функции Deno доступны для приложений, работающих на Deno Deploy. +Например, платформа поддерживает только [ограниченный набор](https://docs.deno.com/deploy/api/runtime-fs) API файловой системы, доступных в Deno. +Она похожа на другие многочисленные бессерверные платформы, но предназначена для приложений Deno. + +Результат этого руководства [можно увидеть в нашем примере репозитория ботов](https://github.com/grammyjs/examples/tree/main/setups/deno-deploy). + +## Подготовка кода + +> Помните, что вам нужно [запускать бота на вебхукам](../guide/deployment-types#как-использовать-вебхуки), поэтому в коде следует использовать `webhookCallback`, а не вызывать `bot.start()`. + +1. Убедитесь, что у вас есть файл, который экспортирует ваш объект `Bot`, чтобы вы могли импортировать его позже для запуска. +2. Создайте файл с именем `main.ts` или `main.js`, или вообще любым другим именем, которое вам нравится (но вы должны запомнить и использовать его как основной файл для развертывания), со следующим содержимым: + +```ts +import { webhookCallback } from "https://deno.land/x/grammy/mod.ts"; +// Вы можете изменить это, чтобы правильно импортировать свой объект `Bot`. +import bot from "./bot.ts"; + +const handleUpdate = webhookCallback(bot, "std/http"); + +Deno.serve(async (req) => { + if (req.method === "POST") { + const url = new URL(req.url); + if (url.pathname.slice(1) === bot.token) { + try { + return await handleUpdate(req); + } catch (err) { + console.error(err); + } + } + } + return new Response(); +}); +``` + +Мы советуем располагать обработчик не в корне (`/`), а по какому-то секретному пути. +Здесь мы используем токен бота (`/<токен>`). + +## Развертывание + +### Метод 1: через GitHub + +> Это рекомендуемый и самый простой метод. +> Основное преимущество этого метода заключается в том, что Deno Deploy будет следить за изменениями в вашем репозитории, включающем код вашего бота, и автоматически развертывать новые версии. + +1. Создайте репозиторий на GitHub, он может быть как приватным, так и публичным. +2. Разместите свой код. + + > Рекомендуется иметь одну стабильную ветку, а тестирование проводить в других ветках, чтобы не случилось непредвиденных ситуаций. + +3. Зайдите на свою панель [Deno Deploy dashboard](https://dash.deno.com/account/overview). +4. Нажмите на "New Project". +5. Установите подключение на свой аккаунт или организацию и выберите репозиторий. +6. Выберите ветку, которую вы хотите развернуть. +7. Выберите файл начальной точки `main.ts` и нажмите «Deploy Project» для развертывания. + +### Метод 2: с помощью `deployctl` + +> Это метод для более опытных пользователей или если вы не хотите загружать свой код на GitHub. +> Он позволяет развернуть проект через командную строку или GitHub Actions. + +1. Установите [`deployctl`](https://github.com/denoland/deployctl). +2. Создайте токен доступа в разделе «Токены доступа» в [настройках учетной записи](https://dash.deno.com/account). +3. Перейдите в каталог проекта и выполните следующую команду: + + ```sh:no-line-numbers + deployctl deploy --project= --entrypoint=./main.ts --prod --token=<токен> + ``` + + ::: tip Установка переменных окружения + Переменные окружения можно установить, зайдя в настройки проекта после развертывания. + + Но это возможно и из командной строки: + + 1. Вы можете назначить переменные окружения из файла dotenv, добавив аргумент `--env-file=`. + 2. Вы также можете указать их по отдельности, используя аргумент `--env=<ключ=значение>`. + + ::: +4. Чтобы настроить GitHub Actions, обратитесь к [этому README](https://github.com/denoland/deployctl/blob/main/action/README.md). + +Для получения дополнительной информации ознакомьтесь с документацией [deployctl](https://docs.deno.com/deploy/manual/deployctl). + +### Примечание + +После запуска приложения необходимо настроить параметры веб-хука бота так, чтобы он указывал на вашего бота. +Для этого отправьте запрос на + +```sh:no-line-numbers +curl https://api.telegram.org/bot<токен>/setWebhook?url= +``` + +заменив `<токен>` на токен вашего бота, а `` на полный URL вашего приложения вместе с путем к обработчику вебхука. diff --git a/site/docs/ru/hosting/firebase.md b/site/docs/ru/hosting/firebase.md new file mode 100644 index 000000000..04453162d --- /dev/null +++ b/site/docs/ru/hosting/firebase.md @@ -0,0 +1,210 @@ +--- +prev: false +next: false +--- + +# Хостинг: Firebase Functions + +Это руководство проведет вас через процесс развертывания вашего бота на +[Firebase Functions](https://firebase.google.com/docs/functions). + +## Необходимые условия + +Чтобы следить за происходящим, вам необходимо иметь аккаунт Google. Если у вас +его еще нет (ну или вас в нём забанили), вы можете создать его +[здесь](https://accounts.google.com/signup). + +## Установка + +Этот раздел поможет вам пройти процесс настройки. Если вам нужны более подробные +объяснения каждого шага, обратитесь к +[официальной документации Firebase](https://firebase.google.com/docs/functions/get-started). + +### Создание проекта Firebase + +1. Перейдите в консоль [Firebase](https://console.firebase.google.com/) и + нажмите **Добавить проект**. +2. Если появится запрос, просмотрите и примите условия Firebase. +3. Нажмите **Продолжить**. +4. Решите, хотите ли вы делиться аналитикой или нет. +5. Нажмите **Создать проект**. + +### Настройка + +Чтобы написать функции и развернуть их в среде выполнения Firebase Functions, +вам нужно настроить среду Node.js и установить Firebase CLI. + +> Важно отметить, что в настоящее время Firebase Functions поддерживает только +> Node.js версий 14, 16 и 18. Подробнее о поддерживаемых версиях Node.js читайте +> [здесь](https://firebase.google.com/docs/functions/manage-functions#set_nodejs_version). + +После установки Node.js и npm установите Firebase CLI глобально: + +```sh +npm install -g firebase-tools +``` + +### Инициализация проекта + +1. Запустите `firebase login`, чтобы открыть браузер и аутентифицировать + Firebase CLI с помощью вашей учетной записи. +2. Зайдите в директорию проекта `cd`. +3. Запустите `firebase init functions` и введите `y`, когда вас спросят, хотите + ли вы инициализировать новый проект. +4. Выберите `use existing project` и выберите проект, который вы создали в + шаге 1. +5. CLI предлагает вам два варианта поддержки языка: + - JavaScript + - TypeScript +6. В качестве опции можно выбрать ESLint. +7. CLI спросит вас, хотите ли вы установить зависимости с помощью npm. Если вы + используете другой менеджер пакетов, например `yarn` или `pnpm`, вы можете + отказаться. В этом случае вам придется `cd` в директорию `functions` и + установить зависимости вручную. +8. Откройте `./functions/package.json` и найдите ключ: + `"engines": {"node": "16"}`. Версия `node` должна соответствовать + установленной у вас версии Node.js. В противном случае проект может не + запуститься. + +## Подготовка кода + +Вы можете использовать этот короткий пример бота в качестве отправной точки: + +```ts +import * as functions from "firebase-functions"; +import { Bot, webhookCallback } from "grammy"; + +const bot = new Bot(""); + +bot.command( + "start", + (ctx) => ctx.reply("Добро пожаловать! Запущен и работаю."), +); +bot.command("ping", (ctx) => ctx.reply(`Понг! ${new Date()}`)); + +// Во время разработки вы можете запустить свою функцию по адресу https://localhost//us-central1/helloWorld. +export const helloWorld = functions.https.onRequest(webhookCallback(bot)); +``` + +## Локальная разработка + +Во время разработки вы можете использовать набор эмуляторов Firebase для +локального запуска вашего кода. Это гораздо быстрее, чем развертывать каждое +изменение на Firebase. Чтобы установить эмуляторы, выполните команду: + +```sh +firebase init emulators +``` + +Эмулятор функций должен быть уже выбран. (Если это не так, перейдите к нему с +помощью клавиш со стрелками и выберите его с помощью `пробела`). На вопросы о +том, какой порт использовать для каждого эмулятора, просто нажмите `enter`. + +Чтобы запустить эмуляторы и запустить ваш код, используйте: + +```sh +npm run serve +``` + +::: tip По какой-то причине стандартная конфигурация скрипта npm не запускает +компилятор TypeScript в режиме watch. Поэтому, если вы используете TypeScript, +вы также должны запустить: + +```sh +npm run build:watch +``` + +::: + +После запуска эмуляторов вы должны найти в выводе консоли строку, которая +выглядит следующим образом: + +```sh ++ functions[us-central1-helloWorld]: http function initialized (http://127.0.0.1:5001//us-central1/helloWorld). +``` + +Это локальный URL-адрес вашей облачной функции. Однако ваша функция доступна +только для localhost на вашем компьютере. Чтобы протестировать бота, вам нужно +вывести функцию в интернет, чтобы Telegram API мог отправлять обновления вашему +боту. Существует несколько сервисов, таких как +[localtunnel](https://localtunnel.me) или [ngrok](https://ngrok.com), которые +могут помочь вам в этом. В этом примере мы будем использовать localtunnel. + +Сначала давайте установим localtunnel: + +```sh +npm i -g localtunnel +``` + +После этого вы можете переадресовать порт `5001`: + +```sh +lt --port 5001 +``` + +localtunnel должен дать вам уникальный URL, например +`https://modern-heads-sink-80-132-166-120.loca.lt`. + +Осталось только указать Telegram, куда отправлять обновления. Это можно сделать +с помощью вызова `setWebhook`. Например, откройте новую вкладку в браузере и +перейдите по этому URL: + +```text +https://api.telegram.org/bot<токен>/setWebhook?url=<ВЕБХУК_URL>//us-central1/helloWorld +``` + +Замените `<токен>` на ваш настоящий токен бота, а `<ВЕБХУК_URL>` на ваш +собственный URL, который вы получили из localtunnel. + +Теперь вы должны увидеть это в окне браузера. + +```json +{ + "ok": true, + "result": true, + "description": "Webhook was set" +} +``` + +Теперь ваш бот готов к тестированию на развертывание. + +## Развертывание + +Чтобы развернуть свою функцию, просто выполните команду: + +```sh +firebase deploy +``` + +Firebase CLI выдаст вам URL вашей функции после завершения развертывания. Он +должен выглядеть примерно так: +`https://<регион>.<мой_проект.cloudfunctions.net/helloWorld`. Для более +подробного объяснения вы можете взглянуть на шаг 8 руководства +[Начало работы](https://firebase.google.com/docs/functions/get-started#deploy-functions-to-a-production-environment). + +После развертывания вам нужно указать Telegram, куда отправлять обновления для +вашего бота, вызвав метод `setWebhook`. Для этого откройте новую вкладку +браузера и перейдите по этому URL: + +```text +https://api.telegram.org/bot<токен>/setWebhook?url=https://<регион>.<мой_проект>.cloudfunctions.net/helloWorld +``` + +Замените `<токен>` на ваш токен бота, `<регион>` на название региона, в +котором вы развернули свою функцию, а `<мой_проект>` на название вашего проекта +Firebase. Firebase CLI должен предоставить вам полный URL вашей облачной +функции, поэтому вы можете просто вставить его после параметра `?url=` в метод +`setWebhook`. + +Если все настроено правильно, вы должны увидеть этот ответ в окне браузера: + +```json +{ + "ok": true, + "result": true, + "description": "Webhook was set" +} +``` + +Вот и все, ваш бот готов к работе. Заходите в Telegram и смотрите, как он +отвечает на сообщения! diff --git a/site/docs/ru/hosting/fly.md b/site/docs/ru/hosting/fly.md new file mode 100644 index 000000000..215663bfa --- /dev/null +++ b/site/docs/ru/hosting/fly.md @@ -0,0 +1,414 @@ +--- +prev: false +next: false +--- + +# Хостинг: Fly + +Это руководство расскажет вам о том, как вы можете разместить своих ботов grammY на [Fly](https://fly.io), используя Deno или Node.js. + +## Подготовка кода + +Вы можете запустить своего бота, используя оба варианта [вебхуки или long polling](../guide/deployment-types). + +### Вебхуки + +> Помните, что при использовании вебхуков не следует вызывать `bot.start()` в коде. + +1. Убедитесь, что у вас есть файл, который экспортирует ваш объект `Bot`, чтобы вы могли импортировать его позже для запуска. +2. Создайте файл с именем `app.ts` или `app.js`, или вообще любым другим именем, которое вам нравится (но вы должны запомнить и использовать его как основной файл для развертывания), со следующим содержанием: + +::: code-group + +```ts{11} [Deno] +import { webhookCallback } from "https://deno.land/x/grammy/mod.ts"; +// Вы можете изменить это, чтобы правильно импортировать свой объект `Bot`. +import { bot } from "./bot.ts"; + +const port = 8000; +const handleUpdate = webhookCallback(bot, "std/http"); + +Deno.serve({ port }, async (req) => { + const url = new URL(req.url); + if (req.method === "POST" && url.pathname.slice(1) === bot.token) { + try { + return await handleUpdate(req); + } catch (err) { + console.error(err); + } + } + return new Response(); +}); +``` + +```ts{10} [Node.js] +import express from "express"; +import { webhookCallback } from "grammy"; +// Вы можете изменить это, чтобы правильно импортировать свой объект `Bot`. +import { bot } from "./bot"; + +const port = 8000; +const app = express(); + +app.use(express.json()); +app.use(`/${bot.token}`, webhookCallback(bot, "express")); +app.use((_req, res) => res.status(200).send()); + +app.listen(port, () => console.log(`прослушиваю порт ${port}`)); +``` + +::: + +Мы советуем располагать обработчик не в корне (`/`), а на каком-то секретном пути. +Как показано в выделенной строке выше, мы используем токен бота (`/<токен бота>`) в качестве секретного пути. + +### Long Polling + +Создайте файл с именем `app.ts` или `app.js`, или вообще любым другим именем, которое вам нравится (но вы должны запомнить и использовать этот файл как основной для развертывания), со следующим содержимым: + +::: code-group + +```ts{4} [Deno] +import { Bot } from "https://deno.land/x/grammy/mod.ts"; + +const token = Deno.env.get("BOT_TOKEN"); +if (!token) throw new Error("BOT_TOKEN не установлен"); + +const bot = new Bot(token); + +bot.command( + "start", + (ctx) => ctx.reply("Я запущен на хостинге Fly с помощью long polling!"), +); + +Deno.addSignalListener("SIGINT", () => bot.stop()); +Deno.addSignalListener("SIGTERM", () => bot.stop()); + +bot.start(); +``` + +```ts{4} [Node.js] +import { Bot } from "grammy"; + +const token = process.env.BOT_TOKEN; +if (!token) throw new Error("BOT_TOKEN не установлен"); + +const bot = new Bot(token); + +bot.command( + "start", + (ctx) => ctx.reply("Я запущен на хостинге Fly с помощью long polling!"), +); + +process.once("SIGINT", () => bot.stop()); +process.once("SIGTERM", () => bot.stop()); + +bot.start(); +``` + +::: + +Как вы можете видеть в выделенной строке выше, мы берем некоторые чувствительные значения (ваш токен бота) из переменных окружения. +Fly позволяет нам сохранить этот секрет, выполнив эту команду: + +```sh +flyctl secrets set BOT_TOKEN="AAAA:12345" +``` + +Аналогичным образом можно указать и другие секреты. +Более подробную информацию об этих _секретах_ см. по адресу . + +## Развертывание + +### Метод 1: С помощью `flyctl` + +Это самый простой метод. + +1. Установите [flyctl](https://fly.io/docs/flyctl/install/) и [войдите в аккаунт](https://fly.io/docs/getting-started/sign-up-sign-in/). +2. Запустите `flyctl launch` для создания `Dockerfile` и `fly.toml` файлов для развертывания. + Но **НЕ** развертывайте. + + ::: code-group + + ```sh [Deno] + flyctl launch + ``` + + ```log{10} [Log] + Creating app in /my/telegram/bot + Scanning source code + Detected a Deno app + ? App Name (leave blank to use an auto-generated name): grammy + Automatically selected personal organization: CatDestroyer + ? Select region: ams (Amsterdam, Netherlands) + Created app grammy in organization personal + Wrote config file fly.toml + ? Would you like to set up a Postgresql database now? No + ? Would you like to deploy now? No + Your app is ready. Deploy with `flyctl deploy` + ``` + + ::: + + ::: code-group + + ```sh [Node.js] + flyctl launch + ``` + + ```log{12} [Log] + Creating app in /my/telegram/bot + Scanning source code + Detected a NodeJS app + Using the following build configuration: + Builder: heroku/buildpacks:20 + ? App Name (leave blank to use an auto-generated name): grammy + Automatically selected personal organization: CatDestroyer + ? Select region: ams (Amsterdam, Netherlands) + Created app grammy in organization personal + Wrote config file fly.toml + ? Would you like to set up a Postgresql database now? No + ? Would you like to deploy now? No + Your app is ready. Deploy with `flyctl deploy` + ``` + + ::: + +3. **Deno**: Измените версию Deno и удалите `CMD`, если он существует в файле `Dockerfile`. + Например, в данном случае мы обновляем `DENO_VERSION` до `1.25.2`. + + **Node.js**: Чтобы изменить версию Node.js, вам нужно вставить свойство `"node"` внутри свойства `"engines"` в файле `package.json`. + Например, в примере ниже мы обновляем версию Node.js до `16.14.0`. + + ::: code-group + + ```dockerfile{2,26} [Deno] + # Dockerfile + ARG DENO_VERSION=1.25.2 + ARG BIN_IMAGE=denoland/deno:bin-${DENO_VERSION} + FROM ${BIN_IMAGE} AS bin + + FROM frolvlad/alpine-glibc:alpine-3.13 + + RUN apk --no-cache add ca-certificates + + RUN addgroup --gid 1000 deno \ + && adduser --uid 1000 --disabled-password deno --ingroup deno \ + && mkdir /deno-dir/ \ + && chown deno:deno /deno-dir/ + + ENV DENO_DIR /deno-dir/ + ENV DENO_INSTALL_ROOT /usr/local + + ARG DENO_VERSION + ENV DENO_VERSION=${DENO_VERSION} + COPY --from=bin /deno /bin/deno + + WORKDIR /deno-dir + COPY . . + + ENTRYPOINT ["/bin/deno"] + # CMD is removed + ``` + + ```json [Node.js]{19} + // package.json + { + "name": "grammy", + "version": "1.0.0", + "description": "grammy", + "main": "app.js", + "author": "itsmeMario", + "license": "MIT", + "dependencies": { + "express": "^4.18.1", + "grammy": "^1.11.0" + }, + "devDependencies": { + "@types/express": "^4.17.14", + "@types/node": "^18.7.18", + "typescript": "^4.8.3" + }, + "engines": { + "node": "16.14.0" + } + } + ``` + + ::: + +4. Отредактируйте `app` внутри файла `fly.toml`. + Путь `./app.ts` (или `./app.js` для Node.js) в примере ниже относится к директории главного файла. + Вы можете изменить их, чтобы они соответствовали директории вашего проекта. + Если вы используете вебхуки, убедитесь, что порт совпадает с портом в вашей [конфигурации](#вебхуки) (`8000`). + + ::: code-group + + ```toml [Deno (Webhooks)]{7,11,12} + # fly.toml + app = "grammy" + kill_signal = "SIGINT" + kill_timeout = 5 + + [processes] + app = "run --allow-net ./app.ts" + + [[services]] + http_checks = [] + internal_port = 8000 + processes = ["app"] + protocol = "tcp" + script_checks = [] + [services.concurrency] + hard_limit = 25 + soft_limit = 20 + type = "connections" + + [[services.ports]] + force_https = true + handlers = ["http"] + port = 80 + + [[services.ports]] + handlers = ["tls", "http"] + port = 443 + + [[services.tcp_checks]] + grace_period = "1s" + interval = "15s" + restart_limit = 0 + timeout = "2s" + ``` + + ```toml [Deno (Long Polling)]{7} + # fly.toml + app = "grammy" + kill_signal = "SIGINT" + kill_timeout = 5 + + [processes] + app = "run --allow-net ./app.ts" + + # Просто опустите весь раздел [[services]] + # поскольку мы не слушаем HTTP + ``` + + ```toml [Node.js (Webhooks)]{7,11,18,19} + # fly.toml + app = "grammy" + kill_signal = "SIGINT" + kill_timeout = 5 + + [processes] + app = "node ./build/app.js" + + # Настройте переменную окружения NODE_ENV, чтобы убрать предупреждение + [build.args] + NODE_ENV = "production" + + [build] + builder = "heroku/buildpacks:20" + + [[services]] + http_checks = [] + internal_port = 8000 + processes = ["app"] + protocol = "tcp" + script_checks = [] + [services.concurrency] + hard_limit = 25 + soft_limit = 20 + type = "connections" + + [[services.ports]] + force_https = true + handlers = ["http"] + port = 80 + + [[services.ports]] + handlers = ["tls", "http"] + port = 443 + + [[services.tcp_checks]] + grace_period = "1s" + interval = "15s" + restart_limit = 0 + timeout = "2s" + ``` + + ```toml [Node.js (Long polling)]{7,11,22,23} + # fly.toml + app = "grammy" + kill_signal = "SIGINT" + kill_timeout = 5 + + [processes] + app = "node ./build/app.js" + + # Настройте переменную окружения NODE_ENV, чтобы убрать предупреждение + [build.args] + NODE_ENV = "production" + + [build] + builder = "heroku/buildpacks:20" + + # Просто опустите всю секцию [[services]], поскольку мы не слушаем HTTP. + ``` + + ::: + +5. Запустите `flyctl deploy`, чтобы развернуть ваш код. + +### Метод 2: С помощью Github Actions + +Основное преимущество этого метода в том, что Fly будет следить за изменениями в вашем репозитории, включающем код бота, и автоматически разворачивать новые версии. +Посетите для получения более подробных инструкций. + +1. Установите [flyctl](https://fly.io/docs/flyctl/install/) и [войдите в аккаунт](https://fly.io/docs/getting-started/sign-up-sign-in/). +2. Получите токен Fly API, выполнив команду `flyctl auth token`. +3. Создайте репозиторий на GitHub, он может быть как приватным, так и публичным. +4. Перейдите в раздел Settings, выберите Secrets и создайте секрет под названием `FLY_API_TOKEN` со значением токена из шага 2. +5. Создайте файл `.github/workflows/main.yml` с таким содержимым: + + ```yml + name: Fly Deploy + on: [push] + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: flyctl deploy --remote-only + ``` + +6. Выполните шаги со 2 по 4 из [Метода 1](#метод-1-с-помощью-flyctl) выше. + Не забудьте пропустить последний шаг (шаг 5), поскольку мы не будем разворачивать код напрямую. +7. Зафиксируйте изменения и выложите их на GitHub. +8. Вот тут-то и происходит волшебство --- push вызвал развертывание, и с этого момента при каждом изменении приложение будет автоматически развертываться. + +### Настройка URL вебхука + +Если вы используете вебхуки, то после запуска приложения вам необходимо настроить параметры вебхуков бота так, чтобы они указывали на ваше приложение. +Для этого отправьте запрос на + +```text +https://api.telegram.org/bot<токен>/setWebhook?url= +``` + +заменив `<токен>` на токен вашего бота, а `` на полный URL вашего приложения вместе с путем к обработчику вебхука. + +### Оптимизация Dockerfile + +Когда запускается наш `Dockerfile`, он копирует все из каталога в образ Docker. +Для приложений Node.js некоторые каталоги, например `node_modules`, будут перестроены в любом случае, поэтому копировать их не нужно. +Для этого создайте файл `.dockerignore` и добавьте в него `node_modules`. +Вы также можете использовать `.dockerignore`, чтобы не копировать любые другие файлы, которые не нужны во время выполнения. + +## Ссылки + +- +- diff --git a/site/docs/ru/hosting/heroku.md b/site/docs/ru/hosting/heroku.md new file mode 100644 index 000000000..137cc6813 --- /dev/null +++ b/site/docs/ru/hosting/heroku.md @@ -0,0 +1,416 @@ +--- +prev: false +next: false +--- + +# Хостинг: Heroku + +> Мы предполагаем, что у вас есть базовые знания о создании ботов с помощью grammY. +> Если вы еще не готовы, не стесняйтесь заглянуть в наш дружественный [Гайд](../guide/)! :rocket: + +В этом руководстве мы расскажем вам, как развернуть Telegram бота на [Heroku](https://heroku.com/) с помощью [вебхуков](../guide/deployment-types#как-использовать-вебхуки) или [long polling](../guide/deployment-types#как-использовать-long-polling). +Мы также предполагаем, что у вас уже есть аккаунт на Heroku. + +## Необходимые условия + +Сначала установите некоторые зависимости: + +```sh +# Создайте директорию проекта. +mkdir grammy-bot +cd grammy-bot +npm init --y + +# Установите основные зависимости. +npm install grammy express + +# Установите зависимости для разработки. +npm install -D typescript @types/express @types/node + +# Создайте конфиг TypeScript. +npx tsc --init +``` + +Мы будем хранить наши файлы TypeScript в папке `src`, а скомпилированные файлы --- в папке `dist`. +Создайте эти папки в корневом каталоге проекта. +Затем в папке `src` создайте новый файл с именем `bot.ts`. +Теперь наша структура папок должна выглядеть следующим образом: + +```asciiart:no-line-numbers +. +├── node_modules/ +├── dist/ +├── src/ +│ └── bot.ts +├── package.json +├── package-lock.json +└── tsconfig.json +``` + +После этого откройте файл `tsconfig.json` и измените его, чтобы использовать эту конфигурацию: + +```json +{ + "compilerOptions": { + "target": "ESNEXT", + "module": "ESNext", // [!code hl] // переход с commonjs на esnext + "lib": ["ES2021"], + "outDir": "./dist/", + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} +``` + +Поскольку опция `module` выше была установлена с `commonjs` на `esnext`, мы должны добавить `"type": "module"` в наш `package.json`. +Теперь наш `package.json` должен быть похож на этот: + +```json{6} +{ + "name": "grammy-bot", + "version": "0.0.1", + "description": "", + "main": "dist/app.js", + "type": "module", // [!code hl] // добавьте свойство "type": "module" + "scripts": { + "dev-build": "tsc" + }, + "author": "", + "license": "ISC", + "dependencies": { + "grammy": "^1.2.0", + "express": "^4.17.1" + }, + "devDependencies": { + "typescript": "^4.3.5", + "@types/express": "^4.17.13", + "@types/node": "^16.3.1" + }, + "keywords": [] +} +``` + +Как уже говорилось, у нас есть два варианта получения данных из Telegram: вебхуки и long polling. +Вы можете узнать больше о преимуществах обоих вариантов, а затем решить, какой из них подходит, в этих [потрясающих советах](../guide/deployment-types)! + +## Вебхуки + +> Если вы решите использовать long polling вместо этого, вы можете пропустить этот раздел и перейти к [разделу о long polling](#long-polling). :rocket: + +Вкратце, в отличие от long polling, вебхук не будет запускаться постоянно для проверки входящих сообщений от Telegram. +Это снизит нагрузку на сервер и сэкономит нам много [dyno часов](https://devcenter.heroku.com/articles/eco-dyno-hours), особенно если вы используете тарифный план Eco. :grin: + +Хорошо, давайте продолжим! +Помните, мы ранее создали `bot.ts`? +Мы не будем сбрасывать туда весь код и оставим программирование бота на ваше усмотрение. +Вместо этого мы сделаем `app.ts` нашей основной точкой входа. +Это означает, что каждый раз, когда Telegram (или кто-то другой) заходит на наш сайт, `express` решает, какая часть вашего сервера будет отвечать за обработку запроса. +Это полезно, когда вы разворачиваете и сайт, и бота в одном домене. +Кроме того, разделение кодов по разным файлам позволяет сделать наш код более аккуратным. :sparkles: + +### Express и его middleware + +Теперь создайте `app.ts` в папке `src` и напишите в нем этот код: + +```ts +import express from "express"; +import { webhookCallback } from "grammy"; +import { bot } from "./bot.js"; + +const domain = String(process.env.DOMAIN); +const secretPath = String(process.env.BOT_TOKEN); +const app = express(); + +app.use(express.json()); +app.use(`/${secretPath}`, webhookCallback(bot, "express")); + +app.listen(Number(process.env.PORT), async () => { + // Убедитесь, что это `https`, а не `http`! + await bot.api.setWebhook(`https://${domain}/${secretPath}`); +}); +``` + +Давайте посмотрим на наш код выше: + +- `process.env`: Помните, НИКОГДА не храните учетные данные в коде! + Для создания [переменных окружения](https://www.freecodecamp.org/news/using-environment-variables-the-right-way/) в Heroku, перейдите по этому [руководству](https://devcenter.heroku.com/articles/config-vars). +- `secretPath`: Это может быть наш `BOT_TOKEN` или любая произвольная строка. + Лучше всего скрывать путь нашего бота, как [объясняет Telegram](https://core.telegram.org/bots/api#setwebhook). + +::: tip ⚡ Оптимизация (необязательно) +`bot.api.setWebhook` в строке 14 будет запускаться всегда, когда Heroku снова запустит ваш сервер. +Для ботов с низким трафиком это будет происходить при каждом запросе. +Однако нам не нужно, чтобы этот код выполнялся каждый раз, когда приходит запрос. +Поэтому мы можем полностью удалить эту часть и выполнять `GET` только один раз вручную. +Откройте эту ссылку в браузере после развертывания нашего бота: + +```asciiart:no-line-numbers +https://api.telegram.org/bot<токен>/setWebhook?url= +``` + +Обратите внимание, что некоторые браузеры требуют вручную [закодировать](https://en.wikipedia.org/wiki/Percent-encoding#Reserved_characters) `webhook_url` перед передачей. +Например, если у нас есть токен бота `abcd:1234` и URL `https://grammybot.herokuapp.com/secret_path`, то наша ссылка должна выглядеть следующим образом: + +```asciiart:no-line-numbers +https://api.telegram.org/botabcd:1234/setWebhook?url=https%3A%2F%2Fgrammybot.herokuapp.com%2Fsecret_path +``` + +::: + +::: tip ⚡ Оптимизация (необязательно) +Используйте [Webhook Reply](../guide/deployment-types#ответ-вебхука) для большей эффективности. +::: + +### Создание `bot.ts` (вебхуки) + +Следующим шагом перейдите к файлу `bot.ts`: + +```ts +import { Bot } from "grammy"; + +const token = process.env.BOT_TOKEN; +if (!token) throw new Error("BOT_TOKEN не установлен"); + +export const bot = new Bot(token); + +bot.command("start", (ctx) => ctx.reply("Привет!")); +bot.on("message", (ctx) => ctx.reply("Получил сообщение!")); +``` + +Отлично! +Теперь мы закончили написание наших основных файлов. +Но прежде чем перейти к шагам развертывания, мы можем немного оптимизировать нашего бота. +Как обычно, это необязательно. + +::: tip ⚡ Оптимизация (необязательно) +При каждом запуске вашего сервера, grammY будет запрашивать [информацию о боте](https://core.telegram.org/bots/api#getme) у Telegram, чтобы предоставить ее в [объект контекста](../guide/context) в `ctx.me`. +Мы можем заранее обозначить всю [информацию о боте](/ref/core/botconfig#botinfo), чтобы предотвратить чрезмерные вызовы `getMe`. + +1. Откройте эту ссылку `https://api.telegram.org/bot<токен>/getMe` в вашем любимом браузере. Рекомендуем [Firefox](https://www.mozilla.org/en-US/firefox/), так как он хорошо справляется с отображением `JSON`. +2. Измените наш код в строке 4 выше и заполните значение в соответствии с результатами, полученными от `getMe`: + + ```ts + const token = process.env.BOT_TOKEN; + if (!token) throw new Error("BOT_TOKEN не установлен"); + + export const bot = new Bot(token, { + botInfo: { + id: 111111111, + is_bot: true, + first_name: "xxxxxxxxx", + username: "xxxxxxbot", + can_join_groups: true, + can_read_all_group_messages: false, + supports_inline_queries: false, + }, + }); + ``` + +::: + +Круто! +Пришло время подготовить среду развертывания! +Всем прямая дорога в раздел [Развертывания](#развертывание)! :muscle: + +## Long Polling + +::: warning Ваш код будет выполняться непрерывно при использовании long polling +Если вы не знаете, как справиться с таким поведением, убедитесь, что у вас достаточно [dyno часов](https://devcenter.heroku.com/articles/eco-dyno-hours). +::: + +> Рассмотрите возможность использования вебхуков? +> Перейдите к разделу [вебхуки](#вебхуки). :rocket: + +Использование long polling на вашем сервере --- не всегда плохая идея. +Иногда он подходит для ботов, собирающих данные, которым не нужно быстро реагировать и обрабатывать большое количество данных. +Если вы хотите делать это раз в час, вы можете легко это сделать. +Это то, что вы не можете контролировать с помощью вебхуков. +Если ваш бот будет переполнен сообщениями, вы увидите много запросов вебхука, однако вы можете более легко ограничить скорость обработки обновлений с помощью long polling. + +### Создание `bot.ts` (Long Polling) + +Откроем файл `bot.ts`, который мы создали ранее. +Пусть он содержит следующие строки кода: + +```ts +import { Bot } from "grammy"; + +const token = process.env.BOT_TOKEN; +if (!token) throw new Error("BOT_TOKEN не установлен"); + +const bot = new Bot(token); + +bot.command( + "start", + (ctx) => ctx.reply("Я работаю на Heroku, используя long polling!"), +); + +bot.start(); +``` + +Вот и все! +Мы готовы к развертыванию. +Довольно просто, правда? :smiley: +Если вам кажется, что это слишком просто, посмотрите наши [советы по развертыванию](../advanced/deployment#long-polling)! :rocket: + +## Развертывание + +Нет... наш _Шикарный бот_ еще не готов к запуску. +Сначала завершите эти этапы! + +### Компиляция файлов + +Запустите этот код в терминале, чтобы скомпилировать файлы TypeScript в JavaScript: + +```sh +npx tsc +``` + +Если он запустится успешно и не выдаст никаких ошибок, наши скомпилированные файлы должны оказаться в папке `dist` с расширением `.js`. + +### Установите `Procfile` + +На данный момент у `Heroku` есть несколько [типов dyno](https://devcenter.heroku.com/articles/dyno-types). +Два из них: + +- **Web dynos**: + + _Web dynos_ --- это dynos процесса "web", которые получают HTTP-трафик от роутеров. + Этот вид дино имеет таймаут в 30 секунд на выполнение кода. + Кроме того, он отключится, если в течение 30 минут не будет обработано ни одного запроса. + Этот тип дино вполне подходит для _вебхуков_. + +- **Worker dynos** + + _Worker dynos_ обычно используются для фоновых заданий. + У них нет таймаута, и они НЕ отключатся, если не обработают никаких запросов. + Он подходит для _long polling_. + +Создайте файл с именем `Procfile` без расширения в корневой директории нашего проекта. +Например, `Procfile.txt` и `procfile` не подходят. +Затем напишите код в формате одной строки: + +```procfile +<тип dynos>: <наш главный файл> +``` + +Для нашего случая должно быть так: + +::: code-group + +```procfile [Webhook] +web: node dist/app.js +``` + +```procfile [Long Polling] +worker: node dist/bot.js +``` + +::: + +### Установка Git + +Мы собираемся развернуть нашего бота с помощью [Git и Heroku Cli](https://devcenter.heroku.com/articles/git). +Вот ссылка для установки: + +- [Инструкция по установке Git](https://git-scm.com/download) +- [Инструкция по установке Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli#install-the-heroku-cli) + +Предположим, что они уже установлены на вашем компьютере, и у вас открыт терминал в корневой директории нашего проекта. +Теперь инициализируйте локальный git-репозиторий, выполнив этот код в терминале: + +```sh +git init +``` + +Далее нам нужно предотвратить попадание ненужных файлов на наш рабочий сервер, в данном случае `Heroku`. +Создайте файл с именем `.gitignore` в корневой директории нашего проекта. +Затем добавьте в него этот список: + +```text +node_modules/ +src/ +tsconfig.json +``` + +Теперь наша окончательная структура папок должна выглядеть следующим образом: + +::: code-group + +```asciiart:no-line-numbers [Вебхук] +. +├── .git/ +├── node_modules/ +├── dist/ +│ ├── bot.js +│ └── app.js +├── src/ +│ ├── bot.ts +│ └── app.ts +├── package.json +├── package-lock.json +├── tsconfig.json +├── Procfile +└── .gitignore +``` + +```asciiart:no-line-numbers [Long Polling] +. +├── .git/ +├── node_modules/ +├── dist/ +│ └── bot.js +├── src/ +│ └── bot.ts +├── package.json +├── package-lock.json +├── tsconfig.json +├── Procfile +└── .gitignore +``` + +::: + +Зафиксируйте файлы в нашем git-репозитории: + +```sh +git add . +git commit -m "My first commit" +``` + +### Настройте удаленный доступ к Heroku + +Если вы уже создали [Heroku app](https://dashboard.heroku.com/apps/), передайте имя вашего `Existing app` в `` ниже, затем запустите код. +В противном случае запустите `New app`. + +::: code-group + +```sh [New app] +heroku create +git remote -v +``` + +```sh [Existing app] +heroku git:remote -a +``` + +::: + +### Развертывание кода + +Наконец, нажмите _красную кнопку_ и взлетайте! :rocket: + +```sh +git push heroku main +``` + +Если это не сработает, то, скорее всего, наша ветка git не `main`, а `master`. +Вместо этого нажмите эту _синюю кнопку_: + +```sh +git push heroku master +``` diff --git a/site/docs/ru/hosting/supabase.md b/site/docs/ru/hosting/supabase.md new file mode 100644 index 000000000..b5a2ade44 --- /dev/null +++ b/site/docs/ru/hosting/supabase.md @@ -0,0 +1,119 @@ +--- +prev: false +next: false +--- + +# Хостинг: Supabase Edge Functions + +В этом руководстве рассказывается о том, как разместить своих ботов grammY на +[Supabase](https://supabase.com/). + +Обратите внимание, что перед использованием +[Supabase Edge Functions](https://supabase.com/docs/guides/functions/quickstart) +вам необходимо иметь аккаунт на [GitHub](https://github.com). Более того, +Supabase Edge Functions основаны на [Deno Deploy](https://deno.com/deploy), +поэтому, как и [наше руководство по Deno Deploy](./deno-deploy), это руководство +предназначено только для пользователей Deno для grammY. + +Supabase Edge Functions идеально подходит для большинства простых ботов, при +этом следует учитывать, что не все функции Deno доступны для приложений, +работающих на Supabase Edge Functions. Например, на Supabase Edge Functions нет +файловой системы. Она такая же, как и на многих других бессерверных платформах, +но предназначена для приложений Deno. + +Результат этого урока +[можно увидеть в нашем репозитории примеров ботов](https://github.com/grammyjs/examples/tree/main/setups/supabase-edge-functions). + +## Установка + +Чтобы развернуть Supabase Edge Function, вам нужно создать учетную запись +Supabase, установить их CLI и создать проект Supabase. Сначала вам следует +[следовать их документации](https://supabase.com/docs/guides/functions/quickstart#initialize-a-project), +чтобы все настроить. + +Создайте новую функцию Supabase Function, выполнив следующую команду: + +```sh +supabase functions new telegram-bot +``` + +Создав проект Supabase Function, вы можете написать своего бота. + +## Настройка + +> Помните, что вам нужно +> [запускать бота на вебхуках](../guide/deployment-types#как-использовать-вебхуки), +> поэтому в коде следует использовать `webhookCallback`, а не вызывать +> `bot.start()`. + +Вы можете использовать этот короткий пример бота в качестве отправной точки. + +```ts +import { Bot, webhookCallback } from "https://deno.land/x/grammy/mod.ts"; + +const token = Deno.env.get("BOT_TOKEN"); +if (!token) throw new Error("BOT_TOKEN не установлен"); + +const bot = new Bot(token); + +bot.command( + "start", + (ctx) => ctx.reply("Добро пожаловать! Запущен и работаю."), +); +bot.command("ping", (ctx) => ctx.reply(`Понг! ${new Date()}`)); + +const handleUpdate = webhookCallback(bot, "std/http"); + +Deno.serve(async (req) => { + try { + const url = new URL(req.url); + if (url.searchParams.get("secret") !== bot.token) { + return new Response("not allowed", { status: 405 }); + } + return await handleUpdate(req); + } catch (err) { + console.error(err); + } + return new Response(); +}); +``` + +## Развертывание + +Теперь вы можете развернуть своего бота на Supabase. Обратите внимание, что вам +придется отключить JWT-авторизацию, поскольку Telegram использует другой способ +убедиться, что запросы поступают от Telegram. Вы можете развернуть функцию с +помощью этой команды. + +```sh +supabase functions deploy --no-verify-jwt telegram-bot +``` + +Далее необходимо передать токен бота в Supabase, чтобы ваш код имел к нему +доступ как к переменной окружения. + +```sh +# Замените 123:aBcDeF-gh на свой настоящий токен бота. +supabase secrets set BOT_TOKEN=123:aBcDeF-gh +``` + +Ваш Supabase Function теперь работает. Осталось только указать Telegram, куда +отправлять обновления. Это можно сделать с помощью вызова `setWebhook`. +Например, откройте новую вкладку в браузере и перейдите по этому URL: + +```text +https://api.telegram.org/bot<токен>/setWebhook?url=https://.supabase.co/functions/v1/telegram-bot?secret=<токен> +``` + +Замените `<токен>` на ваш настоящий токен бота. Также замените второй +`<токен>` на ваш реальный токен бота. Замените `` на +идентификатор ссылки вашего проекта Supabase. + +Теперь вы должны увидеть это в окне браузера. + +```json +{ "ok": true, "result": true, "description": "Webhook was set" } +``` + +Готово! Теперь ваш бот работает. Перейдите в Telegram и посмотрите, как он +отвечает на сообщения! diff --git a/site/docs/ru/hosting/vercel.md b/site/docs/ru/hosting/vercel.md new file mode 100644 index 000000000..9c8bb0604 --- /dev/null +++ b/site/docs/ru/hosting/vercel.md @@ -0,0 +1,135 @@ +--- +prev: false +next: false +--- + +# Хостинг: Vercel Serverless Functions + +В этом руководстве вы узнаете, как развернуть своего бота на [Vercel](https://vercel.com/) с помощью [Vercel Serverless Functions](https://vercel.com/docs/functions), предполагая, что у вас уже есть аккаунт [Vercel](https://vercel.com). + +## Структура проекта + +Единственным условием для начала работы с **Vercel Serverless Functions** является перемещение вашего кода в директорию `api/`, как показано ниже. +Вы также можете посмотреть [документацию Vercel](https://vercel.com/docs/functions/quickstart), чтобы узнать больше об этом. + +```asciiart:no-line-numbers +. +├── node_modules/ +├── build/ +├── api/ +│ └── bot.ts +├── package.json +├── package-lock.json +└── tsconfig.json +``` + +Если вы используете TypeScript, вы можете установить `@vercel/node` в качестве dev-зависимости, но это не обязательно для выполнения данного руководства. + +## Настройка Vercel + +Следующим шагом будет создание файла `vercel.json` на верхнем уровне вашего проекта. +Для нашего примера структуры его содержимое будет таким: + +```json +{ + "functions": { + "api/bot.ts": { + "memory": 1024, + "maxDuration": 10 + } + } +} +``` + +> Если вы хотите использовать бесплатную подписку Vercel, ваши конфигурации `memory` и `maxDuration` могут выглядеть так, как указано выше, чтобы не обходить ограничения. + +Если вы хотите узнать больше о конфигурационном файле `vercel.json`, смотрите [его документацию](https://vercel.com/docs/projects/project-configuration). + +## Настройка TypeScript + +В нашем `tsconfig.json` мы должны указать выходной каталог как `build/`, а корневой каталог как `api/`. +Это важно, поскольку мы будем указывать их в опциях развертывания Vercel. + +```json{5,8} +{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "rootDir": "./api", + "moduleResolution": "node", + "resolveJsonModule": true, + "outDir": "./build", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + } +} +``` + +## Главный файл + +Независимо от использования TypeScript или JavaScript, у нас должен быть исходный файл, через который запускается наш бот. +Он должен выглядеть примерно так: + +```ts +import { Bot, webhookCallback } from "grammy"; + +const token = process.env.BOT_TOKEN; +if (!token) throw new Error("BOT_TOKEN не установлен"); + +const bot = new Bot(token); + +export default webhookCallback(bot, "std/http"); +``` + +::: tip [Vercel Edge Functions](https://vercel.com/docs/functions) обеспечивает ограниченную поддержку grammY +Вы все еще можете использовать основной пакет grammY и ряд плагинов, но другие могут быть несовместимы из-за зависимостей только от Node.js, которые могут не поддерживаться Vercel [Edge Runtime](https://edge-runtime.vercel.app). + +В настоящее время у нас нет полного списка совместимых плагинов, поэтому вам нужно проверить это самостоятельно. + +Добавьте эту строку к приведенному выше сниппету, если вы хотите перейти на Edge Functions: + +```ts +export const config = { + runtime: "edge", +}; +``` + +::: + +## В Vercel Dashboard + +Предполагая, что у вас уже есть аккаунт Vercel, к которому подключен GitHub, добавьте новый проект и выберите репозиторий вашего бота. +В разделе _Build & Development Settings_: + +- Output directory: `build` +- Install command: `npm install` + +Не забудьте добавить секреты, такие как токен вашего бота, в качестве переменных окружения в настройках. +Как только вы это сделаете, вы сможете развернуть его! + +## Настройка вебхука + +Последний шаг --- подключение приложения Vercel к Telegram. +Измените приведенный ниже URL на свои учетные данные и зайдите на него через браузер: + +```text +https://api.telegram.org/bot<токен>/setWebhook?url= +``` + +С `URL_ХОСТА` все немного сложнее, потому что вам нужно использовать ваш **домен приложения Vercel с маршрутом к коду бота**, например `https://appname.vercel.app/api/bot`. +Где `bot` ссылается на ваш файл `bot.ts` или `bot.js`. + +После этого вы увидите следующий ответ: + +```json +{ + "ok": true, + "result": true, + "description": "Webhook was set" +} +``` + +Поздравляем! +Теперь ваш бот должен быть запущен. diff --git a/site/docs/ru/hosting/vps.md b/site/docs/ru/hosting/vps.md new file mode 100644 index 000000000..ebef4381f --- /dev/null +++ b/site/docs/ru/hosting/vps.md @@ -0,0 +1,782 @@ +--- +prev: false +next: false +--- + + + +# Хостинг: VPS + +Виртуальный частный сервер, чаще всего называемый VPS, представляет собой виртуальную машину, работающую в облаке, где вы, разработчик, имеете полный контроль над системой. + +## Аренда сервера + +> Чтобы иметь возможность следовать этому руководству, вам сначала нужно арендовать VPS. +> В этом разделе мы расскажем, как это сделать. +> Если у вас уже есть VPS для работы, переходите к [следующему разделу](#запуск-бота). + +В этом руководстве мы будем использовать услуги [Hostinger](https://hostinger.com). + +> Вы можете выбрать провайдера по своему усмотрению. +> Все провайдеры предоставляют одинаковые услуги, поэтому у вас не возникнет проблем с технической частью этой статьи. +> Вы можете воспринимать эту часть как обзор того, как работает аренда сервера. +> Если вы новичок, вы можете использовать это руководство для аренды своего первого сервера! + +::: tip Аналог сервера +Если вы не можете или не хотите арендовать сервер, но при этом хотите поиграть с запуском бота на VPS, вы можете выполнить это руководство на виртуальной машине. +Для этого воспользуйтесь таким приложением, как [VirtualBox](https://virtualbox.org). +Создайте виртуальную машину с нужным дистрибутивом Linux, чтобы имитировать сервер Linux. +::: + +Перейдите на страницу [VPS-Хостинг](https://hostinger.com/vps-hosting). +Мы будем использовать тарифный план "KVM 1". +Ресурсов "KVM 1" достаточно для ботов с большой аудиторией, а тем более для нашего тестового бота. + +Нажмите кнопку "Add to cart". +Вы будете автоматически перенаправлены на страницу оформления заказа, где также сразу зарегистрируетесь на Hostinger. + +::: warning Измените срок аренды! +Типичный срок аренды --- 1-2 года (маркетинговая уловка), и это стоит больших денег. +Скорее всего, вам это не нужно, поэтому для начала можно арендовать сервер на месяц, что гораздо дешевле. + +В любом случае, Hostinger предоставляет 30-дневную гарантию возврата денег. +::: + +После оплаты вы сможете настроить свой сервер: + +1. **Местоположение.** + Мы рекомендуем вам [выбрать место](../guide/api#выбор-места-расположения-дата-центра), ближайшее к Амстердаму. + Главный сервер Bot API расположен в Амстердаме. + Если вы используете [собственный сервер Bot API](../guide/api#запуск-локального-api-сервера-бота), выберите вместо Амстердама, ближайшую к нему локацию. +2. **Тип сервера.** + Выберите вариант "Clean OS." +3. **Операционная система.** + Мы будем использовать Ubuntu 22.04. + Если вы выберете другую систему, некоторые шаги могут отличаться, поэтому будьте внимательны. +4. **Имя сервера.** + Выберите любое имя, которое вам нравится. +5. **Пароль рута.** + Придумайте надежный пароль и храните его в надежном месте! +6. **SSH-ключ**. + Пропустите этот шаг. + Мы настроим SSH-ключи [позже](#ssh-ключи). + +После создания сервера вы можете подключиться к нему с помощью SSH: + +> SSH (_Secure Shell_) --- это сетевой протокол, который можно использовать для удаленного управления компьютером. + +```sh +ssh root@ +``` + +Замените `` на IP адрес вашего сервера, который вы можете найти на странице управления сервером. + +::: tip Настройка SSH +Запоминать, какой IP адрес и чье имя необходимо для подключения к серверу, может быть сложно и утомительно. +Чтобы избавиться от этих рутинных действий и улучшить работу с сервером, вы можете настроить SSH, создав на своем компьютере файл `~/.ssh/config` (), в котором под определенными произвольными идентификаторами будут храниться все данные, необходимые для подключения к серверу. +Это выходит за рамки данной статьи, поэтому вам придется настраивать его самостоятельно. +::: + +::: tip Отдельный пользователь для каждого приложения +В этом руководстве все действия с сервером будут выполняться от имени пользователя root. +Это сделано специально, чтобы упростить данное руководство. +Однако в реальности root пользователь должен отвечать только за общие службы (веб-сервер, база данных и т. д.), а приложения должны запускаться отдельными пользователями, не являющимися root пользователями. +Такой подход обеспечивает безопасность конфиденциальных данных и предотвращает взлом всей системы. +В то же время он накладывает некоторые неудобства. +Описание всех этих моментов излишне увеличивает сложность статьи, чего мы стараемся избегать. +::: + +## Запуск бота + +Теперь в нашем распоряжении есть сервер, на котором мы можем запустить бота, чтобы он работал круглосуточно. + +Чтобы упростить начало статьи, мы пропустили шаг автоматической доставки кода на сервер каждый раз после размещения вашего кода, но он описан [ниже](#ci-cd). + +Пока же вы можете скопировать локальные файлы на удаленный сервер с помощью следующей команды. +Обратите внимание, что `-r` копирует рекурсивно, поэтому вам нужно указать только корневой каталог вашего проекта: + +```sh +scp -r <путь-до-локальной-директории-с-ботом> root@:<путь-до-удалённой-директории> +``` + +Замените `<путь-до-локальной-директории-с-ботом>` на путь к директории проекта на вашем локальном диске, `` на IP адрес вашего сервера, а `<путь-до-удалённой-директории>` на путь к директории, где на сервере должен храниться исходный код бота. + +Как уже говорилось выше, теперь вы можете открыть удаленный терминал на вашем VPS, запустив сессию SSH. + +```sh +ssh root@ +``` + +Обратите внимание, как изменилась командная строка. +Это означает, что вы теперь подключены к удаленной машине. +Каждая введенная вами команда будет выполняться на вашем VPS. +Попробуйте запустить `ls`, чтобы убедиться, что вы успешно скопировали исходные файлы. + +В оставшейся части этой страницы предполагается, что вы можете подключиться к своему VPS. +Все следующие команды должны быть запущены в сессии SSH. + +:::tip Не забудьте установить среду выполнения! +Чтобы запустить бота, вам нужно установить на сервер Node.js или Deno, в зависимости от среды выполнения, в которой будет работать бот. +Это выходит за рамки данной статьи, поэтому вам придется сделать это самостоятельно. +Вероятно, вы уже делали это при [начале работы](../guide/getting-started), поэтому вам должны быть знакомы эти шаги :wink: +::: + +Ниже приведены два способа поддержания бесперебойной работы бота: использование [systemd](#systemd) или [PM2](#pm2). + +### systemd + +systemd --- это мощный менеджер служб, который предустановлен во многих дистрибутивах Linux, в основном на базе Debian, таких как Ubuntu. + +#### Создание команды для запуска + +1. Получите абсолютный путь к вашей среде выполнения: + + ::: code-group + + ```sh [Deno] + which deno + ``` + + ```sh [Node.js] + which node + ``` + + ::: + +2. У вас должен быть абсолютный путь к директории вашего бота. + +3. Ваша команда запуска должна выглядеть следующим образом: + + ```sh + <путь_к_среде_выполнения> <параметры> <абсолютный_путь_к_фалу_запуска> + + # Путь к директории бота: /home/user/bot1/ + + # Deno пример: + # /home/user/.deno/bin/deno --allow-all run mod.ts + + # Node.js пример: + # /home/user/.nvm/versions/node/v16.9.1/bin/node index.js + ``` + +#### Создание службы + +1. Перейдите в каталог служб: + + ```sh + cd /etc/systemd/system + ``` + +2. Откройте новый служебный файл в редакторе: + + ```sh + nano .service + ``` + + > Замените `` на любой идентификатор. + > `.service` будет именем вашего сервиса. + +3. Добавьте следующее содержание: + + ```text + [Unit] + After=network.target + + [Service] + WorkingDirectory=<путь_до_директории_бота> + ExecStart=<команда_для_запуска> + Restart=on-failure + + [Install] + WantedBy=multi-user.target + ``` + + Замените `<путь_до_директории_бота>` на абсолютный путь к директории вашего бота, а `<команда_для_запуска>` на команду, которую вы получили [выше](#создание-команды-для-запуска). + + Вот краткое объяснение конфигурации сервиса: + + - `After=network.target` --- указывает, что приложение должно быть запущено после загрузки модуля Internet. + - `WorkingDirectory=<путь_до_директории_бота>` --- задает текущий рабочий каталог процесса. + Это позволяет использовать относительные ресурсы, такие как файл `.env`, который содержит все необходимые переменные окружения. + - `ExecStart=<команда_для_запуска>` --- задает команду запуска. + - `Restart=on-failure` --- указывает, что приложение должно перезапускаться после сбоя. + - `WantedBy=multi-user.target` --- определяет состояние системы, в котором должна быть запущена служба. + `multi-user.target` --- типичное значение для серверов. + + > Для получения дополнительной информации о файлах системы читайте [this](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/using_systemd_unit_files_to_customize_and_optimize_your_system/assembly_working-with-systemd-unit-files_working-with-systemd). + +4. Перезагружайте systemd при каждом изменении службы: + + ```sh + systemctl daemon-reload + ``` + +#### Управление службой + +```sh +# Замените `<название-службы>` на имя файла созданной вами службы. + +# Чтобы запустить службу +systemctl start <название-службы> + +# Чтобы просмотреть служебные логи +journalctl -u <название-службы> + +# Чтобы перезапустить службу +systemctl restart <название-службы> + +# Чтобы остановить службу +systemctl stop <название-службы> + +# Чтобы включить запуск службы при загрузке сервера +systemctl enable <название-службы> + +# Чтобы отключить запуск службы при загрузке сервера +systemctl disable <название-службы> +``` + +Запуск службы должен запустить вашего бота! + +### PM2 + +[PM2](https://pm2.keymetrics.io) --- это daemon-менеджер процессов для Node.js, который поможет вам управлять и поддерживать работу вашего приложения в режиме 24/7. + +PM2 разработан специально для управления приложениями, написанными на Node.js. +Однако его можно использовать и для управления приложениями, написанными на других языках или средах исполнения. + +#### Установка + +::: code-group + +```sh [NPM] +npm install -g pm2 +``` + +```sh [Yarn] +yarn global add pm2 +``` + +```sh [pnpm] +pnpm add -g pm2 +``` + +::: + +#### Создание приложения + +PM2 предлагает два способа создания приложения: + +1. Использовать интерфейс командной строки. +2. Использовать [конфигурационный файл](https://pm2.keymetrics.io/docs/usage/application-declaration). + +Первый способ удобен при знакомстве с PM2. +Однако при развертывании следует использовать второй метод, что мы и сделали в нашем случае. + +Создайте на сервере в директории, где хранится сборка бота, файл `ecosystem.config.js` со следующим содержанием: + +```js +module.exports = { + apps: [ + { + name: "<название-приложения>", + script: "<команда-для-запуска>", + }, + ], +}; +``` + +Замените `<название-приложения>` на любой идентификатор, а `<команда-для-запуска>` --- на команду для запуска бота. + +#### Управление приложением + +Ниже перечислены команды, которые можно использовать для управления приложением. + +```sh +# Если файл `ecosystem.config.js` находится в текущем каталоге, +# вы можете ничего не указывать для запуска приложения. +# Если приложение уже запущено, эта команда перезапустит его. +pm2 start + +# Все следующие команды требуют указания имени приложения +# или файл `ecosystem.config.js`. +# Чтобы применить действие ко всем приложениям, укажите `all`. + +# Чтобы перезапустить приложение +pm2 restart + +# Чтобы перезагрузить приложение +pm2 reload + +# Чтобы остановить приложение +pm2 stop + +# Чтобы удалить приложение +pm2 delete +``` + +#### Сохранение операций приложения + +Если сервер перезагрузится, ваш бот не возобновит работу. +Чтобы бот возобновил работу, необходимо подготовить PM2 к этому. + +На сервере в терминале выполните следующую команду: + +```sh +pm2 startup +``` + +Вам будет предложена команда, которую нужно выполнить, чтобы PM2 автоматически запускался после перезагрузки сервера. + +Затем выполните еще одну команду: + +```sh +pm2 save +``` + +Эта команда сохранит список текущих приложений, чтобы их можно было запустить после перезагрузки сервера. + +Если вы создали новое приложение и хотите сохранить и его, просто запустите `pm2 save` снова. + +## Запуск бота на вебхуках + +Чтобы запустить бота на вебхуках, вам нужно использовать веб-фреймворк и **НЕ** вызывать `bot.start()`. + +Вот пример кода для запуска бота по вебхукам, который нужно добавить в основной файл бота: + +::: code-group + +```ts [Node.js] +import { webhookCallback } from "grammy"; +import { fastify } from "fastify"; + +const server = fastify(); + +server.post(`/${bot.token}`, webhookCallback(bot, "fastify")); + +server.listen(); +``` + +```ts [Deno] +import { webhookCallback } from "https://deno.land/x/grammy/mod.ts"; + +const handleUpdate = webhookCallback(bot, "std/http"); + +Deno.serve(async (req) => { + if (req.method === "POST") { + const url = new URL(req.url); + if (url.pathname.slice(1) === bot.token) { + try { + return await handleUpdate(req); + } catch (err) { + console.error(err); + } + } + } + return new Response(); +}); +``` + +::: + +### Аренда домена + +Чтобы подключить бота, работающего на вебхуках, к внешнему миру, вам нужно приобрести домен. +Мы будем объяснять это на примере Hostinger, но есть и множество других сервисов, и все они работают аналогично. + +Перейдите на [страницу поиска доменного имени](https://www.hostinger.com/domain-name-search). +В поле ввода текста введите доменное имя вида `<имя>.<доменная зона>`. +Например, `example.com`. + +Если нужный домен свободен, нажмите кнопку `Add` рядом с ним. +Вы будете автоматически перенаправлены на страницу оформления заказа, где вы также сразу зарегистрируетесь в Hostinger, если вы еще не зарегистрированы. +Оплатите домен. + +#### Домен, указывающий на VPS + +Прежде чем ваш домен сможет работать с вашим VPS, вам необходимо указать домен на ваш сервер. +Для этого в [Панели управления Hostinger](https://hpanel.hostinger.com) нажмите кнопку "Manage" рядом с вашим доменом. +Затем перейдите на страницу управления DNS-записями, нажав на кнопку "DNS / Name Servers" в меню слева. + +> Сначала узнайте IP адрес вашего VPS. + +В списке записей DNS найдите запись типа `A` с именем `@`. +Отредактируйте эту запись, изменив IP адрес в поле "Points to" на IP адрес вашего VPS, и установите TTL на 3600. + +Затем найдите и удалите запись типа `CNAME` с именем `www`. +Вместо нее создайте новую запись типа `A` с именем `www`, указывающую на IP адрес вашего VPS, и установите TTL на 3600. + +> Если у вас возникнут проблемы, воспользуйтесь другим методом, описанным в [базе знаний](https://support.hostinger.com/en/articles/1583227-how-to-point-a-domain-to-your-vps). + +### Настройка веб сервера + +Чтобы сайт заработал и бот начал получать обновления от Telegram, необходимо настроить веб-сервер. +Мы будем использовать [Caddy](https://caddyserver.com). + +Caddy --- это мощный веб-сервер с открытым исходным кодом и автоматическим HTTPS. + +::: tip Веб сервер +Мы используем Caddy, потому что, в отличие от обычных веб-серверов, таких как Nginx или Apache, он автоматически настраивает SSL-сертификаты. +Это значительно упрощает работу над статьей. +Однако вы можете выбрать любой веб-сервер. +::: + +#### Установка + +Следующие пять команд загрузят и автоматически запустят Caddy как службу systemd под названием `caddy`. + +```sh +apt install -y debian-keyring debian-archive-keyring apt-transport-https curl +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list +apt update +apt install caddy +``` + +> Другие варианты установки см. в [Руководстве по установке Caddy](https://caddyserver.com/docs/install). + +Проверьте состояние Caddy: + +```sh +systemctl status caddy +``` + +::: details Устранение неполадок +Некоторые хостинг-провайдеры предоставляют VPS с предустановленным веб-сервером, например [Apache](https://httpd.apache.org). +Несколько веб-серверов не могут работать на одной машине одновременно. +Для работы Caddy необходимо остановить и выключить другой веб-сервер: + +```sh +systemctl stop <имя-службы> +systemctl disable <имя-службы> +``` + +Замените `имя-службы` на имя службы веб-сервера, которая мешает работе Caddy. + +::: + +Теперь, если вы откроете IP адрес вашего сервера в браузере, вы увидите типичную страницу с инструкциями по настройке Caddy. + +#### Настройка + +Чтобы Caddy мог обрабатывать запросы, поступающие в наш домен, нам нужно изменить конфигурацию Caddy. + +Выполните следующую команду, чтобы открыть файл конфигурации Caddy: + +```sh +nano /etc/caddy/Caddyfile +``` + +Вы увидите следующую конфигурацию по умолчанию: + +```text +# The Caddyfile is an easy way to configure your Caddy web server. +# +# Unless the file starts with a global options block, the first +# uncommented line is always the address of your site. +# +# To use your own domain name (with automatic HTTPS), first make +# sure your domain's A/AAAA DNS records are properly pointed to +# this machine's public IP, then replace ":80" below with your +# domain name. + +:80 { + # Set this path to your site's directory. + root * /usr/share/caddy + + # Enable the static file server. + file_server + + # Another common task is to set up a reverse proxy: + # reverse_proxy localhost:8080 + + # Or serve a PHP site through php-fpm: + # php_fastcgi localhost:9000 +} + +# Refer to the Caddy docs for more information: +# https://caddyserver.com/docs/caddyfile +``` + +Чтобы бот работал, сделайте конфигурацию примерно такой: + +```text +<домен> { + reverse_proxy /<токен> localhost:<порт> +} +``` + +Замените `<домен>` на ваш домен, `<токен>` на токен вашего бота, а `<порт>` на порт, на котором вы хотите запустить своего бота. + +Перезагружайте Caddy каждый раз, когда вы изменяете конфигурационный файл сайта, используя следующую команду: + +```sh +systemctl reload caddy +``` + +Теперь все запросы по адресу `https://<домен>/<токен>` будут перенаправляться на адрес `http://localhost:<порт>/<токен>`, где запущен вебхук бота. + +#### Подключение вебхука к Telegram + +Все, что вам нужно сделать --- это указать Telegram, куда отправлять обновления. +Для этого откройте браузер и перейдите на страницу по следующей ссылке: + +```text +https://api.telegram.org/bot<токен>/setWebhook?url=https://<домен>/<токен> +``` + +Замените `<токен>` на токен вашего бота, а `<домен>` на ваш домен. + +## CI/CD + +[CI/CD](https://about.gitlab.com/topics/ci-cd) --- важная часть современного процесса разработки программного обеспечения. +Это руководство охватывает практически весь конвейер [CI/CD](https://about.gitlab.com/topics/ci-cd/cicd-pipeline). + +Мы сосредоточимся на написании скриптов для GitHub и GitLab. +При необходимости вы можете легко адаптировать приведенные ниже примеры к выбранному вами сервису CI/CD, например Jenkins, Buddy и т.д. + +### SSH Ключи + +Для передачи файлов на сервер необходимо настроить беспарольную аутентификацию, которая осуществляется с помощью SSH-ключей. + +На вашем персональном компьютере необходимо выполнить следующие команды. + +Перейдите в каталог с ключами SSH: + +```sh +cd ~/.ssh +``` + +Сгенерируйте новую пару ключей: + +::: code-group + +```sh [GitHub] +ssh-keygen -t rsa -m PEM +``` + +```sh [GitLab] +ssh-keygen -t ed25519 +``` + +::: + +Эта команда сгенерирует открытый и закрытый ключ нужного вам типа и формата для GitHub и GitLab. +При желании вы можете указать собственное имя ключа. + +Затем отправьте **публичный** ключ на сервер: + +```sh +ssh-copy-id -i <имя-ключа>.pub root@ +``` + +Замените `<имя-ключа>` на имя сгенерированного ключа, а `` на IP-адрес вашего сервера. + +Обратите внимание, что **публичный** ключ может находиться на многих серверах, а **приватный** ключ должен быть только у вас и GitHub или GitLab. + +Теперь вы можете подключиться к серверу без необходимости вводить пароль. + +### Примеры Workflow + +#### Node.js (GitHub) + +Используйте + +```yml +name: Main + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-node@v3 + with: + node-version: "latest" + - run: npm ci + - name: Build + run: npm run build + - uses: actions/upload-artifact@v3 + with: + name: source + path: | + dist/*.js + package.json + package-lock.json + deploy: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v3 + with: + name: source + path: dist/ + - name: Deploy + uses: easingthemes/ssh-deploy@v4 + env: + SOURCE: "dist package.json package-lock.json" + ARGS: "--delete -az" + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + REMOTE_HOST: ${{ secrets.REMOTE_HOST }} + REMOTE_USER: ${{ secrets.REMOTE_USER }} + TARGET: "<директория-проекта>" + SCRIPT_AFTER: | + cd <директория-проекта> + npm i --omit=dev + <команда-для-запуска> +``` + +где `<директория-проекта>` заменяется именем директории, в которой на сервере хранится сборка бота, а `<команда-для-запуска>` --- командой для запуска бота, которая может быть, например, вызовом `pm2` или `systemctl`. + +Этот скрипт последовательно выполняет две задачи: `build` и `deploy`. +После выполнения `build`, директория `dist`, содержащая сборку бота, передается задаче `deploy`. + +Доставка файлов на сервер осуществляется с помощью утилиты `rsync`, которая реализована в `easingthemes/ssh-deploy`. +После того как файлы доставлены на сервер, выполняется команда, описанная в переменной окружения `SCRIPT_AFTER`. +В нашем случае после доставки файлов мы переходим в директорию бота, где устанавливаем все зависимости, кроме `devDependencies`, и перезапускаем бота. + +Обратите внимание, что вам необходимо добавить три [секретные переменные окружения](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions): + +1. `SSH_PRIVATE_KEY` --- здесь должен храниться приватный SSH-ключ, который вы создали на [предыдущем шаге](#ssh-ключи). +2. `REMOTE_HOST` --- здесь должен храниться IP адрес вашего сервера. +3. `REMOTE_USER` --- здесь должно храниться имя пользователя, от имени которого запускается бот. + +#### Node.js (GitLab) + +Используйте + +```yml +image: node:latest + +stages: + - build + - deploy + +Build: + stage: build + before_script: npm ci + script: npm run build + artifacts: + paths: + - dist/ + +Deploy: + stage: deploy + before_script: + - "command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )" + - "command -v rsync >/dev/null || ( apt-get update -y && apt-get install rsync -y )" + - eval $(ssh-agent -s) + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + - ssh-keyscan "$REMOTE_HOST" >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + script: + - rsync --delete -az dist package.json package-lock.json $REMOTE_USER@$REMOTE_HOST:<директория-проекта> + - ssh $REMOTE_USER@$REMOTE_HOST "cd <директория-проекта> && npm i --omit=dev && <команда-для-запуска>" +``` + +где `<директория-проекта>` заменяется именем директории, в которой на сервере хранится сборка бота, а `<команда-для-запуска>` --- командой для запуска бота, которая может быть, например, вызовом `pm2` или `systemctl`. + +Этот скрипт последовательно выполняет две задачи: `build` и `deploy`. +После выполнения `build`, директория `dist`, содержащая сборку бота, передается задаче `deploy`. + +Файлы доставляются на сервер с помощью утилиты `rsync`, которую мы должны установить перед выполнением основного скрипта. +После доставки файлов мы подключаемся к серверу по SSH, чтобы выполнить команду установки всех зависимостей, кроме `devDependencies`, и перезапустить приложение. + +Обратите внимание, что вам необходимо добавить три [переменные окружения](https://docs.gitlab.com/ee/ci/variables): + +1. `SSH_PRIVATE_KEY` --- здесь должен храниться приватный SSH-ключ, который вы создали на [предыдущем шаге](#ssh-ключи). +2. `REMOTE_HOST` --- здесь должен храниться IP адрес вашего сервера. +3. `REMOTE_USER` --- здесь должно храниться имя пользователя, от имени которого запускается бот. + +#### Deno (GitHub) + +Используйте + +```yml +name: Main + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Deploy + uses: easingthemes/ssh-deploy@v4 + env: + SOURCE: "src deno.jsonc deno.lock" + ARGS: "--delete -az" + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + REMOTE_HOST: ${{ secrets.REMOTE_HOST }} + REMOTE_USER: ${{ secrets.REMOTE_USER }} + TARGET: "<директория-проекта>" + SCRIPT_AFTER: | + cd <директория-проекта> + <команда-для-запуска> +``` + +где `<директория-проекта>` заменяется именем директории, в которой на сервере хранится сборка бота, а `<команда-для-запуска>` --- командой для запуска бота, которая может быть, например, вызовом `pm2` или `systemctl`. + +Этот скрипт отправляет файлы на сервер с помощью утилиты `rsync`, которая реализована в `easingthemes/ssh-deploy`. +После того как файлы доставлены на сервер, выполняется команда, описанная в переменной окружения `SCRIPT_AFTER`. +В нашем случае после доставки файлов мы переходим в директорию бота и перезапускаем его. + +Обратите внимание, что вам необходимо добавить три [секретные переменные окружения](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions): + +1. `SSH_PRIVATE_KEY` --- здесь должен храниться приватный SSH-ключ, который вы создали на [предыдущем шаге](#ssh-ключи). +2. `REMOTE_HOST` --- здесь должен храниться IP адрес вашего сервера. +3. `REMOTE_USER` --- здесь должно храниться имя пользователя, от имени которого запускается бот. + +#### Deno (GitLab) + +Используйте + +```yml +image: denoland/deno:latest + +stages: + - deploy + +Deploy: + stage: deploy + before_script: + - "command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )" + - "command -v rsync >/dev/null || ( apt-get update -y && apt-get install rsync -y )" + - eval $(ssh-agent -s) + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + - ssh-keyscan "$REMOTE_HOST" >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + script: + - rsync --delete -az src deno.jsonc deno.lock $REMOTE_USER@$REMOTE_HOST:<директория-проекта> + - ssh $REMOTE_USER@$REMOTE_HOST "cd <директория-проекта> && npm i --omit=dev && <команда-для-запуска>" +``` + +где `<директория-проекта>` заменяется именем директории, в которой на сервере хранится сборка бота, а `<команда-для-запуска>` --- командой для запуска бота, которая может быть, например, вызовом `pm2` или `systemctl`. + +Этот скрипт отправляет файлы на сервер с помощью `rsync`, который должен быть предварительно установлен. +После того как файлы скопированы, мы подключаемся к серверу по SSH, чтобы перезапустить бота. + +Обратите внимание, что вам необходимо добавить три [переменные окружения](https://docs.gitlab.com/ee/ci/variables): + +1. `SSH_PRIVATE_KEY` --- здесь должен храниться приватный SSH-ключ, который вы создали на [предыдущем шаге](#ssh-ключи). +2. `REMOTE_HOST` --- здесь должен храниться IP адрес вашего сервера. +3. `REMOTE_USER` --- здесь должно храниться имя пользователя, от имени которого запускается бот. + +Теперь вы должны видеть, как каждый код, добавленный в ветку `main`, будет автоматически разворачиваться на вашем VPS. +Разработка go brrrr :rocket: diff --git a/site/docs/ru/hosting/zeabur-deno.md b/site/docs/ru/hosting/zeabur-deno.md new file mode 100644 index 000000000..0bb96a4b6 --- /dev/null +++ b/site/docs/ru/hosting/zeabur-deno.md @@ -0,0 +1,93 @@ +--- +prev: false +next: false +--- + +# Хостинг: Zeabur (Deno) + +[Zeabur](https://zeabur.com) --- это платформа, позволяющая с легкостью развертывать полнофункциональные приложения. +Она поддерживает различные языки программирования и фреймворки, включая Deno и grammY. + +В этом руководстве вы узнаете, как развернуть ботов grammY с помощью Deno на [Zeabur](https://zeabur.com). + +::: tip Ищете версию для Node.js? +В этом руководстве объясняется, как развернуть Telegram бота на Zeabur с помощью Deno. +Если вам нужна версия для Node.js, пожалуйста, посмотрите [эту страницу](./zeabur-nodejs). +::: + +## Необходимые условия + +Чтобы следить за этим, вам необходимо иметь аккаунты [GitHub](https://github.com) и [Zeabur](https://zeabur.com). + +### Метод 1: Создайте новый проект с нуля + +> Убедитесь, что на вашей локальной машине установлен Deno. + +Инициализируйте проект и установите некоторые необходимые зависимости: + +```sh +# Инициализируйте проект. +mkdir grammy-bot +cd grammy-bot + +# Создайте файл main.ts +touch main.ts + +# Создайте файл deno.json для генерации lock файла +touch deno.json +``` + +Затем измените файл `main.ts`, добавив в него следующий код: + +```ts +import { Bot } from "https://deno.land/x/grammy/mod.ts"; + +const token = Deno.env.get("TELEGRAM_BOT_TOKEN"); +if (!token) throw new Error("TELEGRAM_BOT_TOKEN не установлен"); + +const bot = new Bot(token); + +bot.command("start", (ctx) => ctx.reply("Привет от Deno & grammY!")); + +bot.on("message", (ctx) => ctx.reply("Как я могу вам помочь?")); + +bot.start(); +``` + +> Примечание: Получите токен бота с помощью [@BotFather](https://t.me/BotFather) в Telegram и установите его в качестве переменной окружения `TELEGRAM_BOT_TOKEN` в Zeabur. +> Вы можете ознакомиться с [этим руководством](https://zeabur.com/docs/deploy/variables) по настройке переменных окружения в Zeabur. + +Затем выполните следующую команду для запуска бота: + +```sh +deno run --allow-net main.ts +``` + +Deno автоматически загрузит зависимости, сгенерирует lock файл и запустит вашего бота. + +### Метод 2: Используйте шаблон от Zeabur + +Zeabur уже предоставил вам шаблон для использования. +Вы можете найти его [здесь](https://github.com/zeabur/deno-telegram-bot-starter). + +Вы можете просто использовать шаблон и начать писать код своего бота. + +## Развертывание + +### Метод 1: Развертывание с GitHub в панели Zeabur + +1. Создайте репозиторий на GitHub, он может быть публичным или приватным, и разместите в нем свой код. +2. Перейдите на [Zeabur dashboard](https://dash.zeabur.com). +3. Нажмите на кнопку `New Project`, затем нажмите на кнопку `Deploy New Service`, выберите `GitHub` в качестве источника и выберите ваш репозиторий. +4. Перейдите на вкладку `Variables`, чтобы добавить переменные окружения, например `TELEGRAM_BOT_TOKEN`. +5. Ваш сервис будет развернут автоматически. + +### Метод 2: Развертывание с помощью Zeabur CLI + +`cd` в каталог проекта и выполните следующую команду: + +```sh +npx @zeabur/cli deploy +``` + +Следуйте инструкциям, чтобы выбрать регион для развертывания, и ваш бот будет развернут автоматически. diff --git a/site/docs/ru/hosting/zeabur-nodejs.md b/site/docs/ru/hosting/zeabur-nodejs.md new file mode 100644 index 000000000..52454202d --- /dev/null +++ b/site/docs/ru/hosting/zeabur-nodejs.md @@ -0,0 +1,136 @@ +--- +prev: false +next: false +--- + +# Хостинг: Zeabur (Node.js) + +[Zeabur](https://zeabur.com) --- это платформа, позволяющая с легкостью развертывать полнофункциональные приложения. +Она поддерживает различные языки программирования и фреймворки, включая Node.js и grammY. + +В этом руководстве вы узнаете, как развернуть бота grammY с Node.js на [Zeabur](https://zeabur.com). + +::: tip Ищете версию Deno? +В этом руководстве объясняется, как развернуть Telegram-бота на Zeabur с помощью Node.js. +Если вы ищете версию Deno, пожалуйста, посмотрите [эту страницу](./zeabur-deno) вместо этого. +::: + +## Необходимые условия + +Чтобы следить за этим, вам необходимо иметь аккаунты [GitHub](https://github.com) и [Zeabur](https://zeabur.com). + +### Метод 1: Создайте новый проект с нуля + +Инициализируйте ваш проект и установите некоторые необходимые зависимости: + +```sh +# Инициализируйте проект. +mkdir grammy-bot +cd grammy-bot +npm init -y + +# Установите основные зависимости. +npm install grammy + +# Установите зависимости для разработки. +npm install -D typescript ts-node @types/node + +# Инициализируйте TypeScript. +npx tsc --init +``` + +Затем `cd` в `src/` и создайте файл с именем `bot.ts`. +В нем вы будете писать код вашего бота. + +Теперь вы можете начать писать код бота в `src/bot.ts`. + +```ts +import { Bot } from "grammy"; + +const token = process.env.TELEGRAM_BOT_TOKEN; +if (!token) throw new Error("TELEGRAM_BOT_TOKEN не установлен"); + +const bot = new Bot(token); + +bot.on("message:text", async (ctx) => { + console.log("Сообщение: ", ctx.message.text); + + const response = "Привет, я бот!"; + + await ctx.reply(response); +}); + +bot.start(); +``` + +> Примечание: Получите токен бота с помощью [@BotFather](https://t.me/BotFather) в Telegram и установите его в качестве переменной окружения `TELEGRAM_BOT_TOKEN` в Zeabur. +> Вы можете ознакомиться с [этим руководством](https://zeabur.com/docs/deploy/variables) по настройке переменных окружения в Zeabur. + +Теперь корневая директория вашего проекта должна выглядеть следующим образом: + +```asciiart:no-line-numbers +. +├── node_modules/ +├── src/ +│ └── bot.ts +├── package.json +├── package-lock.json +└── tsconfig.json +``` + +А затем нам нужно добавить скрипты `start` в наш `package.json`. +Теперь наш `package.json` должен быть похож на этот: + +```json +{ + "name": "telegram-bot-starter", + "version": "1.0.0", + "description": "Стартовый бот Telegram с TypeScript и grammY", + "scripts": { + "start": "ts-node src/bot.ts" // [!code focus] + }, + "author": "MichaelYuhe", + "license": "MIT", + "dependencies": { + "grammy": "^1.21.1" + }, + "devDependencies": { + "@types/node": "^20.14.5", + "ts-node": "^10.9.2", + "typescript": "^5.4.5" + } +} +``` + +Теперь вы можете запустить своего бота локально, выполнив команду: + +```sh +npm run start +``` + +### Метод 2: Используйте шаблон от Zeabur + +Zeabur уже предоставил вам шаблон для использования. +Вы можете найти его [здесь](https://github.com/zeabur/deno-telegram-bot-starter). + +Вы можете просто использовать шаблон и начать писать код своего бота. + +## Развертывание + +### Метод 1: Развертывание с GitHub в панели Zeabur + +1. Создайте репозиторий на GitHub, он может быть публичным или приватным, и разместите в нем свой код. +2. Перейдите на [Zeabur dashboard](https://dash.zeabur.com). +3. Нажмите на кнопку `New Project`, затем нажмите на кнопку `Deploy New Service`, выберите `GitHub` в качестве источника и выберите ваш репозиторий. +4. Перейдите на вкладку `Variables`, чтобы добавить переменные окружения, например `TELEGRAM_BOT_TOKEN`. +5. Ваш сервис будет развернут автоматически. + +### Метод 2: Развертывание с помощью Zeabur CLI + +`cd` в каталог проекта и выполните следующую команду: + +```sh +npx @zeabur/cli deploy +``` + +Следуйте инструкциям, чтобы выбрать регион для развертывания, и ваш бот будет развернут автоматически. diff --git a/site/docs/ru/plugins/README.md b/site/docs/ru/plugins/README.md new file mode 100644 index 000000000..a9eb2f4e9 --- /dev/null +++ b/site/docs/ru/plugins/README.md @@ -0,0 +1,84 @@ +# Что такое плагин? + +Мы хотим, чтобы grammY был легковесным и маленьким, но при этом расширяемым. +Почему? +Потому что не все используют всё! +Плагины создаются как дополнительные функции, добавляемые к программе. + +## Плагины grammY + +Некоторые плагины напрямую **встроены** в основную библиотеку grammY, потому что мы предполагаем, что они нужны многим ботам. +Это облегчает новым пользователям их использование, без необходимости устанавливать новый пакет. + +Большинство плагинов публикуются вместе с основным пакетом grammY, мы называем их **официальными** плагинами. +Они устанавливаются с `@grammyjs/*` на npm, и публикуются под организацией [@grammyjs](https://github.com/grammyjs) на GitHub. +Мы координируем их релизы с релизами grammY и следим за тем, чтобы все хорошо работало вместе. +Каждый раздел документации по официальному плагину имеет название пакета в заголовке. +Например, плагин [grammY runner](./runner) (`runner`) должен быть установлен через `npm install @grammyjs/runner`. +(Если вы используете Deno, а не Node.js, вам следует импортировать плагин из , то есть из файла `mod.ts` модуля `grammy_runner`). + +Есть также несколько **сторонних** плагинов. +Их может опубликовать любой желающий. +Мы не даем никаких гарантий, что они актуальны, хорошо документированы или работают вместе с другими плагинами. +Если вы хотите, ваш собственный сторонний плагин также может быть размещен на сайте, чтобы о нем узнало больше людей. + +## Обзор + +Мы подготовили для вас обзор с кратким описанием каждого плагина. +Установка плагинов --- это весело и просто, и мы хотим, чтобы вы знали, что мы приготовили для вас. + +> Нажмите на название любого пакета, чтобы узнать больше о соответствующем плагине. + +| Плагин | Пакет | Описание | +| ------------------------------------------ | -------------------------------------------------- | ----------------------------------------------------------- | +| [Sessions](./session) | _из коробки_ | Храните данные о пользователях в своей базе данных | +| [Inline and Custom Keyboards](./keyboard) | _из коробки_ | Упрощеняет создание встроенных и пользовательских клавиатур | +| [Media Groups](./media-group) | _из коробки_ | Упрощенает отправку медиагрупп и их редактирование | +| [Inline Queries](./inline-query) | _из коробки_ | Легкое создание результатов для встроенных запросов | +| [Auto-retry](./auto-retry) | [`auto-retry`](./auto-retry) | Автоматическое ограничение скорости | +| [Conversations](./conversations) | [`conversations`](./conversations) | Создание мощных разговорных интерфейсов и диалогов | +| [Chat Members](./chat-members) | [`chat-members`](./chat-members) | Отслеживайте, какой пользователь присоединился к чату | +| [Emoji](./emoji) | [`emoji`](./emoji) | Упростите использование эмодзи в коде | +| [Files](./files) | [`files`](./files) | Удобная работа с файлами | +| [Hydration](./hydrate) | [`hydrate`](./hydrate) | Вызывайте методы в объектах, возвращаемых из вызова API | +| [Internationalization](./i18n) | [`i18n`](./i18n) или [`fluent`](./fluent) | Пусть ваш бот говорит на нескольких языках | +| [Interactive Menus](./menu) | [`menu`](./menu) | Создавайте динамические кнопочные меню с гибкой навигацией | +| [Parse Mode](./parse-mode) | [`parse-mode`](./parse-mode) | Упростите форматирование сообщений | +| [Rate Limiter](./ratelimiter) | [`ratelimiter`](./ratelimiter) | Автоматически ограничивайте пользователей, которые спамят | +| [Router](./router) | [`router`](./router) | Направляйте сообщения в разные части вашего кода | +| [Runner](./runner) | [`runner`](./runner) | Одновременное и масштабное выполнение long polling | +| [Stateless Question](./stateless-question) | [`stateless-question`](./stateless-question) | Созданайте диалоги без хранения данных | +| [Throttler](./transformer-throttler) | [`transformer-throttler`](./transformer-throttler) | Замедляйте вызовы API | + +У нас также есть несколько сторонних плагинов! +Вы можете найти их в навигационном меню в разделе _Плагины_ > _Сторонние_. +Обязательно посмотрите и их! + +## Типы плагинов в grammY + +Все, что блестит, - золото, верно? +Но это совсем другое золото! +grammY может использовать преимущества двух типов плагинов: _middleware плагины_ и _плагины-трансформеры_. +Проще говоря, плагины в grammY возвращают либо middleware функцию, либо трансформирующую функцию. +Давайте поговорим о различиях. + +### Тип I: Middleware плагины + +[Middleware](../guide/middleware) --- это функция, которая обрабатывает входящие данные в различных формах. +Плагины Middleware --- это плагины, которые подаются боту как... ну, вы догадались --- как Middleware. +Это означает, что вы устанавливаете их через `bot.use`. + +### Тип II: Плагины-трансформеры + +[Трансформирующая функция](../advanced/transformers) --- это противоположность middleware! +Это функция, которая обрабатывает исходящие данные. +Плагины-трансформеры --- это плагины, которые подаются боту как... безумие! Вы снова угадали --- трансформирующая функция. +Это означает, что вы устанавливаете их через `bot.api.config.use`. + +## Создавайте свои собственные плагины + +Если вы хотите разработать плагин и поделиться им с другими пользователями (даже опубликовать на официальном сайте grammY), есть [полезное руководство](./guide), с которым вы можете ознакомиться. + +## Идеи для новых плагинов + +Мы собираем идеи для новых плагинов [на GitHub в этом issue](https://github.com/grammyjs/grammY/issues/110). diff --git a/site/docs/ru/plugins/auto-retry.md b/site/docs/ru/plugins/auto-retry.md new file mode 100644 index 000000000..f07be3510 --- /dev/null +++ b/site/docs/ru/plugins/auto-retry.md @@ -0,0 +1,96 @@ +--- +prev: false +next: false +--- + +# Повторные запросы к API (`auto-retry`) + +Плагин auto-retry --- это все, что нужно для борьбы с [лимитами на флуда](../advanced/flood), то есть ошибками с кодом 429. +Его можно использовать для каждого бота во время обычной работы, но особенно он пригодится во время [трансляции сообщений](../advanced/flood#как-транслировать-сообщения). + +Этот плагин представляет собой [трансформирующую функцию API](../advanced/transformers), что означает, что он позволяет перехватывать и изменять исходящие HTTP-запросы на лету. +Более конкретно, этот плагин автоматически определяет, если API-запрос не выполняется со значением `retry_after`, т.е. из-за ограничения скорости. +Он перехватит ошибку, подождет указанный период времени, а затем повторит запрос. + +В дополнение к обработке ограничения скорости, этот плагин повторит запрос, если он завершился с внутренней ошибкой сервера, т.е. ошибкой с кодом 500 или больше. +Сетевые ошибки (те, которые [бросают `HttpError`](../guide/errors#объект-httperror) в grammY) также будут вызывать повторную попытку. +Повторное выполнение таких запросов - более или менее единственная разумная стратегия обработки этих двух типов ошибок. +Поскольку ни одна из них не предоставляет значения `retry_after`, плагин использует экспоненциальный возврат, начинающийся с 3 секунд и ограничивающийся одним часом. + +## Установка + +Вы можете установить этот плагин на объект `bot.api`: + +::: code-group + +```ts [TypeScript] +import { autoRetry } from "@grammyjs/auto-retry"; + +// Используйте плагин. +bot.api.config.use(autoRetry()); +``` + +```js [JavaScript] +const { autoRetry } = require("@grammyjs/auto-retry"); + +// Используйте плагин. +bot.api.config.use(autoRetry()); +``` + +```ts [Deno] +import { autoRetry } from "https://deno.land/x/grammy_auto_retry/mod.ts"; + +// Используйте плагин. +bot.api.config.use(autoRetry()); +``` + +::: + +Если вы вызовете, например, `sendMessage` и столкнетесь с ограничением скорости, это будет выглядеть так, как будто запрос выполняется необычно долго. +Под капотом выполняется несколько HTTP-запросов с соответствующими задержками между ними. + +## Настройка + +Вы можете передать объект options, указывающий максимальное количество повторных попыток (`maxRetryAttempts`) или порог максимального времени ожидания (`maxDelaySeconds`). + +### Ограничение повторов + +Как только максимальное количество повторных попыток будет исчерпано, последующие ошибки для того же запроса не будут повторяться. +Вместо этого передается объект ошибки из Telegram, что фактически приводит к неудаче запроса с [`GrammyError`](../guide/errors#объект-grammyerror). + +Аналогично, если запрос когда-либо завершится с `retry_after` больше, чем указано в опции `maxDelaySeconds`, запрос завершится немедленно. + +```ts +autoRetry({ + maxRetryAttempts: 1, // повторяйте запросы только один раз + maxDelaySeconds: 5, // немедленно остановится, если приходится ждать больше 5 секунд +}); +``` + +### Повторный запрос ошибок внутреннего сервера + +Вы можете использовать `rethrowInternalServerErrors`, чтобы отказаться от обработки внутренних ошибок сервера, как описано [выше](#повторные-запросы-к-api-auto-retry). +Опять же, передается объект ошибки от Telegram, что фактически приводит к отказу запроса с [`GrammyError`](../guide/errors#объект-grammyerror). + +```ts +autoRetry({ + rethrowInternalServerErrors: true, // не обрабатывать внутренние ошибки сервера +}); +``` + +### Повторный запрос сетевых ошибок + +Вы можете использовать `rethrowHttpErrors`, чтобы отказаться от обработки сетевых ошибок, как описано [выше](#повторные-запросы-к-api-auto-retry). +Если это включено, то брошенные экземпляры [`HttpError`](../guide/errors#объект-httperror) будут переданы, не выполнив запрос. + +```ts +autoRetry({ + rethrowHttpErrors: true, // не обрабатывать сетевые ошибки +}); +``` + +## Краткая информация о плагине + +- Название: `auto-retry` +- [Исходник](https://github.com/grammyjs/auto-retry) +- [Ссылка](/ref/auto-retry/) diff --git a/site/docs/ru/plugins/autoquote.md b/site/docs/ru/plugins/autoquote.md new file mode 100644 index 000000000..575d890f3 --- /dev/null +++ b/site/docs/ru/plugins/autoquote.md @@ -0,0 +1,136 @@ +--- +prev: false +next: false +--- + +# Всегда отвечать на сообщения + +Иногда необходимо всегда отправлять сообщения в виде ответов, особенно для ботов, которые предназначены для использования в группах. +Обычно мы делаем это, добавляя `reply_parameters` к методам, которые отправляют сообщение: `sendText`, `reply`, `sendPhoto`, `replyWithPhoto` и т.д. +Однако если вы будете делать это для каждого сообщения, это может стать беспорядочным и скучным. + +Этот плагин устанавливает свойства `reply_parameters` для всех методов `reply*` и `send*`, которые его поддерживают, чтобы каждое сообщение было ответом на сообщение и чат, которые его вызвали. + +Вы можете передать объект options со свойством `allowSendingWithoutReply` функциям `addReplyParam` и `autoQuote`, что позволит вашему боту отправлять сообщения, даже если сообщение, на которое отвечают, больше не существует. + +## Использование + +### В определенном контексте + +Если вы хотите, чтобы все сообщения отправлялись в определенном контексте (например, по определенной команде), вы можете специально применить плагин к ним: + +::: code-group + +```ts [TypeScript] +import { Bot } from "grammy"; +import { addReplyParam } from "@roziscoding/grammy-autoquote"; + +const bot = new Bot(""); + +bot.command("demo", async (ctx) => { + ctx.api.config.use(addReplyParam(ctx)); + await ctx.reply("Тестовая команда!"); // это будет цитировать сообщение пользователя +}); + +bot.start(); +``` + +```js [JavaScript] +const { Bot } = require("grammy"); +const { addReplyParam } = require("@roziscoding/grammy-autoquote"); + +const bot = new Bot(""); + +bot.command("demo", async (ctx) => { + ctx.api.config.use(addReplyParam(ctx)); + await ctx.reply("Тестовая команда!"); // это будет цитировать сообщение пользователя +}); + +bot.start(); +``` + +```ts [Deno] +import { Bot } from "https://deno.land/x/grammy/mod.ts"; +import { addReplyParam } from "https://deno.land/x/grammy_autoquote/mod.ts"; + +const bot = new Bot(""); + +bot.command("demo", async (ctx) => { + ctx.api.config.use(addReplyParam(ctx)); + await ctx.reply("Тестовая команда!"); // это будет цитировать сообщение пользователя +}); + +bot.start(); +``` + +::: + +### В каждом контексте + +Если вы хотите, чтобы каждое отправленное сообщение цитировало сообщения пользователя, которые использовали команду, вы можете применить плагин таким образом: + +::: code-group + +```ts [TypeScript] +import { Bot } from "grammy"; +import { autoQuote } from "@roziscoding/grammy-autoquote"; + +const bot = new Bot(""); + +bot.use(autoQuote()); + +bot.command("demo", async (ctx) => { + await ctx.reply("Тестовая команда!"); // это будет цитировать сообщение пользователя +}); + +bot.command("hello", async (ctx) => { + await ctx.reply("Привет :)"); // здесь также цитируется сообщение пользователя +}); + +bot.start(); +``` + +```js [JavaScript] +const { Bot } = require("grammy"); +const { autoQuote } = require("@roziscoding/grammy-autoquote"); + +const bot = new Bot(""); + +bot.use(autoQuote()); + +bot.command("demo", async (ctx) => { + await ctx.reply("Тестовая команда!"); // это будет цитировать сообщение пользователя +}); + +bot.command("hello", async (ctx) => { + await ctx.reply("Привет :)"); // здесь также цитируется сообщение пользователя +}); + +bot.start(); +``` + +```ts [Deno] +import { Bot } from "https://deno.land/x/grammy/mod.ts"; +import { autoQuote } from "https://deno.land/x/grammy_autoquote/mod.ts"; + +const bot = new Bot(""); + +bot.use(autoQuote()); + +bot.command("demo", async (ctx) => { + await ctx.reply("Тестовая команда!"); // это будет цитировать сообщение пользователя +}); + +bot.command("hello", async (ctx) => { + await ctx.reply("Привет :)"); // здесь также цитируется сообщение пользователя +}); + +bot.start(); +``` + +::: + +## Краткая информация о плагине + +- Название: Autoquote +- [Исходник](https://github.com/roziscoding/grammy-autoquote) diff --git a/site/docs/ru/plugins/chat-members.md b/site/docs/ru/plugins/chat-members.md new file mode 100644 index 000000000..a4a5f7c5b --- /dev/null +++ b/site/docs/ru/plugins/chat-members.md @@ -0,0 +1,150 @@ +--- +prev: false +next: false +--- + +# Плагин участников чата (`chat-members`) + +Автоматически сохраняйте информацию о пользователях в чате и легко извлекайте ее. +Отслеживайте участников групп и каналов и составляйте их списки. + +## Введение + +Во многих ситуациях боту необходимо иметь информацию обо всех пользователях данного чата. +Однако в настоящее время Telegram Bot API не предоставляет методов, позволяющих получить эту информацию. + +Этот плагин приходит на помощь: автоматически прослушивает события `chat_member` и сохраняет все объекты `ChatMember`. + +## Использование + +### Сохранение пользователей чата + +Вы можете использовать действительный адаптер grammY [storage adapter](./session#известные-адаптеры-хранения) или экземпляр любого класса, реализующего интерфейс [`StorageAdapter`](/ref/core/storageadapter). + +Обратите внимание, что, согласно [официальной документации Telegram](https://core.telegram.org/bots/api#getupdates), ваш бот должен указать обновление `chat_member` в массиве `allowed_updates`, как показано в примере ниже. +Это означает, что вам также нужно указать любые другие события, которые вы хотели бы получать. + +::: code-group + +```ts [TypeScript] +import { Bot, type Context, MemorySessionStorage } from "grammy"; +import { type ChatMember } from "grammy/types"; +import { chatMembers, type ChatMembersFlavor } from "@grammyjs/chat-members"; + +type MyContext = Context & ChatMembersFlavor; + +const adapter = new MemorySessionStorage(); + +const bot = new Bot(""); + +bot.use(chatMembers(adapter)); + +bot.start({ + // Обязательно укажите нужные типы обновлений + allowed_updates: ["chat_member", "message"], +}); +``` + +```js [JavaScript] +import { Bot, MemorySessionStorage } from "grammy"; +import { chatMembers } from "@grammyjs/chat-members"; + +const adapter = new MemorySessionStorage(); + +const bot = new Bot(""); + +bot.use(chatMembers(adapter)); + +bot.start({ + // Обязательно укажите нужные типы обновлений + allowed_updates: ["chat_member", "message"], +}); +``` + +```ts [Deno] +import { + Bot, + type Context, + MemorySessionStorage, +} from "https://deno.land/x/grammy/mod.ts"; +import { type ChatMember } from "https://deno.land/x/grammy/types.ts"; +import { + chatMembers, + type ChatMembersFlavor, +} from "https://deno.land/x/grammy_chat_members/mod.ts"; + +type MyContext = Context & ChatMembersFlavor; + +const adapter = new MemorySessionStorage(); + +const bot = new Bot(""); + +bot.use(chatMembers(adapter)); + +bot.start({ + // Обязательно укажите нужные типы обновлений + allowed_updates: ["chat_member", "message"], +}); +``` + +::: + +### Чтение пользователей чата + +Этот плагин также добавляет новую функцию `ctx.chatMembers.getChatMember`, которая будет проверять хранилище на наличие информации об участнике чата, прежде чем запрашивать ее у Telegram. +Если участник чата существует в хранилище, он будет возвращен. +В противном случае будет вызвана функция `ctx.api.getChatMember`, и результат будет сохранен в хранилище, что ускорит последующие вызовы и избавит вас от необходимости снова обращаться к Telegram для этого пользователя и чата в будущем. + +Вот пример: + +```ts +bot.on("message", async (ctx) => { + const chatMember = await ctx.chatMembers.getChatMember(); + + return ctx.reply( + `Привет, ${chatMember.user.first_name}! Я вижу, что вы ${chatMember.status} этого чата!`, + ); +}); +``` + +Эта функция принимает следующие необязательные параметры: + +- `chatId`: + - По умолчанию: `ctx.chat.id` + - Идентификатор чата +- `userId`: + - По умолчанию: `ctx.from.id` + - Идентификатор пользователя + +Вы можете передавать их следующим образом: + +```ts +bot.on("message", async (ctx) => { + const chatMember = await ctx.chatMembers.getChatMember( + ctx.chat.id, + ctx.from.id, + ); + return ctx.reply( + `Привет, ${chatMember.user.first_name}! Я вижу, что вы ${chatMember.status} этого чата!`, + ); +}); +``` + +Обратите внимание, что если вы не указали идентификатор чата и в контексте нет свойства `chat` (например, при обновлении запроса), это приведет к ошибке. +То же самое произойдет, если в контексте нет свойства `ctx.from`. + +## Агрессивное хранение + +Параметр конфигурации `enableAggressiveStorage` установит middleware для кэширования членов чата без зависимости от события `chat_member`. +При каждом обновлении middleware проверяет, существуют ли `ctx.chat` и `ctx.from`. +Если они существуют, то выполняется вызов `ctx.chatMembers.getChatMember`, чтобы добавить информацию о пользователи чата в хранилище, если она не существует. + +Обратите внимание, что это означает, что хранилище будет вызываться **каждое обновление**, что может быть очень много, в зависимости от того, сколько обновлений получает ваш бот. +Это может сильно повлиять на производительность вашего бота. +Используйте это только в том случае, если вы действительно знаете, что делаете, и не боитесь рисков и последствий. + +## Краткая информация о плагине + +- Название: `chat-members` +- [Исходник](https://github.com/grammyjs/chat-members) +- [Ссылка](/ref/chat-members/) diff --git a/site/docs/ru/plugins/commands.md b/site/docs/ru/plugins/commands.md new file mode 100644 index 000000000..b7986732a --- /dev/null +++ b/site/docs/ru/plugins/commands.md @@ -0,0 +1,14 @@ +--- +prev: false +next: false +--- + +# Команды (`commands`) + +Скоро будет, возвращайтесь позднее. + +## Краткая информация о плагине + +- Название: `commands` +- [Исходник](https://github.com/grammyjs/commands) +- Документация diff --git a/site/docs/ru/plugins/console-time.md b/site/docs/ru/plugins/console-time.md new file mode 100644 index 000000000..73f42fce9 --- /dev/null +++ b/site/docs/ru/plugins/console-time.md @@ -0,0 +1,85 @@ +--- +prev: false +next: false +--- + +# Ведение журнала консоли при отладке + +Если вы знакомы с JavaScript/TypeScript, то наверняка использовали [`console.log`](https://developer.mozilla.org/en-US/docs/Web/API/console/log_static) или [`console.time`](https://developer.mozilla.org/en-US/docs/Web/API/console/time_static), чтобы проверить, что происходит во время отладки. +Во время работы над ботом или middleware вы можете захотеть проверить например: что произошло и сколько времени это заняло? + +Этот плагин заинтересован в индивидуальных запросах для отладки отдельных проблем. +Находясь в производственной среде, вы, вероятно, захотите получить что-то противоположное, чтобы получить приблизительный обзор. +Например: при отладке причин сбоя `/start` вы будете проверять отдельные обновления Telegram. +В производственном контексте вас больше интересуют все сообщения `/start`, которые происходят. +Эта библиотека призвана помочь в работе с отдельными обновлениями. + +## Отладка вашей реализации + +```ts +import { generateUpdateMiddleware } from "telegraf-middleware-console-time"; + +if (process.env.NODE_ENV !== "production") { + bot.use(generateUpdateMiddleware()); +} + +// Ваша реализация +bot.command("start" /* , ... */); +``` + +который выведет примерно следующее: + +```text +2020-03-31T14:32:36.974Z 490af message text Edgar 6 /start: 926.247ms +2020-03-31T14:32:57.750Z 490ag message text Edgar 6 /start: 914.764ms +2020-03-31T14:33:01.188Z 490ah message text Edgar 5 /stop: 302.666ms +2020-03-31T14:46:11.385Z 490ai message text Edgar 6 /start: 892.452ms +``` + +`490af` --- это `update_id`. + +Число перед командами --- это общая длина содержимого. +Это полезно при определении максимальной длины для таких вещей, как данные calllback. + +Само содержимое сокращено, чтобы предотвратить спам в журнале. + +## Отладка вашего middleware + +Если вы создаете собственный middleware или используете медленные тайминги другого middleware, вы можете использовать эти middleware для создания профиля тайминга. + +```ts +import { + generateAfterMiddleware, + generateBeforeMiddleware, +} from "telegraf-middleware-console-time"; + +const bot = new Bot(""); + +// Используйте BeforeMiddleware перед загрузкой протестированного middleware. +bot.use(generateBeforeMiddleware("foo")); + +// Middleware, который нужно протестировать +bot.use(); /* ... */ + +// Используйте AfterMiddleware после загрузки тестируемого middleware (с тем же названием). +bot.use(generateAfterMiddleware("foo")); + +// Другие middleware/имплементации (при использовании они будут занимать "внутреннее" количество времени). +bot.use(); /* ... */ +bot.on("message" /* ... */); +``` + +В результате получится что-то вроде этого: + +```text +490ai foo before: 304.185ms +490ai foo inner: 83.122ms +490ai foo after: 501.028ms +490ai foo total: 891.849ms +``` + +Это указывает на то, что проверка одного только middleware заняла 800 мс и не является настолько производительной, как это может быть необходимо. + +## Краткая информация о плагине + +- [Исходник](https://github.com/EdJoPaTo/telegraf-middleware-console-time) diff --git a/site/docs/ru/plugins/conversations.md b/site/docs/ru/plugins/conversations.md new file mode 100644 index 000000000..c02971c92 --- /dev/null +++ b/site/docs/ru/plugins/conversations.md @@ -0,0 +1,1346 @@ +--- +prev: false +next: false +--- + +# Диалоги (`conversations`) + +Создавайте мощные диалоговые интерфейсы с легкостью. + +## Введение + +Большинство чатов состоит не только из одного сообщения. (ага) + +Например, вы можете задать пользователю вопрос, а затем дождаться ответа. Это +может происходить даже несколько раз, так что получается целая беседа. + +Когда вы думаете о [middleware](../guide/middleware), вы замечаете, что все +основано на одном [объекте контекста](../guide/context) для каждого обработчика. +Это означает, что вы всегда обрабатываете только одно сообщение в отдельности. +Не так-то просто написать что-то вроде "проверьте текст три сообщения назад" или +что-то в этом роде. + +**Этот плагин приходит на помощь:**. Он предоставляет чрезвычайно гибкий способ +определения разговоров между вашим ботом и пользователями. + +Многие фреймворки заставляют вас определять большие объекты конфигурации с +шагами, этапами, переходами, wizard и так далее. Это приводит к появлению +большого количества шаблонного кода и затрудняет дальнейшую работу. **Этот +плагин не работает таким образом**. + +Вместо этого с помощью этого плагина вы будете использовать нечто гораздо более +мощное: **код**. По сути, вы просто определяете обычную функцию JavaScript, +которая позволяет вам определить, как будет развиваться разговор. По мере того +как бот и пользователь будут разговаривать друг с другом, функция будет +выполняться по порядку. + +(Честно говоря, на самом деле все работает не так. Но очень полезно думать об +этом именно так! В реальности ваша функция будет выполняться немного иначе, но +мы вернемся к этому [позже](#ожидание-обновлении)). + +## Простой пример + +Прежде чем мы перейдем к рассмотрению того, как можно создавать диалоги, +посмотрите на короткий пример JavaScript того, как будет выглядеть беседа. + +```js +async function greeting(conversation, ctx) { + await ctx.reply("Привет, как тебя зовут?"); + const { message } = await conversation.wait(); + await ctx.reply(`Добро пожаловать в чат, ${message.text}!`); +} +``` + +В этом разговоре бот сначала поприветствует пользователя и спросит его имя. +Затем он будет ждать, пока пользователь не назовет свое имя. И наконец, бот +приветствует пользователя в чате, повторяя его имя. + +Легко, правда? Давайте посмотрим, как это делается! + +## Функции конструктора диалогов + +Прежде всего, давайте разберемся в некоторых моментах. + +::: code-group + +```ts [TypeScript] +import { + type Conversation, + type ConversationFlavor, + conversations, + createConversation, +} from "@grammyjs/conversations"; +``` + +```js [JavaScript] +const { + conversations, + createConversation, +} = require("@grammyjs/conversations"); +``` + +```ts [Deno] +import { + type Conversation, + type ConversationFlavor, + conversations, + createConversation, +} from "https://deno.land/x/grammy_conversations/mod.ts"; +``` + +::: + +С этим покончено, теперь мы можем посмотреть, как определять разговорные +интерфейсы. + +Основным элементом разговора является функция с двумя аргументами. Мы называем +ее _функцией построения беседы_. + +```js +async function greeting(conversation, ctx) { + // TODO: Код для диалога +} +``` + +Давайте посмотрим, что представляют собой эти два параметра. + +**Второй параметр** не так интересен, это обычный объект контекста. Как обычно, +он называется `ctx` и использует ваш +[пользовательский тип контекста](../guide/context#кастомизация-объекта-контекста) +(может называться `MyContext`). Плагин conversations экспортирует +[расширитель контекста](../guide/context#дополнительные-расширители-контекста) +под названием `ConversationFlavor`. + +**Первый параметр** является центральным элементом этого плагина. Он обычно +называется `conversation` и имеет тип `Conversation` +([документация API](/ref/conversations/conversation)). Он может использоваться в +качестве ручага для управления беседой, например, для ожидания ввода данных +пользователем и т.д. Тип `Conversation` ожидает ваш +[пользовательский тип контекста](../guide/context#кастомизация-объекта-контекста) +в качестве параметра типа, поэтому вы часто будете использовать +`Conversation`. + +В общем, в TypeScript ваша функция построения диалога будет выглядеть следующим +образом. + +```ts +type MyContext = Context & ConversationFlavor; +type MyConversation = Conversation; + +async function greeting(conversation: MyConversation, ctx: MyContext) { + // TODO: Код для диалога +} +``` + +Внутри функции построения диалога вы можете определить, как она должна +выглядеть. Прежде чем мы подробно остановимся на каждой функции этого плагина, +давайте рассмотрим более сложный пример, чем [простой](#простои-пример) выше. + +::: code-group + +```ts [TypeScript] +async function movie(conversation: MyConversation, ctx: MyContext) { + await ctx.reply("Сколько у вас любимых фильмов?"); + const count = await conversation.form.number(); + const movies: string[] = []; + for (let i = 0; i < count; i++) { + await ctx.reply(`Какой фильм будет под номером ${i + 1}!`); + const titleCtx = await conversation.waitFor(":text"); + movies.push(titleCtx.msg.text); + } + await ctx.reply("Вот рейтинг!"); + movies.sort(); + await ctx.reply(movies.map((m, i) => `${i + 1}. ${m}`).join("\n")); +} +``` + +```js [JavaScript] +async function movie(conversation, ctx) { + await ctx.reply("Сколько у вас любимых фильмов?"); + const count = await conversation.form.number(); + const movies = []; + for (let i = 0; i < count; i++) { + await ctx.reply(`Какой фильм будет под номером ${i + 1}!`); + const titleCtx = await conversation.waitFor(":text"); + movies.push(titleCtx.msg.text); + } + await ctx.reply("Вот рейтинг!"); + movies.sort(); + await ctx.reply(movies.map((m, i) => `${i + 1}. ${m}`).join("\n")); +} +``` + +::: + +Можете ли вы понять, как будет работать этот бот? + +## Создание диалога и вступление в него + +Прежде всего, вы **должны** использовать плагин [session plugin](./session), +если хотите использовать плагин conversations. Вам также необходимо установить +сам плагин conversations, прежде чем вы сможете регистрировать отдельные +разговоры в вашем боте. + +```ts +// Установите плагин сессии. +bot.use(session({ + initial() { + // пока возвращайте пустой объект + return {}; + }, +})); + +// Установите плагин conversations +bot.use(conversations()); +``` + +Далее вы можете установить функцию конструктора диалогов в качестве middleware +на объект бота, обернув ее внутри `createConversation`. + +```ts +bot.use(createConversation(greeting)); +``` + +Теперь, когда ваша беседа зарегистрирована в боте, вы можете войти в нее из +любого обработчика. Обязательно используйте `await` для всех методов на +`ctx.conversation` - иначе ваш код сломается. + +```ts +bot.command("start", async (ctx) => { + await ctx.conversation.enter("greeting"); +}); +``` + +Как только пользователь отправит боту команду `/start`, беседа будет начата. +Текущий объект контекста передается в качестве второго аргумента функции +построения беседы. Например, если вы начнете разговор с +`await ctx.reply(ctx.message.text)`, он будет содержать обновление, содержащее +`/start`. + +::: tip Изменение идентификатора разговора +По умолчанию вы должны передать имя +функции в `ctx.conversation.enter()`. Однако если вы предпочитаете использовать +другой идентификатор, вы можете указать его следующим образом: + +```ts +bot.use(createConversation(greeting, "new-name")); +``` + +В свою очередь, вы можете вступить с ним в разговор: + +```ts +bot.command("start", (ctx) => ctx.conversation.enter("new-name")); +``` + +::: + +В целом ваш код теперь должен выглядеть примерно так: + +::: code-group + +```ts [TypeScript] +import { Bot, Context, session } from "grammy"; +import { + type Conversation, + type ConversationFlavor, + conversations, + createConversation, +} from "@grammyjs/conversations"; + +type MyContext = Context & ConversationFlavor; +type MyConversation = Conversation; + +const bot = new Bot(""); + +bot.use(session({ initial: () => ({}) })); +bot.use(conversations()); + +/** Определяет разговор */ +async function greeting(conversation: MyConversation, ctx: MyContext) { + // TODO: Код для диалога +} + +bot.use(createConversation(greeting)); + +bot.command("start", async (ctx) => { + // войдите в функцию greeting, которую вы создали + await ctx.conversation.enter("greeting"); +}); + +bot.start(); +``` + +```js [JavaScript] +const { Bot, Context, session } = require("grammy"); +const { + conversations, + createConversation, +} = require("@grammyjs/conversations"); + +const bot = new Bot(""); + +bot.use(session({ initial: () => ({}) })); +bot.use(conversations()); + +/** Определяет разговор */ +async function greeting(conversation, ctx) { + // TODO: Код для диалога +} + +bot.use(createConversation(greeting)); + +bot.command("start", async (ctx) => { + // войдите в функцию greeting, которую вы создали + await ctx.conversation.enter("greeting"); +}); + +bot.start(); +``` + +```ts [Deno] +import { Bot, Context, session } from "https://deno.land/x/grammy/mod.ts"; +import { + type Conversation, + type ConversationFlavor, + conversations, + createConversation, +} from "https://deno.land/x/grammy_conversations/mod.ts"; + +type MyContext = Context & ConversationFlavor; +type MyConversation = Conversation; + +const bot = new Bot(""); + +bot.use(session({ initial: () => ({}) })); +bot.use(conversations()); + +/** Определяет разговор */ +async function greeting(conversation: MyConversation, ctx: MyContext) { + // TODO: Код для диалога +} + +bot.use(createConversation(greeting)); + +bot.command("start", async (ctx) => { + // войдите в функцию greeting, которую вы создали + await ctx.conversation.enter("greeting"); +}); + +bot.start(); +``` + +::: + +### Установка с пользовательскими данными сессии + +Обратите внимание, что если вы используете TypeScript и хотите хранить свои +собственные данные сессии, а также использовать разговоры, вам нужно будет +предоставить компилятору больше информации о типе. Допустим, у вас есть этот +интерфейс, который описывает ваши пользовательские данные сессии: + +```ts +interface SessionData { + /** пользовательское свойство сессии */ + foo: string; +} +``` + +Ваш пользовательский тип контекста может выглядеть следующим образом: + +```ts +type MyContext = Context & SessionFlavor & ConversationFlavor; +``` + +Самое главное, что при установке плагина сессий с внешним хранилищем вам +придется явно предоставлять данные сессии. Все адаптеры хранилищ позволяют +передавать `SessionData` в качестве параметра типа. Например, вот как это нужно +сделать с [`freeStorage`](./session#бесплатное-хранилище), который предоставляет +grammY. + +```ts +// Установите плагин сессии. +bot.use(session({ + // Добавьте типы сессии в адаптер. + storage: freeStorage(bot.token), + initial: () => ({ foo: "" }), +})); +``` + +То же самое можно сделать и для всех остальных адаптеров хранения, например +`new FileAdapter()` и так далее. + +### Установка с несколькими сессиями + +Естественно, вы можете объединять беседы с помощью +[мультисессий](./session#мульти-сессии). + +Этот плагин хранит данные разговора внутри `session.conversation`. Это означает, +что если вы хотите использовать несколько сессий, вам необходимо указать этот +фрагмент. + +```ts +// Установите плагин сессии. +bot.use(session({ + type: "multi", + custom: { + initial: () => ({ foo: "" }), + }, + conversation: {}, // может быть пустым +})); +``` + +Таким образом, вы можете хранить данные разговора в другом месте, чем другие +данные сессии. Например, если оставить конфигурацию беседы пустой, как показано +выше, плагин беседы будет хранить все данные в памяти. + +## Выход из диалога + +Разговор будет продолжаться до тех пор, пока ваша функция конструктора диалогов +не завершится. Это означает, что вы можете просто выйти из беседы, используя +`return` или `throw`. + +::: code-group + +```ts [TypeScript] +async function hiAndBye(conversation: MyConversation, ctx: MyContext) { + await ctx.reply("Привет! И пока!"); + // Выйти из беседы: + return; +} +``` + +```js [JavaScript] +async function hiAndBye(conversation, ctx) { + await ctx.reply("Привет! И пока!"); + // Выйти из беседы: + return; +} +``` + +::: + +(Да, ставить `return` в конце функции немного бессмысленно, но вы поняли идею). + +Выброс ошибки также приведет к завершению беседы. Однако плагин +[session](#создание-диалога-и-вступление-в-него) сохраняет данные только при +успешном выполнении middleware. Таким образом, если вы выбросите ошибку внутри +беседы и не поймаете ее до того, как она достигнет плагина сессии, то не будет +сохранено, что беседа была завершена. В результате следующее сообщение вызовет +ту же ошибку. + +Вы можете смягчить это, установив +[границу ошибки](../guide/errors#границы-ошибок) между сессией и разговором. +Таким образом, вы предотвратите распространение ошибки по дереву +[middleware](../advanced/middleware) и, следовательно, позволите плагину сессии +записать данные обратно. + +> Обратите внимание, что если вы используете стандартные сессии in-memory, все +> изменения в данных сессии отражаются немедленно, поскольку нет бэкенда +> хранения. В этом случае вам не нужно использовать границы ошибок, чтобы выйти +> из разговора, выбросив ошибку. + +Вот как границы ошибок и разговоры можно использовать вместе. + +::: code-group + +```ts [TypeScript] +bot.use(session({ + storage: freeStorage(bot.token), // настройка + initial: () => ({}), +})); +bot.use(conversations()); + +async function hiAndBye(conversation: MyConversation, ctx: MyContext) { + await ctx.reply("Привет! И пока!"); + // Выйти из беседы: + throw new Error("Поймай меня, если сможешь!"); +} + +bot.errorBoundary( + (err) => console.error("Диалог выбросил ошибку!", err), + createConversation(greeting), +); +``` + +```js [JavaScript] +bot.use(session({ + storage: freeStorage(bot.token), // настройка + initial: () => ({}), +})); +bot.use(conversations()); + +async function hiAndBye(conversation, ctx) { + await ctx.reply("Привет! И пока!"); + // Выйти из беседы: + throw new Error("Поймай меня, если сможешь!"); +} + +bot.errorBoundary( + (err) => console.error("Диалог выбросил ошибку!", err), + createConversation(greeting), +); +``` + +::: + +Что бы вы ни делали, не забудьте [установить обработчик ошибок](../guide/errors) +для вашего бота. + +Если вы хотите жестко отключить беседу от вашего обычного middleware, пока она +ожидает ввода пользователя, вы также можете использовать +`await ctx.conversation.exit()`. Это просто удалит данные плагина беседы из +сессии. Часто лучше придерживаться простого возврата из функции, но есть +несколько примеров, когда использование `await ctx.conversation.exit()` будет +удобным. Помните, что вы должны `дождаться` вызова. + +::: code-group + +```ts{6,22} [TypeScript] +async function movie(conversation: MyConversation, ctx: MyContext) { + // TODO: Код для диалога +} + +// Установите плагин conversations +bot.use(conversations()); + +// Всегда выходите из любого разговора по команде /cancel +bot.command("cancel", async (ctx) => { + await ctx.conversation.exit(); + await ctx.reply("Leaving."); +}); + +// Всегда выходите из диалога `movie`. +// при нажатии кнопки `отмена` на встроенной клавиатуре. +bot.callbackQuery("cancel", async (ctx) => { + await ctx.conversation.exit("movie"); + await ctx.answerCallbackQuery("Выход из диалога"); +}); + +bot.use(createConversation(movie)); +bot.command("movie", (ctx) => ctx.conversation.enter("movie")); +``` + +```js{6,22} [JavaScript] +async function movie(conversation, ctx) { + // TODO: Код для диалога +} + +// Установите плагин conversations +bot.use(conversations()); + +// Всегда выходите из любого разговора по команде /cancel +bot.command("cancel", async (ctx) => { + await ctx.conversation.exit(); + await ctx.reply("Leaving."); +}); + +// Всегда выходите из диалога `movie`. +// при нажатии кнопки `отмена` на встроенной клавиатуре. +bot.callbackQuery("cancel", async (ctx) => { + await ctx.conversation.exit("movie"); + await ctx.answerCallbackQuery("Выход из диалога"); +}); + +bot.use(createConversation(movie)); +bot.command("movie", (ctx) => ctx.conversation.enter("movie")); +``` + +::: + +Обратите внимание, что порядок здесь имеет значение. Вы должны сначала +установить плагин conversations (строка 6), прежде чем сможете вызвать +`await ctx.conversation.exit()`. Кроме того, общие обработчики отмены должны +быть установлены до того, как будут зарегистрированы реальные разговоры (строка +22). + +## Ожидание обновлений + +Чтобы дождаться следующего обновления в этом конкретном чате, можно использовать +обработчик беседы `conversation`. + +::: code-group + +```ts [TypeScript] +async function waitForMe(conversation: MyConversation, ctx: MyContext) { + // Дождитесь следующего обновления: + const newContext = await conversation.wait(); +} +``` + +```js [JavaScript] +async function waitForMe(conversation, ctx) { + // Дождитесь следующего обновления: + const newContext = await conversation.wait(); +} +``` + +::: + +Обновление может означать, что было отправлено текстовое сообщение, или нажата +кнопка, или что-то было отредактировано, или практически любое другое действие +было выполнено пользователем. Ознакомьтесь с полным списком в документации +Telegram [здесь](https://core.telegram.org/bots/api#update). + +Метод `wait` всегда выдает новый объект [контекста](../guide/context), +представляющий полученное обновление. Это означает, что вы всегда имеете дело с +таким количеством объектов контекста, сколько обновлений получено во время +разговора. + +::: code-group + +```ts [TypeScript] +const TEAM_REVIEW_CHAT = -1001493653006; +async function askUser(conversation: MyConversation, ctx: MyContext) { + // Попросите пользователя указать его домашний адрес. + await ctx.reply("Не могли бы вы указать свой домашний адрес?"); + + // Дождитесь, пока пользователь отправит свой адрес: + const userHomeAddressContext = await conversation.wait(); + + // Спросите пользователя о его национальности. + await ctx.reply("Не могли бы вы также указать вашу национальность?"); + + // Дождитесь, пока пользователь укажет свою национальность: + const userNationalityContext = await conversation.wait(); + + await ctx.reply( + "Это был последний шаг. Теперь, когда я получил всю необходимую информацию, я передам ее нашей команде для рассмотрения. Спасибо!", + ); + + // Теперь мы копируем ответы в другой чат для просмотра. + await userHomeAddressContext.copyMessage(TEAM_REVIEW_CHAT); + await userNationalityContext.copyMessage(TEAM_REVIEW_CHAT); +} +``` + +```js [JavaScript] +const TEAM_REVIEW_CHAT = -1001493653006; +async function askUser(conversation, ctx) { + // Попросите пользователя указать его домашний адрес. + await ctx.reply("Не могли бы вы указать свой домашний адрес?"); + + // Дождитесь, пока пользователь отправит свой адрес: + const userHomeAddressContext = await conversation.wait(); + + // Спросите пользователя о его национальности. + await ctx.reply("Не могли бы вы также указать вашу национальность?"); + + // Дождитесь, пока пользователь укажет свою национальность: + const userNationalityContext = await conversation.wait(); + + await ctx.reply( + "Это был последний шаг. Теперь, когда я получил всю необходимую информацию, я передам ее нашей команде для рассмотрения. Спасибо!", + ); + + // Теперь мы копируем ответы в другой чат для просмотра. + await userHomeAddressContext.copyMessage(TEAM_REVIEW_CHAT); + await userNationalityContext.copyMessage(TEAM_REVIEW_CHAT); +} +``` + +::: + +Обычно, вне плагина разговоров, каждое из этих обновлений обрабатывается +[middleware](../guide/middleware) вашего бота. Таким образом, ваш бот будет +обрабатывать обновления через объект контекста, который передается вашим +обработчикам. + +В обработчиках вы получите этот новый объект контекста из вызова `wait`. В свою +очередь, вы можете обрабатывать различные обновления по-разному, основываясь на +этом объекте. Например, вы можете проверять наличие текстовых сообщений: + +::: code-group + +```ts [TypeScript] +async function waitForText(conversation: MyConversation, ctx: MyContext) { + // Дождитесь следующего обновления: + ctx = await conversation.wait(); + // Проверьте наличие текста: + if (ctx.message?.text) { + // ... + } +} +``` + +```js [JavaScript] +async function waitForText(conversation, ctx) { + // Дождитесь следующего обновления: + ctx = await conversation.wait(); + // Проверьте наличие текста: + if (ctx.message?.text) { + // ... + } +} +``` + +::: + +Кроме того, наряду с `wait` существует ряд других методов, которые позволяют +ждать только определенных обновлений. Одним из примеров является `waitFor`, +который принимает [фильтрующий запрос](../guide/filter-queries) и затем ожидает +только те обновления, которые соответствуют заданному запросу. Это особенно +эффективно в сочетании с +[деструктуризацией объектов](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment): + +::: code-group + +```ts [TypeScript] +async function waitForText(conversation: MyConversation, ctx: MyContext) { + // Дождитесь следующего обновления текстового сообщения: + const { msg: { text } } = await conversation.waitFor("message:text"); +} +``` + +```js [JavaScript] +async function waitForText(conversation, ctx) { + // Дождитесь следующего обновления текстового сообщения: + const { msg: { text } } = await conversation.waitFor("message:text"); +} +``` + +::: + +Посмотрите [документацию API](/ref/conversations/conversationhandle#wait), чтобы +увидеть все доступные методы, похожие на `wait`. + +## Три золотых правила ведения диалога + +Есть три правила, которые применяются к коду, написанному внутри функции +построения беседы. Вы должны следовать им, если хотите, чтобы ваш код работал +правильно. + +Прокрутите [вниз](#как-это-работает), если хотите узнать больше о том, _почему_ +применяются эти правила, и что на самом деле делают вызовы `wait` внутри +функции. + +### Правило I: Все побочные эффекты должны быть завернуты + +Код, зависящий от внешних систем, таких как базы данных, API, файлы или другие +ресурсы, которые могут меняться от одного выполнения к другому, должен быть +обернут в вызовы `conversation.external()`. + +```ts +// ПЛОХО +const response = await externalApi(); +// ХОРОШО +const response = await conversation.external(() => externalApi()); +``` + +Сюда входит как чтение данных, так и выполнение побочных эффектов (например, +запись в базу данных). + +::: tip Сравнимо с React +Если вы знакомы с React, то вам может быть знакома +сопоставимая концепция `useEffect`. +::: + +### Правило II: все случайные значения должны быть завернуты + +Код, который зависит от случайности или от глобального состояния, которое может +измениться, должен обернуть все обращения к нему в вызовы +`conversation.external()` или использовать удобную функцию +`conversation.random()`. + +```ts +// ПЛОХО +if (Math.random() < 0.5) { /* сделать что-то */ } +// ХОРОШО +if (conversation.random() < 0.5) { /* сделать что-то */ } +``` + +### Правило III: Используйте удобные функции + +На `conversation` установлена куча вещей, которые могут сильно помочь вам. Ваш +код иногда даже не ломается, если вы не используете их, но даже тогда он может +быть медленным или вести себя непонятным образом. + +```ts +// `ctx.session` сохраняет изменения только для самого последнего объекта контекста +conversation.session.myProp = 42; // надежнее! + +// Date.now() может быть неточным внутри обращений +await conversation.now(); // более точно! + +// Ведение журнала отладки через разговор, не печатает запутанные логи +conversation.log("Привет, мир"); // более прозрачно! +``` + +Обратите внимание, что большинство из вышеперечисленных действий можно выполнить +через `conversation.external()`, но это может быть утомительно, поэтому проще +использовать удобные функции +([документация API](/ref/conversations/conversationhandle#methods)). + +## Переменные, ветвление и циклы + +Если вы следуете трем вышеперечисленным правилам, вы можете использовать любой +код, который вам нравится. Сейчас мы рассмотрим несколько концепций, которые вы +уже знаете из программирования, и покажем, как они применяются в чистых и +читабельных беседах. + +Представьте, что весь приведенный ниже код написан внутри функции построения +беседы. + +Вы можете объявлять переменные и делать с ними все, что захотите: + +```ts +await ctx.reply("Присылайте мне свои любимые номера, разделяя их запятыми!"); +const { message } = await conversation.waitFor("message:text"); +const sum = message.text + .split(",") + .map((n) => parseInt(n.trim(), 10)) + .reduce((x, y) => x + y); +await ctx.reply("Сумма этих чисел равна: " + sum); +``` + +Разветвление тоже работает: + +```ts +await ctx.reply("Пришлите мне фотографию!"); +const { message } = await conversation.wait(); +if (!message?.photo) { + await ctx.reply("Это не фотография! Я ухожу!"); + return; +} +``` + +Как и циклы: + +```ts +do { + await ctx.reply("Пришлите мне фотографию!"); + ctx = await conversation.wait(); + + if (ctx.message?.text === "/cancel") { + await ctx.reply("Отмена, ухожу!"); + return; + } +} while (!ctx.message?.photo); +``` + +## Функции и рекурсии + +Вы также можете разделить свой код на несколько функций и использовать их +повторно. Например, так можно определить многоразовую капчу. + +::: code-group + +```ts [TypeScript] +async function captcha(conversation: MyConversation, ctx: MyContext) { + await ctx.reply( + "Докажите, что вы человек! Что является ответом на все вопросы?", + ); + const { message } = await conversation.wait(); + return message?.text === "42"; +} +``` + +```js [JavaScript] +async function captcha(conversation, ctx) { + await ctx.reply( + "Докажите, что вы человек! Что является ответом на все вопросы?", + ); + const { message } = await conversation.wait(); + return message?.text === "42"; +} +``` + +::: + +Она возвращает `true`, если пользователь может пройти, и `false` в противном +случае. Теперь вы можете использовать его в своей основной функции построения +разговора следующим образом: + +::: code-group + +```ts [TypeScript] +async function enterGroup(conversation: MyConversation, ctx: MyContext) { + const ok = await captcha(conversation, ctx); + + if (ok) await ctx.reply("Добро пожаловать!"); + else await ctx.banChatMember(); +} +``` + +```js [JavaScript] +async function enterGroup(conversation, ctx) { + const ok = await captcha(conversation, ctx); + + if (ok) await ctx.reply("Добро пожаловать!"); + else await ctx.banChatMember(); +} +``` + +::: + +Посмотрите, как функция captcha может быть повторно использована в разных местах +вашего кода. + +> Этот простой пример предназначен только для того, чтобы проиллюстрировать +> работу функций. В реальности он может работать плохо, потому что он только +> ожидает нового обновления из соответствующего чата, но не проверяет, что оно +> действительно исходит от того же пользователя, который присоединился. Если вы +> хотите создать настоящую капчу, вы можете использовать +> [параллельные диалоги](#параллельные-диалоги). + +При желании вы можете разделить код на еще большее количество функций или +использовать рекурсию, взаимную рекурсию, генераторы и так далее. (Только +убедитесь, что все функции следуют +[трем правилам](#три-золотых-правила-ведения-диалога)). + +Естественно, вы можете использовать обработку ошибок и в своих функциях. Обычные +операторы `try`/`catch` прекрасно работают, в том числе и в функциях. В конце +концов, беседы --- это всего лишь JavaScript. + +Если главная функция беседы выкинет ошибку, она распространится дальше в +[механизмы обработки ошибок](../guide/errors) вашего бота. + +## Модули и классы + +Естественно, вы можете просто перемещать свои функции между модулями. Таким +образом, вы можете определить некоторые функции в одном файле, `экспортировать` +их, а затем `импортировать` и использовать их в другом файле. + +При желании вы также можете определять классы. + +::: code-group + +```ts [TypeScript] +class Auth { + public token?: string; + + constructor(private conversation: MyConversation) {} + + authenticate(ctx: MyContext) { + const link = getAuthLink(); // получите ссылку авторизации из вашей системы + await ctx.reply( + "Откройте эту ссылку, чтобы получить токен, и отправьте его мне обратно: " + + link, + ); + ctx = await this.conversation.wait(); + this.token = ctx.message?.text; + } + + isAuthenticated(): this is Auth & { token: string } { + return this.token !== undefined; + } +} + +async function askForToken(conversation: MyConversation, ctx: MyContext) { + const auth = new Auth(conversation); + await auth.authenticate(ctx); + if (auth.isAuthenticated()) { + const token = auth.token; + // делать что-то с токеном + } +} +``` + +```js [JavaScript] +class Auth { + constructor(conversation) { + this.#conversation = conversation; + } + + authenticate(ctx) { + const link = getAuthLink(); // получите ссылку авторизации из вашей системы + await ctx.reply( + "Откройте эту ссылку, чтобы получить токен, и отправьте его мне обратно: " + + link, + ); + ctx = await this.#conversation.wait(); + this.token = ctx.message?.text; + } + + isAuthenticated() { + return this.token !== undefined; + } +} + +async function askForToken(conversation, ctx) { + const auth = new Auth(conversation); + await auth.authenticate(ctx); + if (auth.isAuthenticated()) { + const token = auth.token; + // делать что-то с токеном + } +} +``` + +::: + +Дело не столько в том, что мы строго рекомендуем вам так поступать. Это скорее +пример того, как можно использовать бесконечную гибкость JavaScript для +структурирования кода. + +## Формы + +Как уже упоминалось [ранее](#ожидание-обновлении), существует несколько +различных вспомогательных функций на обработчике беседы, таких как +`await conversation.waitFor('message:text')`, которая возвращает только +обновления текстовых сообщений. + +Если этих методов недостаточно, плагин conversations предоставляет еще больше +вспомогательных функций для создания форм через `conversation.form`. + +::: code-group + +```ts [TypeScript] +async function waitForMe(conversation: MyConversation, ctx: MyContext) { + await ctx.reply("Сколько вам лет?"); + const age: number = await conversation.form.number(); +} +``` + +```js [JavaScript] +async function waitForMe(conversation, ctx) { + await ctx.reply("Сколько вам лет?"); + const age = await conversation.form.number(); +} +``` + +::: + +Как всегда, ознакомьтесь с +[документацией API](/ref/conversations/conversationform), чтобы узнать, какие +методы доступны. + +## Работа с плагинами + +Как уже упоминалось [ранее](#введение), обработчики grammY всегда обрабатывают +только одно обновление. Однако с помощью бесед вы можете обрабатывать множество +обновлений последовательно, как если бы все они были доступны в одно и то же +время. Плагин делает это возможным, сохраняя старые контекстные объекты и +предоставляя их позже. Именно поэтому контекстные объекты внутри бесед не всегда +подвержены влиянию некоторых плагинов grammY так, как можно было бы ожидать. + +:: warning Интерактивные меню внутри разговоров С плагином [menu plugin](./menu) +эти концепции очень сильно конфликтуют. Хотя меню _могут_ работать внутри бесед, +мы не рекомендуем использовать эти два плагина вместе. Вместо этого используйте +обычный плагин [встроенной клавиатуры](./keyboard#встроенные-клавиатуры) (пока +мы не добавим встроенную поддержку меню в беседах). Вы можете ожидать +определенных запросов обратного вызова с помощью +`await conversation.waitForCallbackQuery("my-query")` или любого запроса с +помощью `await conversation.waitFor("callback_query")`. + +```ts +const keyboard = new InlineKeyboard() + .text("A", "a").text("B", "b"); +await ctx.reply("A или B?", { reply_markup: keyboard }); +const response = await conversation.waitForCallbackQuery(["a", "b"], { + otherwise: (ctx) => + ctx.reply("Используйте кнопки!", { reply_markup: keyboard }), +}); +if (response.match === "a") { + // Пользователь выбрал "A". +} else { + // Пользователь выбрал "B". +} +``` + +::: + +Другие плагины работают нормально. Некоторые из них просто нужно установить не +так, как вы обычно это делаете. Это относится к следующим плагинам: + +- [hydrate](./hydrate) +- [i18n](./i18n) и [fluent](./fluent) +- [emoji](./emoji) + +Их объединяет то, что все они хранят функции на объекте контекста, которые +плагин conversations не может обрабатывать корректно. Поэтому, если вы хотите +объединить беседы с одним из этих плагинов grammY, вам придется использовать +специальный синтаксис для установки другого плагина внутри каждой беседы. + +Вы можете установить другие плагины внутри бесед с помощью `conversation.run`: + +::: code-group + +```ts [TypeScript] +async function convo(conversation: MyConversation, ctx: MyContext) { + // Установите плагины grammY здесь + await conversation.run(plugin()); + // Продолжайте диалог ... +} +``` + +```js [JavaScript] +async function convo(conversation, ctx) { + // Установите плагины grammY здесь + await conversation.run(plugin()); + // Продолжайте диалог ... +} +``` + +::: + +Это сделает плагин доступным внутри беседы. + +### Пользовательские объекты контекста + +Если вы используете +[пользовательский контекстный объект](../guide/context#кастомизация-объекта-контекста) +и хотите установить пользовательские свойства на свои контекстные объекты перед +вводом беседы, то некоторые из этих свойств тоже могут быть потеряны. В +некотором смысле middleware, который вы используете для настройки контекстного +объекта, тоже можно рассматривать как плагин. + +Самое чистое решение - полностью **отказаться от использования пользовательских +свойств контекста** или, по крайней мере, устанавливать только сериализуемые +свойства на объект контекста. Другими словами, если все пользовательские +свойства контекста могут быть сохранены в базе данных и впоследствии +восстановлены, вам не нужно ни о чем беспокоиться. + +Как правило, существуют другие решения проблем, которые обычно решаются с +помощью пользовательских свойств контекста. Например, часто можно просто +получить их в самом разговоре, а не в обработчике. + +Если ничего из перечисленного вам не подходит, вы можете попробовать +самостоятельно разобраться с `conversation.run`. Следует знать, что вы должны +вызывать `next` внутри переданного middleware --- в противном случае обработка +обновлений будет перехвачена. + +Middleware будет выполняться для всех прошлых обновлений каждый раз, когда +приходит новое обновление. Например, если приходят три контекстных объекта, то +происходит следующее: + +1. получено первое обновление +2. middleware работает для первого обновления +3. получено второе обновление +4. middleware запускается для первого обновления +5. middleware запускается для второго обновления +6. получено третье обновление +7. middleware запускается для первого обновления +8. middleware запускается для второго обновления +9. middleware запускается для третьего обновления + +Обратите внимание, что middleware запускается с первым обновлением трижды. + +## Параллельные диалоги + +Естественно, плагин conversations может запускать любое количество бесед +параллельно в разных чатах. + +Однако если ваш бот добавлен в групповой чат, он может захотеть вести +параллельные беседы с несколькими разными пользователями _в одном и том же +чате_. Например, если ваш бот содержит капчу, которую он хочет отправлять всем +новым пользователям. Если два пользователя присоединяются одновременно, бот +должен иметь возможность вести с ними две независимые беседы. + +Именно поэтому плагин conversations позволяет вводить несколько бесед +одновременно для каждого чата. Например, можно вести пять разных бесед с пятью +новыми пользователями и в то же время общаться с администратором по поводу новой +конфигурации чата. + +### Как это работает под капотом + +Каждое входящее обновление будет обработано только одной из активных бесед в +чате. По аналогии с обработчиками middleware, беседы будут вызываться в том +порядке, в котором они зарегистрированы. Если беседа запускается несколько раз, +то эти экземпляры беседы будут вызываться в хронологическом порядке. + +Каждая беседа может либо обработать обновление, либо вызвать +`await conversation.skip()`. В первом случае обновление будет просто пропущено, +пока разговор обрабатывает его. Во втором случае разговор фактически отменит +получение обновления и передаст его следующему разговору. Если все разговоры +пропустят обновление, поток управления будет передан обратно в систему +middleware и запустит все последующие обработчики. + +Это позволяет начать новый разговор из обычного middleware. + +### Как вы можете это использовать + +На практике вам вообще не нужно вызывать `await conversation.skip()`. Вместо +этого вы можете просто использовать такие вещи, как +`await conversation.waitFrom(userId)`, которые позаботятся о деталях за вас. Это +позволяет общаться в групповом чате только с одним пользователем. + +Например, давайте снова реализуем пример с капчей, но на этот раз с +параллельными беседами. + +::: code-group + +```ts{4} [TypeScript] +async function captcha(conversation: MyConversation, ctx: MyContext) { + if (ctx.from === undefined) return false; + await ctx.reply("Докажите, что вы человек! Что является ответом на все вопросы?"); + const { message } = await conversation.waitFrom(ctx.from); + return message?.text === "42"; +} + +async function enterGroup(conversation: MyConversation, ctx: MyContext) { + const ok = await captcha(conversation, ctx); + + if (ok) await ctx.reply("Добро пожаловать!"); + else await ctx.banChatMember(); +} +``` + +```js{4} [JavaScript] +async function captcha(conversation, ctx) { + if (ctx.from === undefined) return false; + await ctx.reply("Докажите, что вы человек! Что является ответом на все вопросы?"); + const { message } = await conversation.waitFrom(ctx.from); + return message?.text === "42"; +} + +async function enterGroup(conversation, ctx) { + const ok = await captcha(conversation, ctx); + + if (ok) await ctx.reply("Добро пожаловать!"); + else await ctx.banChatMember(); +} +``` + +::: + +Обратите внимание, что мы ждем сообщений только от конкретного пользователя. + +Теперь мы можем создать простой обработчик, который вступает в разговор, когда к +нему присоединяется новый участник + +```ts +bot.on("chat_member") + .filter((ctx) => ctx.chatMember.old_chat_member.status === "left") + .filter((ctx) => ctx.chatMember.new_chat_member.status === "member") + .use((ctx) => ctx.conversation.enter("enterGroup")); +``` + +### Проверка активных диалогов + +Вы можете увидеть, сколько разговоров с каким идентификатором запущено. + +```ts +const stats = await ctx.conversation.active(); +console.log(stats); // { "enterGroup": 1 } +``` + +Он будет предоставлен в виде объекта, ключами которого являются идентификаторы +разговоров, а число указывает на количество запущенных разговоров для каждого +идентификатора. + +## Как это работает + +> [Помните](#три-золотых-правила-ведения-диалога), что код внутри ваших функций +> построения разговоров должен следовать трем правилам. Сейчас мы рассмотрим, +> _почему_ их нужно строить именно так. + +Сначала мы посмотрим, как этот плагин работает концептуально, а затем +остановимся на некоторых деталях. + +### Как вызов `wait` работает + +Давайте немного поменяем точку зрения и зададим вопрос с точки зрения +разработчика плагина. Как реализовать вызов `wait` в плагине? + +Наивным подходом к реализации вызова `wait` в плагине conversations было бы +создание нового promise и ожидание прибытия следующего контекстного объекта. Как +только он появится, мы решим promise, и разговор может быть продолжен. + +Однако это плохая идея по нескольким причинам. + +**Потеря данных.** Что, если ваш сервер упадет во время ожидания контекстного +объекта? В этом случае мы потеряем всю информацию о состоянии беседы. По сути, +бот теряет ход своих мыслей, и пользователю приходится начинать все сначала. Это +плохой и раздражающий дизайн. + +**Блокировка.** Если вызовы wait блокируются до прихода следующего обновления, +это означает, что выполнение middleware для первого обновления не может +завершиться до тех пор, пока не завершится весь разговор. + +- Для встроенного polling это означает, что никакие последующие обновления не + могут быть обработаны, пока не завершится текущее. Таким образом, бот будет + просто заблокирован навсегда. +- Для [grammY runner](./runner) бот не будет заблокирован. Однако при + параллельной обработке тысяч разговоров с разными пользователями он будет + занимать потенциально очень большой объем памяти. Если многие пользователи + перестанут отвечать, бот застрянет посреди бесчисленных разговоров. +- Вебхуки имеют целую + [категорию проблем](../guide/deployment-types#своевременное-завершение-запросов-вебхуков) + с долго работающим middleware. + +**Состояние.** В бессерверной инфраструктуре, такой как облачные функции, мы не +можем предположить, что один и тот же экземпляр обрабатывает два последующих +обновления от одного и того же пользователя. Следовательно, если мы создадим +разговоры с состоянием, они могут постоянно случайно ломаться, поскольку +некоторые вызовы `wait` не разрешаются, но внезапно выполняется другой +middleware. В результате мы получим обилие случайных ошибок и хаос. + +Есть и другие проблемы, но вы поняли, о чем идет речь. + +Следовательно, плагин conversations делает все по-другому. Очень по-другому. Как +уже говорилось ранее, вызовы **`wait` не заставляют бота ждать**, хотя мы можем +запрограммировать разговоры так, как будто это так и есть. + +Плагин conversations отслеживает выполнение вашей функции. Когда достигается +вызов ожидания, он преобразовывает состояние выполнения в сессию и надежно +сохраняет его в базе данных. Когда приходит следующее обновление, он сначала +проверяет данные сессии. Если он обнаружит, что прервался на середине разговора, +он преобразовывает состояние выполнения, берет вашу функцию построения разговора +и воспроизводит его до момента последнего вызова `wait`. Затем он возобновляет +обычное выполнение вашей функции - до тех пор, пока не будет достигнут следующий +вызов `wait`, и выполнение снова должно быть остановлено. + +Что мы понимаем под состоянием выполнения? В двух словах, оно состоит из трех +вещей: + +1. Входящие обновления +2. Исходящие вызовы API +3. Внешние события и эффекты, такие как случайность или обращения к внешним API + или базам данных + +Что мы имеем в виду под воспроизведением? Воспроизведение означает регулярный +вызов функции с самого начала, но когда она делает такие вещи, как вызов `wait` +или выполнение вызовов API, мы на самом деле не делаем ничего из этого. Вместо +этого мы проверяем логи, где записано, какие значения были возвращены при +предыдущем запуске. Затем мы вставляем эти значения, чтобы функция построения +разговора просто выполнялась очень быстро - до тех пор, пока наши логи не +иссякнут. В этот момент мы переключаемся обратно в обычный режим выполнения и +начинаем снова выполнять вызовы API. + +Вот почему плагин должен отслеживать все входящие обновления, а также все вызовы +Bot API. (См. пункты 1 и 2 выше.) Однако плагин не может контролировать внешние +события, побочные эффекты или случайности. Например, вы можете сделать +следующее: + +```ts +if (Math.random() < 0.5) { + // делать одно +} else { + // делать другое +} +``` + +В этом случае при вызове функции она может внезапно вести себя каждый раз +по-разному, так что повторное выполнение функции будет нарушено! Она может +случайно сработать не так, как при первоначальном выполнении. Вот почему +существует пункт 3 и необходимо следовать +[Трем золотым правилам](#три-золотых-правила-ведения-диалога). + +### Как перехватить выполнение функции + +Концептуально говоря, ключевые слова `async` и `await` дают нам контроль над +тем, где поток будет +[вытеснен](https://en.wikipedia.org/wiki/Preemption_(computing)). Следовательно, +если кто-то вызывает `await conversation.wait()`, которая является функцией +нашей библиотеки, мы получаем возможность упредить ее выполнение. + +Говоря конкретнее, секретный основной примитив, позволяющий нам прерывать +выполнение функции, --- это `Promise`, который никогда не решается. + +```ts +await new Promise(() => {}); // БУМ +``` + +Если в любом JavaScript-файле выполнить `await` такого promise, то выполнение +мгновенно завершится. (Не стесняйтесь вставить приведенный выше код в файл и +опробовать его). + +Поскольку мы, очевидно, не хотим завершать выполнения JS, мы должны поймать это +снова. Как бы вы поступили в этом случае? (Не стесняйтесь заглянуть в исходный +код плагина, если это не сразу понятно). + +## Краткая информация о плагине + +- Название: `conversations` +- [Исходник](https://github.com/grammyjs/conversations) +- [Ссылка](/ref/conversations/) diff --git a/site/docs/ru/plugins/emoji.md b/site/docs/ru/plugins/emoji.md new file mode 100644 index 000000000..cfe3b8c20 --- /dev/null +++ b/site/docs/ru/plugins/emoji.md @@ -0,0 +1,132 @@ +--- +prev: false +next: false +--- + +# Плагин эмодзи (`emoji`) + +С помощью этого плагина вы можете легко вставлять эмодзи в свои ответы, находя +их, вместо того чтобы вручную копировать и вставлять эмодзи из Интернета в свой +код. + +## Почему я должен использовать это? + +Почему бы и нет? Люди постоянно используют эмодзи в своем коде, чтобы лучше +проиллюстрировать сообщение, которое они хотят передать, или чтобы упорядочить +вещи. Но вы теряете фокус каждый раз, когда вам нужен новый эмодзи: + +1. Вы прекращаете программировать для поиска конкретного эмодзи. +2. Вы заходите в чат Telegram и тратите ~6 секунд (если не больше) на поиск + нужного вам эмодзи. +3. Вы копируете и вставляете их в свой код и возвращаетесь к программированию, + но уже с потерянным фокусом. + +С этим плагином вы не только не перестанете писать код, но и не потеряете фокус. +Есть также плохо работающие системы и/или редакторы, которые не любят и не +показывают эмодзи, поэтому в итоге вы вставляете белый квадрат, как в этом +грустном сообщении: `Я так счастлив □`. + +Этот плагин призван решить эти проблемы, выполняя за вас сложную задачу по +разбору эмодзи во всех системах и позволяя вам только искать их простым способом +(доступно автоподсказка). Теперь все описанные выше действия можно свести к +одному: + +1. Опишите нужный вам эмодзи и используйте его. Прямо в вашем коде. Все просто. + +### Это что, колдовство? + +Нет, это называется шаблонными строками. Подробнее о них вы можете прочитать +[здесь](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals). + +## Установка и примеры + +Вы можете установить этот плагин на своего бота следующим образом: + +::: code-group + +```ts [TypeScript] +import { Bot, Context } from "grammy"; +import { EmojiFlavor, emojiParser } from "@grammyjs/emoji"; + +// Это называется расширители контекста +// Подробнее об этом вы можете прочитать на: +// https://grammy.dev/guide/context#transformative-context-flavors +type MyContext = EmojiFlavor; + +const bot = new Bot(""); + +bot.use(emojiParser()); +``` + +```js [JavaScript] +const { Bot } = require("grammy"); +const { emojiParser } = require("@grammyjs/emoji"); + +const bot = new Bot(""); + +bot.use(emojiParser()); +``` + +```ts [Deno] +import { Bot, Context } from "https://deno.land/x/grammy/mod.ts"; +import { + EmojiFlavor, + emojiParser, +} from "https://deno.land/x/grammy_emoji/mod.ts"; + +// Это называется расширители контекста +// Подробнее об этом вы можете прочитать на: +// https://grammy.dev/guide/context#transformative-context-flavors +type MyContext = EmojiFlavor; + +const bot = new Bot(""); + +bot.use(emojiParser()); +``` + +::: + +Теперь вы можете получать эмодзи по их именам: + +```js +bot.command("start", async (ctx) => { + const parsedString = ctx + .emoji`Добро пожаловать! ${"smiling_face_with_sunglasses"}`; // => Добро пожаловать! 😎 + await ctx.reply(parsedString); +}); +``` + +Кроме того, вы можете ответить напрямую, используя метод `replyWithEmoji`: + +```js +bot.command("ping", async (ctx) => { + await ctx.replyWithEmoji`Понг ${"ping_pong"}`; // => Понг 🏓 +}); +``` + +::: warning Имейте в виду, что `ctx.emoji` и `ctx.replyWithEmoji` **ВСЕГДА** +используют шаблонные строки. Если вы не знакомы с этим синтаксисом, вы можете +прочитать о нем подробнее +[здесь](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals). +::: + +## Полезные данные для реакций + +Когда вы используете [реакции](../guide/reactions) в своем боте, вам придется +много программировать и с эмодзи! Это не менее раздражает, и так как этот +плагин --- это влажные сны всех ваших эмодзи, он может помочь вам и с реакциями. + +Вы можете импортировать `Reactions` из этого плагина и затем использовать его +следующим образом. + +```ts +bot.on("message", (ctx) => ctx.react(Reactions.thumbs_up)); +``` + +Как мило. + +## Краткая информация о плагине + +- Название: `emoji` +- [Исходник](https://github.com/grammyjs/emoji) +- [Ссылка](/ref/emoji/) diff --git a/site/docs/ru/plugins/files.md b/site/docs/ru/plugins/files.md new file mode 100644 index 000000000..f33d284ed --- /dev/null +++ b/site/docs/ru/plugins/files.md @@ -0,0 +1,147 @@ +--- +prev: false +next: false +--- + +# Упрощенная работа с файлами в grammY (`files`) + +Этот плагин позволяет легко загружать файлы с серверов Telegram, а также получать URL-адрес, чтобы вы могли скачать файл самостоятельно. + +> [Помните](../guide/files) как работают файлы и как их загружать. + +## Скачивание файлов + +Вам нужно передать токен вашего бота этому плагину, потому что он должен аутентифицироваться как ваш бот, когда загружает файлы. +Затем этот плагин устанавливает метод `download` на результаты вызова `getFile`. +Пример: + +::: code-group + +```ts [TypeScript] +import { Bot, Context } from "grammy"; +import { FileFlavor, hydrateFiles } from "@grammyjs/files"; + +// Трансформирующий расширитель контекста +type MyContext = FileFlavor; + +// Создайте бота. +const bot = new Bot(""); + +// Используйте плагин. +bot.api.config.use(hydrateFiles(bot.token)); + +// Загружайте видео и GIF-файлы во временные локации +bot.on([":video", ":animation"], async (ctx) => { + // Подготовьте файл к загрузке. + const file = await ctx.getFile(); + // Загрузите файл во временную локацию + const path = await file.download(); + // Выведите путь к файлу. + console.log("Файл сохранён в ", path); +}); +``` + +```js [JavaScript] +import { Bot } from "grammy"; +import { hydrateFiles } from "@grammyjs/files"; + +// Создайте бота. +const bot = new Bot(""); + +// Используйте плагин. +bot.api.config.use(hydrateFiles(bot.token)); + +// Загружайте видео и GIF-файлы во временные локации +bot.on([":video", ":animation"], async (ctx) => { + // Подготовьте файл к загрузке. + const file = await ctx.getFile(); + // Загрузите файл во временную локацию + const path = await file.download(); + // Выведите путь к файлу. + console.log("Файл сохранён в ", path); +}); +``` + +```ts [Deno] +import { Bot, Context } from "https://deno.land/x/grammy/mod.ts"; +import { + FileFlavor, + hydrateFiles, +} from "https://deno.land/x/grammy_files/mod.ts"; + +// Трансформирующий расширитель контекста +type MyContext = FileFlavor; + +// Создайте бота. +const bot = new Bot(""); + +// Используйте плагин. +bot.api.config.use(hydrateFiles(bot.token)); + +// Загружайте видео и GIF-файлы во временные локации +bot.on([":video", ":animation"], async (ctx) => { + // Подготовьте файл к загрузке. + const file = await ctx.getFile(); + // Загрузите файл во временную локацию + const path = await file.download(); + // Выведите путь к файлу. + console.log("Файл сохранён в ", path); +}); +``` + +::: + +Вы можете передать строку с путем к файлу в `download`, если не хотите создавать временный файл. +Просто сделайте `await file.download("/path/to/file")`. + +Если вам нужно получить только URL-адрес файла, чтобы вы могли скачать его самостоятельно, используйте `file.getUrl`. +Это вернет HTTPS ссылку на ваш файл, которая будет действительна в течение как минимум одного часа. + +## Локальный API сервер бота + +Если вы используете [локальный сервер Bot API](https://core.telegram.org/bots/api#using-a-local-bot-api-server), то вызов `getFile` фактически уже загружает файл на ваш диск. + +В свою очередь, вы можете вызвать `file.getUrl()` для доступа к этому пути к файлу. +Обратите внимание, что `await file.download()` теперь просто скопирует этот локально присутствующий файл во временное место (или по заданному пути, если он указан). + +## Поддержка вызовов `bot.api` + +По умолчанию результаты `await bot.api.getFile()` будут также оснащены методами `download` и `getUrl`. +Однако это не отражено в типах. +Если вам нужны эти вызовы, вы должны также установить [расширители API](../advanced/transformers#расширитель-api) на объект бота под названием `FileApiFlavor`: + +::: code-group + +```ts [Node.js] +import { Api, Bot, Context } from "grammy"; +import { FileApiFlavor, FileFlavor, hydrateFiles } from "@grammyjs/files"; + +type MyContext = FileFlavor; +type MyApi = FileApiFlavor; + +const bot = new Bot(""); +// ... +``` + +```ts [Deno] +import { Api, Bot, Context } from "https://deno.land/x/grammy/mod.ts"; +import { + FileApiFlavor, + FileFlavor, + hydrateFiles, +} from "https://deno.land/x/grammy_files/mod.ts"; + +type MyContext = FileFlavor; +type MyApi = FileApiFlavor; + +const bot = new Bot(""); +// ... +``` + +::: + +## Краткая информация о плагине + +- Название: `files` +- [Исходник](https://github.com/grammyjs/files) +- [Ссылка](/ref/files/) diff --git a/site/docs/ru/plugins/fluent.md b/site/docs/ru/plugins/fluent.md new file mode 100644 index 000000000..61ad12bf8 --- /dev/null +++ b/site/docs/ru/plugins/fluent.md @@ -0,0 +1,181 @@ +--- +prev: false +next: false +--- + +# Интернационализация с помощью Fluent (`fluent`) + +[Fluent](https://projectfluent.org/) это система локализации, созданная Mozilla +Foundation для создания естественных переводов. Она обладает очень мощным и +элегантным синтаксисом, позволяющим любому человеку писать эффективные и +полностью понятные переводы. Этот плагин использует преимущества этой +удивительной системы локализации, чтобы сделать ботов, работающих на grammY, +способными делать высококачественные переводы. + +::: tip Не путать Не путайте это с [i18n](./i18n). + +[i18n](./i18n) это улучшенная версия этого плагина, которая работает как на +Deno, так и на Node.js. +::: + +## Инициализация Fluent + +Первое, что вы делаете, - инициализируете Fluent: + +```ts +import { Fluent } from "@moebius/fluent"; + +const fluent = new Fluent(); +``` + +Затем нужно добавить хотя бы один перевод в экземпляр Fluent: + +```ts +await fluent.addTranslation({ + // Укажите одну или несколько локалей, поддерживаемых вашим переводом: + locales: "ru", + + // Вы можете напрямую указать контент перевода: + source: "{СОДЕРЖАНИЕ ФАЙЛА ПЕРЕВОДА}", + + // Или файлы перевода: + filePath: [ + `${__dirname}/feature-1/translation.ru.ftl`, + `${__dirname}/feature-2/translation.ru.ftl`, + ], + + // Все аспекты Fluent очень хорошо настраиваются: + bundleOptions: { + // Используйте этот параметр, чтобы избежать появления невидимых символов вокруг размещаемых объектов. + useIsolating: false, + }, +}); +``` + +## Написание переведённх сообщений + +Синтаксис Fluent должен быть прост в освоении. Вы можете начать с просмотра +[официальных примеров](https://projectfluent.org/#examples) или с изучения +[полного руководства по синтаксису](https://projectfluent.org/fluent/guide/). + +Давайте пока начнем с этого примера: + +```ftl +-bot-name = Apples Bot + +welcome = + Добро пожаловать, {$name}, это {-bot-name}! + У вас { NUMBER($applesCount) -> + [0] нет яблок + [one] {$applesCount} яблоко + *[other] {$applesCount} яблок(а) + }. +``` + +Он демонстрирует три важные особенности Fluent, а именно: **термины**, **замена +переменных** (она же _замещаемые_) и **плюрализация**. + +Переменная `welcome` --- это **идентификатор сообщения**, который будет +использоваться для ссылки на это сообщение при его отправке. + +Переменная `-bot-name = Apples Bot` определяет **переменную** с именем +`bot-name` и значением `Apples Bot`. Конструкция `{-bot-name}` ссылается на +ранее определенную переменную и при отправке сообщения будет заменена его +значением. + +Переменная `{$name}` будет заменена значением переменной `name`, которой вам +нужно будет передать функции перевода самостоятельно. + +И последнее Переменная (_строки с 5 по 9_) определяет **селектор** (очень похоже +на оператор switch), который принимает результат специальной функции `NUMBER`, +примененной к переменной `applesCount`, и выбирает одно из трех возможных +сообщений для отображения на основе совпавшего значения. Функция `NUMBER` +возвращает категорию +[CLDR plural category](https://www.unicode.org/cldr/cldr-aux/charts/30/supplemental/language_plural_rules.html), +основанную на предоставленном значении и используемоем переводе. Это эффективно +реализует плюрализацию. + +## Конфигурация grammY + +Теперь давайте посмотрим, как это сообщение может быть отображено ботом. Но +сначала нам нужно настроить grammY на использование плагина. + +Прежде всего, вам нужно настроить бота на использование контекстного расширителя +Fluent. Если вы не знакомы с этой концепцией, вам следует прочитать официальную +документацию по [Расширители контекста](../guide/context#расширители-контекста). + +```ts +import { Context } from "grammy"; +import { FluentContextFlavor } from "@grammyjs/fluent"; + +// Расширьте тип контекста вашего приложения с помощью предоставляемого расширителя контекста +export type MyAppContext = Context & FluentContextFlavor; +``` + +Чтобы использовать дополненный тип контекста, вам нужно создать экземпляр бота +следующим образом: + +```ts +const bot = new Bot(""); +``` + +И последним шагом будет регистрация самого плагина Fluent в grammY: + +```ts +bot.use( + useFluent({ + fluent, + }), +); +``` + +Обязательно передайте [ранее созданный экземпляр Fluent](#инициализация-fluent). + +## Передача локализованных сообщений + +Отлично, теперь у нас есть все необходимое для вывода наших сообщений! Давайте +сделаем это, определив тестовую команду в нашем боте: + +```ts +bot.command("i18n_test", async (ctx) => { + // Вызовите помощник "translate" или "t" для отображения + // сообщения, указав идентификатор перевода и дополнительные параметры: + await ctx.reply( + ctx.t("welcome", { + name: ctx.from.first_name, + applesCount: 1, + }), + ); +}); +``` + +Теперь вы можете запустить бота и использовать команду `/i18n_test`. Должно +появиться следующее сообщение: + +```text +Добро пожаловать, Slava, это Apples Bot! +У вас 1 яблоко. +``` + +Конечно, вместо "Slava" вы увидите свое собственное имя. Попробуйте изменить +значение переменной `applesCount` и посмотрите, как изменится отображаемое +сообщение! + +Имейте в виду, что теперь вы можете использовать функцию перевода везде, где +доступен `Context`. Библиотека автоматически определит язык для каждого +пользователя, который будет взаимодействовать с вашим ботом, основываясь на его +установленном языке в настройках Telegram. Вам нужно будет только создать +несколько файлов перевода и убедиться, что все переводы правильно +синхронизированы. + +## Следующие шаги + +- Завершите чтение [документации по Fluent](https://projectfluent.org/), + особенно [руководства по синтаксису](https://projectfluent.org/fluent/guide/). +- [Мигрируйте с плагина `i18n`](https://github.com/grammyjs/fluent#i18n-plugin-replacement). +- Ознакомьтесь с [`@moebius/fluent`](https://github.com/the-moebius/fluent#readme). + +## Краткая информация о плагине + +- Название: `fluent` +- [Исходник](https://github.com/grammyjs/fluent) diff --git a/site/docs/ru/plugins/guide.md b/site/docs/ru/plugins/guide.md new file mode 100644 index 000000000..b2f835802 --- /dev/null +++ b/site/docs/ru/plugins/guide.md @@ -0,0 +1,224 @@ +--- +next: false +--- + +# Руководство по плагинам grammY для чайников + +Если вы хотите разработать свой собственный плагин и опубликовать его, или если вы хотите узнать, как плагины grammY работают под капотом, это место для вас! + +> Обратите внимание, что уже есть краткое описание [grammY-плагинов](./) и того, что они делают. +> Эта статья - глубокое погружение в их внутреннюю работу. + +## Типы плагинов в grammY + +В grammY есть два основных типа плагинов: + +- Middleware-плагины: Единственная задача плагина - вернуть функцию [middleware function](../guide/middleware), которую можно передать боту grammY. +- Плагины-трансформеры: Единственная задача плагина - вернуть функцию [transformer function](../advanced/transformers), которая может быть передана боту grammY. + +Однако иногда встречаются плагины, которые выполняют обе задачи. +Существуют и другие плагины, которые не являются ни middleware, ни трансформирующими функциями, но мы все равно будем называть их плагинами, поскольку они расширяют grammY различными способами. + +## Правила внесения изменений + +Вы можете публиковать свои плагины в одной из следующих форм: + +- Публикация в качестве **официального** плагина. +- Публикация в качестве **третьего** плагина. + +Если вы решите опубликовать свои плагины от третьего лица, мы все равно сможем предложить вам заметное место на этом сайте. +Однако мы предпочитаем, чтобы вы опубликовали свой плагин под [именем grammyjs](https://github.com/grammyjs) на GitHub, тем самым сделав его официальным плагином. +В этом случае вам будет предоставлен доступ к GitHub и npm для публикации. +Кроме того, вы будете нести ответственность за поддержку своего кода. + +Прежде чем приступить к рассмотрению практических примеров, следует обратить внимание на некоторые правила, если вы хотите, чтобы ваши плагины были размещены на этом сайте. + +1. Иметь файл README на GitHub (и npm) с **короткими и понятными** инструкциями по использованию. +2. Объясните назначение вашего плагина и как его использовать, добавив страницу в [документацию](https://github.com/grammyjs/website). + (Мы можем создать страницу для вас, если вы не знаете, как это сделать). +3. Выберите лицензию, например MIT или ISC. + +Наконец, вы должны знать, что хотя grammY поддерживает как Node.js, так и [Deno](https://deno.com), это проект, ориентированный на Deno, и мы также поощряем вас писать свои плагины для Deno (и впоследствии в стиле!). +Существует удобный инструмент [deno2node](https://github.com/fromdeno/deno2node), который переносит ваш код из Deno в Node.js, так что мы можем поддерживать обе платформы одинаково хорошо. +Поддержка Deno является строгим требованием только для официальных плагинов, но не для сторонних. +Тем не менее, очень рекомендуем попробовать Deno. +Вы не захотите возвращаться назад. + +## Разработка фиктивного плагина Middleware + +Предположим, мы хотим разработать плагин, который отвечает только определенным пользователям! +Например, мы можем решить отвечать только тем, чье имя содержит определенное слово. +Для всех остальных бот просто откажется работать. + +Вот фиктивный пример: + +```ts +// plugin.ts + +// Импорт типов из grammY (мы реэкспортируем их в `deps.deno.ts`). +import type { Context, Middleware, NextFunction } from "./deps.deno.ts"; + +// Ваш плагин может иметь одну основную функцию, которая создает middleware +export function onlyAccept(str: string): Middleware { + // Создавайте и возвращайте middleware + return async (ctx, next) => { + // Получение имени пользователя. + const name = ctx.from?.first_name; + // Пропустите все подходящие обновления. + if (name === undefined || name.includes(str)) { + // Передача потока управления последующему middleware + await next(); + } else { + // Скажите им, что они нам не нравятся. + await ctx.reply(`Я с тобой не разговариваю! Тебе наплевать на ${str}!`); + } + }; +} +``` + +Теперь его можно использовать в настоящем боте: + +```ts +// Здесь код плагина находится в файле под названием `plugin.ts`. +import { onlyAccept } from "./plugin.ts"; +import { Bot } from "./deps.deno.ts"; + +const bot = new Bot(""); + +bot.use(onlyAccept("grammY")); + +bot.on("message", (ctx) => ctx.reply("Вы передали плагин middleware")); + +bot.start(); +``` + +Вуаля! +У вас есть плагин, верно? +Ну, не так быстро. +Нам еще нужно упаковать его, но перед этим давайте рассмотрим плагины-трансформеры. + +## Проектирование плагина фиктивного трансформатора + +Представьте, что вы пишете плагин, который автоматически отправляет соответствующее [действие чата](https://core.telegram.org/bots/api#sendchataction) всякий раз, когда бот отправляет документ. +Это значит, что пока ваш бот отправляет файл, пользователи будут автоматически видеть статус "_отправляю файл…_". +Довольно круто, правда? + +```ts +// plugin.ts +import type { Transformer } from "./deps.deno.ts"; + +// Основная функция плагина +export function autoChatAction(): Transformer { + // Создайте и верните трансформирующую функцию + return async (prev, method, payload, signal) => { + // Сохраните обработчик установленного интервала, чтобы мы могли очистить его позже. + let handle: ReturnType | undefined; + if (method === "sendDocument" && "chat_id" in payload) { + // Теперь мы знаем, что документ отправлен. + const actionPayload = { + chat_id: payload.chat_id, + action: "upload_document", + }; + // Повторная установка действия чата во время загрузки файла. + handle ??= setInterval(() => { + prev("sendChatAction", actionPayload).catch(console.error); + }, 5000); + } + + try { + // Запустите реальный метод из бота. + return await prev(method, payload, signal); + } finally { + // Очистите интервал, чтобы мы перестали отправлять действие чата клиенту. + clearInterval(handle); + } + }; +} +``` + +Теперь мы можем использовать его в реальном боте: + +```ts +import { Bot, InputFile } from "./deps.deno.ts"; +// Код плагина находится в файле `plugin.ts`. +import { autoChatAction } from "./plugin.ts"; + +// Создайте экземпляр бота. +const bot = new Bot(""); + +// Используйте плагин. +bot.api.config.use(autoChatAction()); + +bot.hears("отправь мне документ", async (ctx) => { + // Если пользователь отправит эту команду, мы вышлем ему pdf-файл (в демонстрационных целях) + await ctx.replyWithDocument(new InputFile("/tmp/document.pdf")); +}); + +// Запустите бота +bot.start(); +``` + +Теперь каждый раз, когда мы отправляем документ, действие чата `upload_document` будет отправляться нашему клиенту. +Обратите внимание, что это было сделано в демонстрационных целях. +Telegram рекомендует использовать действия чата только в тех случаях, когда "ответ от бота займет **заметное** количество времени". +Вероятно, вам не нужно устанавливать статус, если файл очень маленький, поэтому здесь можно провести некоторую оптимизацию. + +## Извлечение в плагин + +Какой бы тип плагина вы ни создали, его нужно упаковать в отдельный пакет. +Это довольно простая задача. +Нет никаких особых правил, как это сделать, и npm --- это ваша устрица, но для того, чтобы все было организовано, мы предлагаем вам шаблон. +Вы можете скачать код из [нашего репозитория шаблонов плагинов на GitHub](https://github.com/grammyjs/plugin-template) и начать разработку своего плагина, не тратя времени на настройку. + +Первоначально предложенная структура папок: + +```asciiart:no-line-numbers +plugin-template/ +├─ src/ +│ ├─ deps.deno.ts +│ ├─ deps.node.ts +│ └─ index.ts +├─ package.json +├─ tsconfig.json +└─ README.md +``` + +**`deps.deno.ts` и `deps.node.ts`**: Это для разработчиков, которые хотят написать плагин для Deno, а затем переписать его на Node.js. +Как уже упоминалось, мы используем инструмент `deno2node` для перевода нашего кода Deno в Node.js. +В `deno2node` есть функция, которая позволяет вам предоставлять ему файлы, специфичные для времени выполнения. +Эти файлы должны находиться рядом друг с другом и следовать структуре имен `*.deno.ts` и `*.node.ts`, как [объясняется в документации](https://github.com/fromdeno/deno2node#runtime-specific-code). +Вот почему существует два файла: `deps.deno.ts` и `deps.node.ts`. +Если есть какие-либо специфические для Node.js зависимости, поместите их в `deps.node.ts`, в противном случае оставьте его пустым. + +> _**Примечание**_: Вы также можете использовать другие инструменты, такие как [deno dnt](https://github.com/denoland/dnt), для передачи кода Deno или использования других структур папок. +> Инструментарий, который вы используете, не имеет значения, главное, что писать код для Deno стало лучше и проще. + +**`tsconfig.json`**: Это файл конфигурации компилятора TypeScript, используемый `deno2node` для компиляции вашего кода. +В качестве рекомендации в репозитории предоставлен файл по умолчанию. +Он соответствует конфигурации TypeScript, которую Deno использует внутри, и мы рекомендуем вам придерживаться этого. + +**`package.json`**: Файл package.json для npm-версии вашего плагина. +**Не забудьте изменить его в соответствии с вашим проектом**. + +**`README.md`**: Инструкции по использованию плагина. +**Обязательно измените его в соответствии с вашим проектом**. + +**`index.ts`**: Файл, содержащий вашу логику работу, то есть основной код плагина. + +## Существует шаблон + +Если вы хотите разработать плагин для grammY и не знаете, с чего начать, мы настоятельно рекомендуем воспользоваться кодом шаблона в [нашем репозитории](https://github.com/grammyjs/plugin-template). +Вы можете клонировать код для себя и начать кодить, основываясь на том, что было описано в этой статье. +Этот репозиторий также содержит некоторые дополнительные файлы, такие как `.editorconfig`, `LICENSE`, `.gitignore` и т.д., но вы можете удалить их. + +## Мне не нравится Deno + +Что ж, вы упускаете возможность! +Но вы также можете писать свои плагины только для Node.js. +Вы всё равно можете опубликовать плагин и включить его в список сторонних плагинов на этом сайте. +В этом случае вы можете использовать любую структуру папок, которая вам нравится (если она организована как любой другой проект npm). +Просто установите grammY через npm с помощью `npm install grammy` и начинайте кодить. + +## Как подать заявку? + +Если у вас есть готовый плагин, вы можете просто отправить запрос на выгрузку на GitHub (в соответствии с [Правилами внесения изменений](#правила-внесения-изменении)) или сообщить нам в [чат коммьюнити](https://t.me/grammyjs) для получения дальнейшей помощи. diff --git a/site/docs/ru/plugins/hydrate.md b/site/docs/ru/plugins/hydrate.md new file mode 100644 index 000000000..4f76f68fb --- /dev/null +++ b/site/docs/ru/plugins/hydrate.md @@ -0,0 +1,185 @@ +--- +prev: false +next: false +--- + +# Плагин Hydration для grammY (`hydrate`) + +Этот плагин добавляет полезные методы на два типа объектов, а именно + +1. результаты вызовов API, и +2. объекты в контекстном объекте `ctx`. + +Вместо того чтобы вызывать `ctx.api` или `bot.api` и вводить всевозможные +идентификаторы, теперь вы можете просто вызывать методы на объектах, и они будут +работать. Лучше всего это видно на примере. + +**БЕЗ** этого плагина: + +```ts +bot.on(":photo", async (ctx) => { + const statusMessage = await ctx.reply("Обрабатываю"); + await doWork(ctx.msg.photo); // длительная обработка изображения + await ctx.api.editMessageText( + ctx.chat.id, + statusMessage.message_id, + "Готово!", + ); + setTimeout( + () => + ctx.api.deleteMessage(ctx.chat.id, statusMessage.message_id).catch(() => { + // Ничего не делайте при ошибках. + }), + 3000, + ); +}); +``` + +**C** этим плагином: + +```ts +bot.on(":photo", async (ctx) => { + const statusMessage = await ctx.reply("Обрабатываю"); + await doWork(ctx.msg.photo); // длительная обработка изображения + await statusMessage.editText("Done!"); // очень просто! + setTimeout(() => statusMessage.delete().catch(() => {}), 3000); +}); +``` + +Неплохо, правда? + +## Установка + +Существует два способа установки этого плагина. + +### Простая установка + +Этот плагин можно установить простым способом, которого должно быть достаточно +для большинства пользователей. + +::: code-group + +```ts [TypeScript] +import { Bot, Context } from "grammy"; +import { hydrate, HydrateFlavor } from "@grammyjs/hydrate"; + +type MyContext = HydrateFlavor; + +const bot = new Bot(""); + +bot.use(hydrate()); +``` + +```js [JavaScript] +import { Bot } from "grammy"; +import { hydrate } from "@grammyjs/hydrate"; + +const bot = new Bot(""); + +bot.use(hydrate()); +``` + +```ts [Deno] +import { Bot, Context } from "https://deno.land/x/grammy/mod.ts"; +import { + hydrate, + HydrateFlavor, +} from "https://deno.land/x/grammy_hydrate/mod.ts"; + +type MyContext = HydrateFlavor; + +const bot = new Bot(""); + +bot.use(hydrate()); +``` + +::: + +### Расширенная установка + +При использовании простой установки гидратируются только те результаты вызовов +API, которые проходят через `ctx.api`, например, `ctx.reply`. Это большинство +вызовов для большинства ботов. + +Однако некоторым ботам может потребоваться обращение к `bot.api`. В этом случае +вам следует использовать эту расширенную установку. + +Она интегрирует гидратацию контекста и гидратацию результатов вызовов API +отдельно в вашего бота. Обратите внимание, что теперь вам также необходимо +установить [расширители API](../advanced/transformers#расширитель-api). + +::: code-group + +```ts [TypeScript] +import { Api, Bot, Context } from "grammy"; +import { + hydrateApi, + HydrateApiFlavor, + hydrateContext, + HydrateFlavor, +} from "@grammyjs/hydrate"; + +type MyContext = HydrateFlavor; +type MyApi = HydrateApiFlavor; + +const bot = new Bot(""); + +bot.use(hydrateContext()); +bot.api.config.use(hydrateApi()); +``` + +```js [JavaScript] +import { Bot } from "grammy"; +import { hydrateApi, hydrateContext } from "@grammyjs/hydrate"; + +const bot = new Bot(""); + +bot.use(hydrateContext()); +bot.api.config.use(hydrateApi()); +``` + +```ts [Deno] +import { Api, Bot, Context } from "https://deno.land/x/grammy/mod.ts"; +import { + hydrateApi, + HydrateApiFlavor, + hydrateContext, + HydrateFlavor, +} from "https://deno.land/x/grammy_hydrate/mod.ts"; + +type MyContext = HydrateFlavor; +type MyApi = HydrateApiFlavor; + +const bot = new Bot(""); + +bot.use(hydrateContext()); +bot.api.config.use(hydrateApi()); +``` + +::: + +## Какие предметы гидратируются + +В настоящее время этот плагин гидратирует + +- сообщения и посты канала +- редактируемые сообщения и редактируемые посты канала +- callback queries +- inline queries +- выбранные результаты inline +- запросы веб приложения +- запросы перед оформлением заказа и доставкой +- запросы на присоединение к чату + +Все объекты гидратируются на + +- объект контекста `ctx`, +- объект обновления `ctx.update` внутри контекста, +- краткие записи на объекте контекста, такие как `ctx.msg`, и +- результаты вызовов API, где это применимо. + +## Краткая информация о плагине + +- Название: `hydrate` +- [Исходник](https://github.com/grammyjs/hydrate) +- [Ссылка](/ref/hydrate/) diff --git a/site/docs/ru/plugins/i18n.md b/site/docs/ru/plugins/i18n.md new file mode 100644 index 000000000..5f83a9ca7 --- /dev/null +++ b/site/docs/ru/plugins/i18n.md @@ -0,0 +1,745 @@ +--- +prev: false +next: false +--- + +# Интернационализация (`i18n`) + +Плагин интернационализации заставляет вашего бота говорить на нескольких языках. + +::: tip Не путать +Не путайте его с [fluent](./fluent). + +Этот плагин - улучшенная версия [fluent](./fluent), которая работает как на Deno, так и на Node.js. +::: + +## Объяснение интернационализации + +> Этот раздел объясняет, что такое интернационализация, зачем она нужна, что в ней сложного, как она связана с локализацией и зачем вам нужен плагин для всего этого. +> Если вы уже знаете эти вещи, прокрутите страницу до раздела [Начало работы](#начало-работы). + +Во-первых, `internationalization` --- это очень длинное слово. +Поэтому люди любят писать первую букву (i) и последнюю (n). +Затем они подсчитывают все оставшиеся буквы (nternationalizatio - 18 букв) и помещают это число между i и n, так что в итоге получается _i18n_. +Не спрашивайте нас, почему. +Так что i18n --- это просто странная аббревиатура слова internationalization. + +Так же поступают и с локализацией, которая превращается в _l10n_. + +### Что такое локализация? + +Локализация означает создание бота, который может говорить на нескольких языках. +Он должен автоматически подстраивать свой язык под язык пользователя. + +Локализовать можно не только язык. +Вы также можете учесть культурные различия или другие стандарты, такие как форматы даты и времени. +Вот еще несколько примеров того, что в разных странах представлено по-разному: + +1. Даты +2. Времена +3. Числа +4. Единицы измерения +5. Множественные числа +6. Гендеры +7. Переносы +8. Большие буквы +9. Выравнивание +10. Символы и иконки +11. Сортировка + +… и [много другое](https://youtu.be/0j74jcxSunY). + +Все эти вещи в совокупности определяют _локаль_ пользователя. +Локалям часто присваивают двухбуквенные коды, например `en` для английского языка, `de` для немецкого, `ru` для русского и так далее. +Если вы хотите узнать код своей локали, посмотрите этот [список](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags). + +### Что такое интернационализация? + +В двух словах, интернационализация означает написание кода, который может подстраиваться под локаль пользователя. +Другими словами, интернационализация --- это то, что обеспечивает локализацию (см. [выше](#что-такое-локализация)). +Это означает, что хотя ваш бот в принципе работает одинаково для всех, конкретные сообщения, которые он отправляет, отличаются от пользователя к пользователю, поэтому бот может говорить на разных языках. + +Вы занимаетесь интернационализацией, если не пишите тексты, которые отправляет бот, а считываете их из файла динамически. +Вы делаете интернационализацию, если не пишите жесткое представление дат и времени, а используете библиотеку, которая корректирует эти значения в соответствии с различными стандартами. +Вы поняли идею: Не стоит жестко программировать то, что должно меняться в зависимости от места проживания пользователя или языка, на котором он говорит. + +### Зачем нам нужен этот плагин? + +Этот плагин поможет вам в процессе интернационализации. +Он основан на [Fluent](https://projectfluent.org/) --- системе локализации, созданной [Mozilla](https://mozilla.org/en-US/). +Эта система имеет очень мощный и элегантный синтаксис, который позволяет вам писать естественные переводы эффективным способом. + +По сути, вы можете извлечь все, что должно быть изменено в зависимости от локали пользователя, в некоторые текстовые файлы, которые вы помещаете рядом с вашим кодом. +Затем вы можете использовать этот плагин для загрузки этих локализаций. +Плагин автоматически определит локаль пользователя и позволит вашему боту выбрать нужный язык для общения. + +Ниже мы будем называть эти текстовые файлы _файлами перевода_. +Они должны соответствовать синтаксису Fluent. + +## Начало работы + +> В этом разделе описывается настройка структуры проекта и места размещения файлов перевода. +> Если вы уже знакомы с этим, [пропустите вперед](#использование), чтобы узнать, как установить и использовать плагин. + +Существует [несколько способов](#добавление-перевода) добавить больше языков в бот. +Самый простой способ - создать папку с файлами перевода Fluent. +Обычно эта папка называется `locales/`. +Файлы перевода должны иметь расширение `.ftl` (fluent). + +Вот пример структуры проекта: + +```asciiart:no-line-numbers +. +├── bot.ts +└── locales/ + ├── de.ftl + ├── en.ftl + ├── it.ftl + └── ru.ftl +``` + +Если вы не знакомы с синтаксисом Fluent, вы можете прочитать их руководство: . + +Вот пример файла перевода для английского языка, который называется `locales/en.ftl`: + +```ftl +start = Hi, how can I /help you? +help = + Send me some text, and I can make it bold for you. + You can change my language using the /language command. +``` + +Русский эквивалент будет называться `locales/ru.ftl` и выглядеть следующим образом: + +```ftl +start = Привет, как я могу /help вам? +help = + Пришлите мне текст, и я сделаю его полу-жирным. + Вы можете изменить мой язык с помощью команды /language. +``` + +Теперь вы можете использовать эти переводы в своем боте через плагин. +Он сделает их доступными через `ctx.t`: + +```ts +bot.command("start", async (ctx) => { + await ctx.reply(ctx.t("start")); +}); + +bot.command("help", async (ctx) => { + await ctx.reply(ctx.t("help")); +}); +``` + +Когда вы вызываете `ctx.t`, локаль текущего контекстного объекта `ctx` используется для поиска подходящего перевода. +Поиск правильного перевода осуществляется с помощью _посредника локали_. +В простейшем случае он просто возвращает `ctx.from.language_code`. + +В результате пользователи с разными локалями смогут читать сообщения каждый на своем языке. + +## Использование + +Плагин определяет локаль пользователя на основе множества различных факторов. +Одним из них является `ctx.from.language_code`, который будет предоставлен клиентом пользователя. + +Однако существует множество других факторов, которые можно использовать для определения локали пользователя. +Например, вы можете хранить локаль пользователя в вашей [сессии](./session). +Таким образом, существует два основных способа использования этого плагина: [С сессиями](#с-сессиями) и [Без сессий](#без-сессии). + +### Без сессий + +Проще использовать и настраивать плагин без сессий. +Его главный недостаток заключается в том, что вы не можете хранить языки, которые выбирают пользователи. + +Как уже говорилось выше, локаль, которую будет использовать пользователь, определяется с помощью `ctx.from.language_code`, который поступает от клиента пользователя. +Но язык по умолчанию будет использоваться, если у вас нет перевода на этот язык. +Иногда бот может не видеть предпочитаемый язык пользователя, предоставленный его клиентом, и в этом случае также будет использоваться язык по умолчанию. + +**Код `ctx.from.language_code` будет виден только в том случае, если пользователь ранее начал приватную беседу с вашим ботом.**. + +::: code-group + +```ts [TypeScript] +import { Bot, Context } from "grammy"; +import { I18n, I18nFlavor } from "@grammyjs/i18n"; + +// Для поддержки TypeScript и автозаполнения, +// расширьте контекст с помощью расширителя I18n: +type MyContext = Context & I18nFlavor; + +// Создайте бота, как обычно. +// Не забудьте расширить контекст. +const bot = new Bot(""); + +// Создайте экземпляр `I18n`. +// Продолжайте читать, чтобы узнать, как настроить экземпляр. +const i18n = new I18n({ + defaultLocale: "ru", // смотрите ниже для получения дополнительной информации + directory: "locales", // Загрузите все файлы перевода из locales/. +}); + +// Наконец, зарегистрируйте экземпляр i18n в боте, +// чтобы сообщения переводились на ходу! +bot.use(i18n); + +// Теперь все готово. +// Вы можете получить доступ к переводам с помощью `t` или `translate`. +bot.command("start", async (ctx) => { + await ctx.reply(ctx.t("start-msg")); +}); +``` + +```js [JavaScript] +const { Bot } = require("grammy"); +const { I18n } = require("@grammyjs/i18n"); + +// Создайте бота, как обычно. +const bot = new Bot(""); + +// Создайте экземпляр `I18n`. +// Продолжайте читать, чтобы узнать, как настроить экземпляр. +const i18n = new I18n({ + defaultLocale: "ru", // смотрите ниже для получения дополнительной информации + directory: "locales", // Загрузите все файлы перевода из locales/. +}); + +// Наконец, зарегистрируйте экземпляр i18n в боте, +// чтобы сообщения переводились на ходу! +bot.use(i18n); + +// Теперь все готово. +// Вы можете получить доступ к переводам с помощью `t` или `translate`. +bot.command("start", async (ctx) => { + await ctx.reply(ctx.t("start-msg")); +}); +``` + +```ts [Deno] +import { Bot, Context } from "https://deno.land/x/grammy/mod.ts"; +import { I18n, I18nFlavor } from "https://deno.land/x/grammy_i18n/mod.ts"; + +// Для поддержки TypeScript и автозаполнения, +// расширьте контекст с помощью расширителя I18n: +type MyContext = Context & I18nFlavor; + +// Создайте бота, как обычно. +// Не забудьте расширить контекст. +const bot = new Bot(""); + +// Создайте экземпляр `I18n`. +// Продолжайте читать, чтобы узнать, как настроить экземпляр. +const i18n = new I18n({ + defaultLocale: "ru", // смотрите ниже для получения дополнительной информации + // Загрузите все файлы перевода из locales/. (Не работает в Deno Deploy.) + directory: "locales", +}); + +// Загрузка файлов перевода таким образом работает и в Deno Deploy. +// await i18n.loadLocalesDir("locales"); + +// Наконец, зарегистрируйте экземпляр i18n в боте, +// чтобы сообщения переводились на ходу! +bot.use(i18n); + +// Теперь все готово. +// Вы можете получить доступ к переводам с помощью `t` или `translate`. +bot.command("start", async (ctx) => { + await ctx.reply(ctx.t("start-msg")); +}); +``` + +::: + +`ctx.t` возвращает переведенное сообщение для указанного ключа. +Вам не нужно беспокоиться о языках, так как они будут выбраны плагином автоматически. + +Поздравляем! +Теперь ваш бот говорит на нескольких языках! :earth_africa::tada: + +### С сессиями + +Предположим, что у вашего бота есть команда `/language`. +Как правило, в grammY мы можем использовать [sessions](./session) для хранения данных пользователя в чате. +Чтобы сообщить вашему экземпляру интернационализации, что сессии включены, нужно установить `useSession` в `true` в опциях `I18n`. + +Вот пример, включающий простую команду `/language`: + +::: code-group + +```ts [TypeScript] +import { Bot, Context, session, SessionFlavor } from "grammy"; +import { I18n, I18nFlavor } from "@grammyjs/i18n"; + +interface SessionData { + __language_code?: string; +} + +type MyContext = Context & SessionFlavor & I18nFlavor; + +const bot = new Bot(""); + +const i18n = new I18n({ + defaultLocale: "ru", + useSession: true, // хранить ли язык пользователя в сессии + directory: "locales", // Загрузите все файлы перевода из locales/. +}); + +// Не забудьте зарегистрировать middleware `session` перед тем, как +// регистрировать middleware для i18n +bot.use( + session({ + initial: () => { + return {}; + }, + }), +); + +// Зарегистрируйте middleware для i18n +bot.use(i18n); + +bot.command("start", async (ctx) => { + await ctx.reply(ctx.t("greeting")); +}); + +bot.command("language", async (ctx) => { + if (ctx.match === "") { + return await ctx.reply(ctx.t("language.specify-a-locale")); + } + + // `i18n.locales` содержит все локали, которые были зарегистрированы + if (!i18n.locales.includes(ctx.match)) { + return await ctx.reply(ctx.t("language.invalid-locale")); + } + + // `ctx.i18n.getLocale` возвращает текущую используемую локаль. + if ((await ctx.i18n.getLocale()) === ctx.match) { + return await ctx.reply(ctx.t("language.already-set")); + } + + await ctx.i18n.setLocale(ctx.match); + await ctx.reply(ctx.t("language.language-set")); +}); +``` + +```js [JavaScript] +const { Bot, session } = require("grammy"); +const { I18n } = require("@grammyjs/i18n"); + +const bot = new Bot(""); + +const i18n = new I18n({ + defaultLocale: "ru", + useSession: true, // хранить ли язык пользователя в сессии + directory: "locales", // Загрузите все файлы перевода из locales/. +}); + +// Не забудьте зарегистрировать middleware `session` перед тем, как +// регистрировать middleware для i18n +bot.use( + session({ + initial: () => { + return {}; + }, + }), +); + +// Зарегистрируйте middleware для i18n +bot.use(i18n); + +bot.command("start", async (ctx) => { + await ctx.reply(ctx.t("greeting")); +}); + +bot.command("language", async (ctx) => { + if (ctx.match === "") { + return await ctx.reply(ctx.t("language.specify-a-locale")); + } + + // `i18n.locales` содержит все локали, которые были зарегистрированы + if (!i18n.locales.includes(ctx.match)) { + return await ctx.reply(ctx.t("language.invalid-locale")); + } + + // `ctx.i18n.getLocale` возвращает текущую используемую локаль. + if ((await ctx.i18n.getLocale()) === ctx.match) { + return await ctx.reply(ctx.t("language.already-set")); + } + + await ctx.i18n.setLocale(ctx.match); + await ctx.reply(ctx.t("language.language-set")); +}); +``` + +```ts [Deno] +import { + Bot, + Context, + session, + SessionFlavor, +} from "https://deno.land/x/grammy/mod.ts"; +import { I18n, I18nFlavor } from "https://deno.land/x/grammy_i18n/mod.ts"; + +interface SessionData { + __language_code?: string; +} + +type MyContext = Context & SessionFlavor & I18nFlavor; + +const bot = new Bot(""); + +const i18n = new I18n({ + defaultLocale: "ru", + useSession: true, // хранить ли язык пользователя в сессии + + // НЕ РАБОТАЕТ в Deno Deploy + directory: "locales", +}); + +// Загрузка файлов перевода таким образом работает и в Deno Deploy. +// await i18n.loadLocalesDir("locales"); + +// Не забудьте зарегистрировать middleware `session` перед тем, как +// регистрировать middleware для i18n +bot.use( + session({ + initial: () => { + return {}; + }, + }), +); + +// Зарегистрируйте middleware для i18n +bot.use(i18n); + +bot.command("start", async (ctx) => { + await ctx.reply(ctx.t("greeting")); +}); + +bot.command("language", async (ctx) => { + if (ctx.match === "") { + return await ctx.reply(ctx.t("language.specify-a-locale")); + } + + // `i18n.locales` содержит все локали, которые были зарегистрированы + if (!i18n.locales.includes(ctx.match)) { + return await ctx.reply(ctx.t("language.invalid-locale")); + } + + // `ctx.i18n.getLocale` возвращает текущую используемую локаль. + if ((await ctx.i18n.getLocale()) === ctx.match) { + return await ctx.reply(ctx.t("language.already-set")); + } + + await ctx.i18n.setLocale(ctx.match); + await ctx.reply(ctx.t("language.language-set")); +}); +``` + +::: + +Когда сессии включены, свойство `__language_code` в сессии будет использоваться вместо `ctx.from.language_code` (предоставляемого клиентом Telegram) при выборе языка. +Когда ваш бот отправляет сообщения, локаль выбирается из `ctx.session.__language_code`. + +Существует метод `setLocale`, который вы можете использовать для установки желаемого языка. +Он сохранит это значение в вашей сессии. + +```ts +await ctx.i18n.setLocale("de"); +``` + +Это эквивалентно ручной настройке в сессии, а затем повторному определению локали: + +```ts +ctx.session.__language_code = "de"; +await ctx.i18n.renegotiateLocale(); +``` + +::: tip Переопределение локали +Если вы используете сессии или что-то еще --- помимо `ctx.from.language_code` --- для выбора пользовательской локали, есть некоторые ситуации, когда вы можете изменить язык при обработке обновления. +Например, посмотрите на приведенный выше пример с использованием сессий. + +Когда вы делаете только + +```ts +ctx.session.__language_code = "de"; +``` + +он не будет обновлять текущую используемую локаль в экземпляре `I18n`. +Вместо этого он обновляет только сессию. +Таким образом, изменения произойдут только _при следующем обновлении_. + +Если вы не можете дождаться следующего обновления, вам может понадобиться обновить изменения после обновления языка пользователя. +Для таких случаев используйте метод `renegotiateLocale`. + +```ts +ctx.session.__language_code = "de"; +await ctx.i18n.renegotiateLocale(); +``` + +После этого, когда бы мы ни использовали метод `t`, бот будет пытаться ответить немецким переводом этого сообщения (указанным в `locales/de.ftl`). + +Также помните, что при использовании встроенных сессий вы можете добиться того же результата с помощью метода `setLocale`. +::: + +::: tip Установка локали без сессий +Если в случае [работы без сессий](#без-сессии) вам необходимо установить локаль для пользователя, вы можете сделать это с помощью метода `useLocale`. + +```ts +await ctx.i18n.useLocale("de"); +``` + +Устанавливает указанную локаль для использования в будущих переводах. +Эффект действует только в текущем обновлении и не сохраняется. +Этот метод можно использовать для изменения локали перевода в середине обновления (например, когда пользователь меняет язык). +::: + +## Пользовательское согласование локали + +Вы можете использовать опцию `localeNegotiator`, чтобы указать пользовательское определение локали. +Эта опция полезна, если вы хотите выбрать локаль на основе внешних источников (например, баз данных) или в других ситуациях, когда вы хотите контролировать, какая локаль будет использоваться. + +По умолчанию плагин выбирает локаль в следующем порядке: + +1. Если сессии включены, попробуйте прочитать `__language_code` из сессии. + Если он возвращает правильную локаль, то она используется. + Если ничего не возвращается или возвращается незарегистрированная локаль, переходите к шагу 2. +2. Попытайтесь прочитать из `ctx.from.language_code`. + Если он возвращает действительную локаль, то она используется. + Если он не возвращает ничего или возвращает незарегистрированную локаль, перейдите к шагу 3. + + > Обратите внимание, что `ctx.from.language_code` доступен только в том случае, если пользователь запустил бота. + > Это означает, что если бот увидит пользователя в группе или где-то еще без предварительного запуска бота, он не сможет увидеть `ctx.from.language_code`. + +3. Попробуйте использовать язык по умолчанию, настроенный в опциях `I18n`. + Если он установлен в правильную локаль, то он будет использоваться. + Если он не указан или установлен в незарегистрированную локаль, перейдите к шагу 4. +4. Попробуйте использовать английский язык (`en`). + Плагин сам устанавливает эту локаль как конечную резервную. + Несмотря на то, что это запасная локаль, и мы рекомендуем иметь перевод, это не является обязательным условием. + Если английская локаль не указана, переходите к шагу 5. +5. Если все вышеперечисленное не помогло, используйте `{key}` вместо перевода. + Мы **настоятельно рекомендуем** установить локаль, которая существует в ваших переводах, в качестве `defaultLocale` в опциях `I18n`. + +::: tip Определение локали +Определение локали обычно происходит только один раз во время обработки обновлений Telegram. +Однако вы можете выполнить команду `ctx.i18n.renegotiateLocale()` для повторного вызова определителя и установки новой локали. +Это полезно, если локаль меняется во время обработки одного обновления. +::: + +Вот пример `localeNegotiator`, где мы используем `locale` из сессии вместо `__language_code`. +В таком случае не нужно устанавливать `useSession` на `true` в опциях `I18n`. + +::: code-group + +```ts [TypeScript] +const i18n = new I18n({ + localeNegotiator: (ctx) => + ctx.session.locale ?? ctx.from?.language_code ?? "ru", +}); +``` + +```js [JavaScript] +const i18n = new I18n({ + localeNegotiator: (ctx) => + ctx.session.locale ?? ctx.from?.language_code ?? "ru", +}); +``` + +::: + +Если пользовательский определитель локали возвращает недопустимую локаль, он отступает и выбирает локаль, следуя вышеуказанному порядку. + +## Отображение переведенных сообщений + +Давайте рассмотрим отображение сообщений подробнее. + +```ts +bot.command("start", async (ctx) => { + // Вызовите "translate" или "t" для отображения + // сообщения, указав его ID и дополнительные параметры: + await ctx.reply(ctx.t("welcome")); +}); +``` + +Теперь вы можете `/start` своего бота. +Он должен отобразить следующее сообщение + +```:no-line-numbers +Привет! +``` + +### Подстановка + +Иногда вы можете захотеть поместить такие значения, как числа и имена, внутрь строк. +Это можно сделать с помощью подстановки. + +```ts +bot.command("cart", async (ctx) => { + // Вы можете передать подстановочные данные в качестве второго объекта. + await ctx.reply(ctx.t("cart-msg", { items: 10 })); +}); +``` + +Объект `{ items: 10 }` называется _контекстом перевода_ строки `cart-msg`. + +Теперь, используя команду `/cart`: + +```:no-line-numbers +В настоящее время в вашей корзине находится 10 товаров. +``` + +Попробуйте изменить значение переменной `items` и посмотрите, как изменится отображаемое сообщение! +Также ознакомьтесь с документацией по Fluent, особенно с [документацией по подстановщикам](https://projectfluent.org/fluent/guide/placeables.html). + +### Глобальные переменные подстановки + +Может быть полезно указать количество переменных, которые должны быть доступны для _всех_ переводов. +Например, если вы используете имя пользователя во многих сообщениях, может быть утомительно передавать везде контекст перевода `{ name: ctx.from.first_name }`. + +На помощь приходят глобальные подстановки! +Рассмотрим следующее: + +```ts +const i18n = new I18n({ + defaultLocale: "ru", + directory: "locales", + // Определители глобальное доступные переменные + globalTranslationContext(ctx) { + return { name: ctx.from?.first_name ?? "" }; + }, +}); + +bot.use(i18n); + +bot.command("start", async (ctx) => { + // Можно использовать `name`, не указывая его снова! + await ctx.reply(ctx.t("welcome")); +}); +``` + +::: warning Возможные проблемы с форматированием +По умолчанию Fluent использует знаки изоляции Unicode для интерполяций. + +Если вы используете подстановки внутри тегов или сущностей, наличие изолирующих знаков может привести к неправильному форматированию (например, вместо ожидаемой ссылки или кештега --- обычный текст). + +Чтобы исправить это, используйте следующие параметры: + +```ts +const i18n = new I18n({ + fluentBundleOptions: { useIsolating: false }, +}); +``` + +::: + +## Добавление перевода + +Существует три основных способа загрузки переводов. + +### Загрузка локалей с помощью опции `directory` + +Самый простой способ добавить переводы в экземпляр `I18n` --- это разместить все переводы в папке и указать её название в опциях. + +```ts +const i18n = new I18n({ + directory: "locales", +}); +``` + +### Загрузка локалей из директории + +Этот метод - то же самое, что и указание `папки` в параметрах. +Просто поместите их все в папку и загрузите следующим образом: + +```ts +const i18n = new I18n(); + +await i18n.loadLocalesDir("locales"); // асинхронная версия +i18n.loadLocalesDirSync("locales-2"); // синхронная версия +``` + +> Обратите внимание, что некоторые среды требуют использования версии `async`. +> Например, Deno Deploy не поддерживает синхронные файловые операции. + +### Загрузка одной локали + +Также можно добавить один перевод в экземпляр. +Вы можете указать путь к файлу перевода, используя + +```ts +const i18n = new I18n(); + +await i18n.loadLocale("en", { filePath: "locales/en.ftl" }); // асинхронная версия +i18n.loadLocaleSync("ru", { filePath: "locales/ru.ftl" }); // синхронная версия +``` + +или вы можете напрямую загрузить данные перевода в виде строки, как показано ниже: + +```ts +const i18n = new I18n(); + +// асинхронная версия +await i18n.loadLocale("en", { + source: `greeting = Hello { $name }! +language-set = Language has been set to English!`, +}); + +// синхронная версия +i18n.loadLocaleSync("ru", { + source: `greeting = Привет, { $name }! +language-set = Язык был установлен на Русский!`, +}); +``` + +## Прослушивание локализованного текста + +Нам удалось отправить локализованные сообщения пользователю. +Теперь давайте рассмотрим, как прослушивать сообщения, отправленные пользователем. +В grammY мы обычно используем обработчик `bot.hears` для прослушивания входящих сообщений. +Но поскольку мы говорили об интернационализации, в этом разделе мы рассмотрим, как прослушивать локализованные входящие сообщения. + +Эта функция пригодится, если у вашего бота есть [пользовательские клавиатуры](./keyboard#пользовательские-клавиатуры), содержащие локализованный текст. + +Вот небольшой пример прослушивания локализованного текстового сообщения, отправленного с помощью пользовательской клавиатуры. +Вместо того чтобы использовать обработчик `bot.hears`, мы используем `bot.filter` в сочетании с middleware `hears`, предоставляемым этим плагином. + +::: code-group + +```ts [TypeScript] +import { hears } from "@grammyjs/i18n"; + +bot.filter(hears("back-to-menu-btn"), async (ctx) => { + await ctx.reply(ctx.t("main-menu-msg")); +}); +``` + +```js [JavaScript] +const { hears } = require("@grammyjs/i18n"); + +bot.filter(hears("back-to-menu-btn"), async (ctx) => { + await ctx.reply(ctx.t("main-menu-msg")); +}); +``` + +```ts [Deno] +import { hears } from "https://deno.land/x/grammy_i18n/mod.ts"; + +bot.filter(hears("back-to-menu-btn"), async (ctx) => { + await ctx.reply(ctx.t("main-menu-msg")); +}); +``` + +::: + +Вспомогательная функция `hears` позволяет вашему боту прослушать сообщение, написанное в локали пользователя. + +## Дальнейшие шаги + +- Завершите чтение [документации по Fluent](https://projectfluent.org/), особенно [руководства по синтаксису](https://projectfluent.org/fluent/guide/). +- Ознакомьтесь с соответствующими [примерами](https://github.com/grammyjs/i18n/tree/main/examples) этого плагина для Deno и Node.js. + +## Краткая информация о плагине + +- Название: `i18n` +- [Исходник](https://github.com/grammyjs/i18n) +- [Ссылка](/ref/i18n/) diff --git a/site/docs/ru/plugins/inline-query.md b/site/docs/ru/plugins/inline-query.md new file mode 100644 index 000000000..6799f6c96 --- /dev/null +++ b/site/docs/ru/plugins/inline-query.md @@ -0,0 +1,340 @@ +--- +prev: false +next: false +--- + +# Inline запросы (встроенно) + +С помощью inline запросов пользователи могут искать, просматривать и отправлять +контент, предложенный вашим ботом, в любой чат, даже если он не является его +участником. Для этого они начинают сообщение с `@имя_вашего_бота` и выбирают +один из результатов. + +> Пересмотрите раздел о режиме Inline в статье +> [Telegram Bot Features](https://core.telegram.org/bots/features#inline-requests), +> написанной командой Telegram. Дополнительные ресурсы --- это +> [подробное описание](https://core.telegram.org/bots/inline) инлайн-ботов, а +> также [пост в блоге](https://telegram.org/blog/inline-bots), анонсирующий эту +> функцию, и раздел Inline mode в +> [Telegram Bot API Reference](https://core.telegram.org/bots/api#inline-mode). +> Их стоит прочитать, прежде чем внедрять инлайн-запросы для своего бота, так +> как инлайн-запросы немного продвинуты. Если вам не хочется читать все это, то +> будьте уверены, что эта страница проведет вас через все шаги. + +## Включение Inline режима + +По умолчанию поддержка режима inline для вашего бота отключена. Вы должны +связаться с [@BotFather](https://t.me/BotFather) и включить режим inline, чтобы +ваш бот начал получать inline запросы. + +Получилось? Теперь ваш клиент Telegram должен отображать "...", когда вы вводите +имя бота в любое текстовое поле, и показывать иконку загрузки. Вы уже можете +начинать что-то набирать. Теперь давайте посмотрим, как ваш бот может +обрабатывать эти запросы. + +## Обработка inline запросов + +Как только пользователь затриггерит инлайн-запрос, то есть начнет сообщение, +набрав "@имя_вашего_бота..." в поле ввода текста, ваш бот будет получать +обновления об этом. В grammY есть специальная поддержка обработки +инлайн-запросов с помощью метода `bot.inlineQuery()`, как описано для класса +`Composer` в [документации grammY API](/ref/core/composer#inlinequery). Он +позволяет прослушивать конкретные инлайн запросы, соответствующие строкам или +регулярным выражениям. Если вы хотите обрабатывать все инлайн запросы в общем +виде, используйте `bot.on("inline_query")`. + +```ts +// Прослушивание определенных строк или регулярных выражений. +bot.inlineQuery(/best bot (framework|library)/, async (ctx) => { + const match = ctx.match; // объект, который подходит регулярному выражению + const query = ctx.inlineQuery.query; // строка запроса +}); + +// Прослушивайте любые инлайн запросы. +bot.on("inline_query", async (ctx) => { + const query = ctx.inlineQuery.query; // строка запроса +}); +``` + +Теперь, когда мы знаем, как прослушивать обновления инлайн запросов, мы можем +ответить на них списком результатов. + +## Построение результатов инлайн запросов + +Построение списка результатов для инлайн-запросов --- утомительная задача, +поскольку необходимо создавать +[сложные вложенные объекты](https://core.telegram.org/bots/api#inlinequeryresult) +с множеством свойств. К счастью, вы используете grammY, и, конечно, есть +помощники, которые делают эту задачу очень простой. + +Для каждого результата необходимы три вещи. + +1. Уникальный строковый идентификатор. +2. Объект _result_, который описывает, как отобразить результат inline запроса. + Он может содержать такие элементы, как заголовок, ссылка или изображение. +3. Объект _message content_, описывающий содержание сообщения, которое будет + отправлено пользователю, если он выберет этот результат. В некоторых случаях + содержимое сообщения может быть неявно выведено из объекта результата. + Например, если вы хотите, чтобы результат отображался в виде GIF, Telegram + поймет, что содержимым сообщения будет тот же GIF, если вы не укажете объект + message content. + +grammY экспортирует построитель результатов инлайн-запросов, названный +`InlineQueryResultBuilder`. Вот несколько примеров его использования. + +::: code-group + +```ts [TypeScript] +import { InlineKeyboard, InlineQueryResultBuilder } from "grammy"; + +// Создайте результат с фото +InlineQueryResultBuilder.photo("id-0", "https://grammy.dev/images/grammY.png"); + +// Постройте результат с фото, но который отправляет текстовое сообщение +InlineQueryResultBuilder.photo("id-1", "https://grammy.dev/images/grammY.png") + .text("Этот текст будет отправлен вместо фото"); + +// Постройте текстовый результат +InlineQueryResultBuilder.article("id-2", "Инлайн запросы") + .text( + "Отличная документация по инлайн запросам: grammy.dev/plugins/inline-query", + ); + +// Передайте дополнительные параметры результату. +const keyboard = new InlineKeyboard() + .text("О, да", "перезвони мне, крошка"); +InlineQueryResultBuilder.article("id-3", "Нажми на меня", { + reply_markup: keyboard, +}) + .text("Нажимай на мои кнопки"); + +// Передайте дополнительные параметры в содержимое сообщения. +InlineQueryResultBuilder.article("id-4", "Инлайн запросы") + .text("**Выдающаяся** документация: grammy.dev", { + parse_mode: "MarkdownV2", + }); +``` + +```js [JavaScript] +const { InlineKeyboard, InlineQueryResultBuilder } = require("grammy"); + +// Создайте результат с фото +InlineQueryResultBuilder.photo("id-0", "https://grammy.dev/images/grammY.png"); + +// Постройте результат с фото, но который отправляет текстовое сообщение +InlineQueryResultBuilder.photo("id-1", "https://grammy.dev/images/grammY.png") + .text("Этот текст будет отправлен вместо фото"); + +// Постройте текстовый результат +InlineQueryResultBuilder.article("id-2", "Инлайн запросы") + .text( + "Отличная документация по инлайн запросам: grammy.dev/plugins/inline-query", + ); + +// Передайте дополнительные параметры результату. +const keyboard = new InlineKeyboard() + .text("О, да", "перезвони мне, крошка"); +InlineQueryResultBuilder.article("id-3", "Нажми на меня", { + reply_markup: keyboard, +}) + .text("Нажимай на мои кнопки"); + +// Передайте дополнительные параметры в содержимое сообщения. +InlineQueryResultBuilder.article("id-4", "Inline Queries") + .text("**Выдающаяся** документация: grammy.dev", { + parse_mode: "MarkdownV2", + }); +``` + +```ts [Deno] +import { + InlineKeyboard, + InlineQueryResultBuilder, +} from "https://deno.land/x/grammy/mod.ts"; + +// Создайте результат с фото +InlineQueryResultBuilder.photo("id-0", "https://grammy.dev/images/grammY.png"); + +// Постройте результат с фото, но который отправляет текстовое сообщение +InlineQueryResultBuilder.photo("id-1", "https://grammy.dev/images/grammY.png") + .text("Этот текст будет отправлен вместо фото"); + +// Постройте текстовый результат +InlineQueryResultBuilder.article("id-2", "Inline Queries") + .text( + "Отличная документация по инлайн запросам: grammy.dev/plugins/inline-query", + ); + +// Передайте дополнительные параметры результату. +const keyboard = new InlineKeyboard() + .text("О, да", "перезвони мне, крошка"); +InlineQueryResultBuilder.article("id-3", "Нажми на меня", { + reply_markup: keyboard, +}) + .text("Нажимай на мои кнопки"); + +// Передайте дополнительные параметры в содержимое сообщения. +InlineQueryResultBuilder.article("id-4", "Inline Queries") + .text("**Выдающаяся** документация: grammy.dev", { + parse_mode: "MarkdownV2", + }); +``` + +::: + +Обратите внимание, что если вы хотите отправлять файлы через существующие +идентификаторы файлов, вам следует использовать методы `*Cached`. + +```ts +// Результат для аудиофайла, отправленного через идентификатор файла. +const audioFileId = "AgADBAADZRAxGyhM3FKSE4qKa-RODckQHxsoABDHe0BDC1GzpGACAAEC"; +InlineQueryResultBuilder.audioCached("id-0", audioFileId); +``` + +> Подробнее об идентификаторах файлов можно прочитать +> [здесь](../guide/files#как-работают-фаилы-для-ботов-telegram). + +Вам следует ознакомиться с +[API документацией](/ref/core/inlinequeryresultbuilder) +`InlineQueryResultBuilder`, а также, возможно, с +[спецификацией](https://core.telegram.org/bots/api#inlinequeryresult) +`InlineQueryResult`, чтобы увидеть все доступные опции. + +## Ответ на инлайн запросы + +Сформировав массив результатов инлайн-запроса с помощью конструктора +[выше](#построение-результатов-инлаин-запросов), вы можете вызвать +`answerInlineQuery`, чтобы отправить эти результаты пользователю. + +```ts +// Бесстыдная самореклама в документации одного проекта +// Это лучший вид рекламы. +bot.inlineQuery(/best bot (framework|library)/, async (ctx) => { + // Создайте один результат инлайн запроса. + const result = InlineQueryResultBuilder + .article("id:grammy-website", "grammY", { + reply_markup: new InlineKeyboard() + .url("grammY вебсайт", "https://grammy.dev/"), + }) + .text( + `grammY это лучший способ создания собственных ботов Telegram. +У них даже есть симпатичный сайт! 👇`, + { parse_mode: "HTML" }, + ); + + // Ответьте на инлайн запросы + await ctx.answerInlineQuery( + [result], // ответ со списком результатов + { cache_time: 30 * 24 * 3600 }, // 30 дней в секундах + ); +}); + +// Возвращайте пустой список результатов для других запросов. +bot.on("inline_query", (ctx) => ctx.answerInlineQuery([])); +``` + +[Помните](../guide/basics#отправка-сообщении), что вы всегда можете указать +дополнительные опции при вызове методов API, используя объект options типа +`Other`. Например, `answerInlineQuery` позволяет выполнять построение страниц +для инлайн-запросов через смещение, как вы можете увидеть +[здесь](https://core.telegram.org/bots/api#answerinlinequery). + +::: tip Смешивание текста и медиа Хотя разрешается отправлять списки +результатов, содержащие как медиа, так и текстовые элементы, большинство +клиентов Telegram не очень хорошо их отображают. С точки зрения +пользовательского опыта, их следует избегать. +::: + +## Кнопка над результатами инлайн запроса + +Клиенты Telegram могут +[показать кнопку](https://core.telegram.org/bots/api#inlinequeryresultsbutton) +над списком результатов. Эта кнопка может перевести пользователя в приватный чат +с ботом. + +```ts +const button = { + text: "Открыть приватный чат", + start_parameter: "login", +}; +await ctx.answerInlineQuery(results, { button }); +``` + +Когда пользователь нажмет на кнопку, вашему боту будет отправлено командное +сообщение `/start`. Параметр start будет доступен через +[deep linking](../guide/commands#поддержка-deep-linking). Другими словами, +используя приведенный выше фрагмент кода, `ctx.match` будет иметь значение +`"login"` в вашем обработчике команд. + +Если затем отправить +[встроенную клавиатуру](./keyboard#построение-встроенных-клавиатур) с кнопкой +`switchInline`, пользователь будет возвращен в чат, где он первоначально нажал +кнопку результатов inline-запроса. + +```ts +bot + .command("start") + .filter((ctx) => ctx.match === "login", async (ctx) => { + // Пользователь приходит из результатов встроенного запроса. + await ctx.reply("Личные сообщения открыты, теперь вы можете вернуться!", { + reply_markup: new InlineKeyboard() + .switchInline("Вернуться назад"), + }); + }); +``` + +Таким образом, вы можете выполнять, например, процедуры входа в систему в +приватном чате с пользователем перед выдачей результатов запроса. Диалог может +идти туда-сюда, прежде чем вы отправите их обратно. Например, вы можете +[ввести короткий диалог](./conversations#создание-диалога-и-вступление-в-него) с +помощью плагина conversations. + +## Получение отзывов о выбранных результатах + +Результаты инлайн-запросов доставляются по принципу "авось нормально будет". +Другими словами, после того как ваш бот отправил список результатов +инлайн-запросов в Telegram, он не будет знать, какой результат выбрал +пользователь (и выбрал ли он его вообще). + +Если вы заинтересованы в этом, вы можете включить обратную связь с помощью +[@BotFather](https://t.me/BotFather). Вы можете определить, сколько обратной +связи вы хотите получать, выбрав один из нескольких вариантов от 0 % (обратная +связь отключена) до 100 % (получать обратную связь за каждый выбранный inline +результат). + +Обратная связь доставляется через обновления `chosen_inline_result`. Вы можете +прослушивать определенные идентификаторы результатов через строку или регулярное +выражение. Естественно, вы также можете прослушивать обновления обычным способом +с помощью запросов-фильтров. + +```ts +// Прослушивание определенных идентификаторов результатов. +bot.chosenInlineResult(/id-[0-9]+/, async (ctx) => { + const match = ctx.match; // объект, который подходит регулярному выражению + const query = ctx.chosenInlineResult.query; // используемый инлайн запрос +}); + +// Прослушайте все выбранные inline результаты +bot.on("chosen_inline_result", async (ctx) => { + const query = ctx.chosenInlineResult.query; // используемый инлайн запрос +}); +``` + +Некоторые боты устанавливают обратную связь на 100 % и используют ее в качестве +преимущества. Они передают фиктивные сообщения без реального содержимого в +`answerInlineQuery`. Сразу после получения обновления `chosen_inline_result` они +редактируют соответствующее сообщение и вставляют в него реальное содержимое. + +Эти боты не будут работать для анонимных администраторов или при отправке +сообщений по расписанию, так как в этом случае нельзя получить обратную связь. +Однако если для вас это не проблема, то данный хак позволит вам не генерировать +большое количество содержимого сообщений, которые в итоге так и не будут +отправлены. Это позволит сэкономить ресурсы бота. + +## Краткая информация о плагине + +Этот плагин встроен в ядро grammY. Вам не нужно ничего устанавливать, чтобы +использовать его. Просто импортируйте все из самого grammY. + +Кроме того, документация и ссылка на API этого плагина объединены с основным +пакетом. diff --git a/site/docs/ru/plugins/keyboard.md b/site/docs/ru/plugins/keyboard.md new file mode 100644 index 000000000..7bdba3727 --- /dev/null +++ b/site/docs/ru/plugins/keyboard.md @@ -0,0 +1,388 @@ +--- +prev: false +next: false +--- + +# Встроенные и пользовательские клавиатуры (встроенно) + +Ваш бот может отправлять несколько кнопок, которые будут либо +[отображаться под сообщением](#встроенные-клавиатуры), либо +[заменять клавиатуру пользователя](#пользовательские-клавиатуры). Они называются +_inline-клавиатурами_ и _custom-клавиатурами_, соответственно. Если вы думаете, +что это запутанно, то так оно и есть. Спасибо, Telegram, за эту пересекающуюся +терминологию. + +Давайте попробуем немного прояснить ситуацию: + +| Термин | Определение | +| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| [**Встроенные клавиатуры**](#встроенные-клавиатуры) | набор кнопок, который отображается под сообщением в чате. | +| [**Пользовательские клавиатуры**](#пользовательские-клавиатуры) | набор кнопок, который отображается вместо системной клавиатуры пользователя. | +| **Кнопка встроенной клавиатуры** | кнопка в встроенной клавиатуре, при нажатии посылает callback запрос, не видимый пользователю, иногда называется просто _inline button_. | +| **Кнопка пользовательской клавиатуры** | кнопка клавиатуры, при нажатии отправляет текстовое сообщение со своим ярлыком, иногда называется просто _keyboard button_. | +| **`InlineKeyboard`** | класс в grammY для создания встроенных клавиатур. | +| **`Keyboard`** | класс в grammY для создания пользовательских клавиатур. | + +> Обратите внимание, что и кнопки пользовательской клавиатуры, и кнопки +> встроенной клавиатуры могут выполнять и другие функции, например, запрашивать +> местоположение пользователя, открывать веб-сайт и так далее. Это было опущено +> для краткости. + +Невозможно указать в одном сообщении и пользовательскую, и встроенную +клавиатуру. Эти два варианта являются взаимоисключающими. Более того, +отправленный вид разметки ответа нельзя изменить впоследствии, отредактировав +сообщение. Например, невозможно сначала отправить пользовательскую клавиатуру +вместе с сообщением, а затем отредактировать сообщение, чтобы использовать +встроенную клавиатуру. + +## Встроенные клавиатуры + +> Пересмотрите раздел о встроенной клавиатуре в статье +> [Возможности Telegram Бота](https://core.telegram.org/bots/features#inline-keyboards), +> написанной командой Telegram. + +В grammY есть простой и интуитивно понятный способ создания встроенных +клавиатур, которые ваш бот может отправлять вместе с сообщением. Для этого он +предоставляет класс `InlineKeyboard`. + +> Кнопки, добавляемые вызовом `switchInline`, `switchInlineCurrent` и +> `switchInlineChosen`, запускают встроенные запросы. Подробнее о том, как они +> работают, читайте в разделе о [встроенные запросы](./inline-query). + +### Построение встроенных клавиатур + +Вы можете создать встроенную клавиатуру, создав новый экземпляр класса +`InlineKeyboard`, а затем добавив в него нужные вам кнопки с помощью `.text()` и +других его методов. + +Вот пример: + +![Пример](/images/inline-keyboard-example.png) + +```ts +const inlineKeyboard = new InlineKeyboard() + .text("« 1", "first") + .text("‹ 3", "prev") + .text("· 4 ·", "stay") + .text("5 ›", "next") + .text("31 »", "last"); +``` + +Вызовите `.row()`, если хотите начать новый ряд кнопок. Вы также можете +использовать другие методы, например `.url()`, чтобы позволить клиенту +пользователя открыть определенный URL или сделать другие интересные вещи. +Обязательно ознакомьтесь со [всеми методами](/ref/core/inlinekeyboard#methods) +класса `InlineKeyboard`. + +Если у вас уже есть массив строк, который вы хотели бы превратить в встроенную +клавиатуру, вы можете использовать второй, альтернативный стиль построения +экземпляров встроенной клавиатуры. Класс `InlineKeyboard` имеет статические +методы, такие как `InlineKeyboard.text`, которые позволяют создавать объекты +кнопок. В свою очередь, вы можете создать экземпляр встроенной клавиатуры из +массива объектов кнопок с помощью `InlineKeyboard.from`. + +Таким образом, вы можете построить описанную выше линейную клавиатуру +функциональным способом. + +```ts +const labelDataPairs = [ + ["« 1", "first"], + ["‹ 3", "prev"], + ["· 4 ·", "stay"], + ["5 ›", "next"], + ["31 »", "last"], +]; +const buttonRow = labelDataPairs + .map(([label, data]) => InlineKeyboard.text(label, data)); +const keyboard = InlineKeyboard.from([buttonRow]); +``` + +### Отправка встроенных клавиатур + +Вы можете отправить встроенную клавиатуру прямо вместе с сообщением, независимо +от того, используете ли вы `bot.api.sendMessage`, `ctx.api.sendMessage` или +`ctx.reply`: + +```ts +// Отправьте встроенную клавиатуру с сообщением. +await ctx.reply(text, { + reply_markup: inlineKeyboard, +}); +``` + +Естественно, все остальные методы, отправляющие сообщения, отличные от +текстовых, поддерживают те же опции, которые указаны в +[Telegram Bot API](https://core.telegram.org/bots/api). Например, вы можете +отредактировать клавиатуру, вызвав `editMessageReplyMarkup` и передав новый +экземпляр `InlineKeyboard` в качестве `reply_markup`. Укажите пустую клавиатуру, +чтобы удалить все кнопки под сообщением. + +### Ответ на нажатие по встроенной клавиатуре + +::: tip Плагин для меню Плагин клавиатуры дает вам прямой доступ к объектам +обновлений, которые отправляет Telegram. Однако реагировать на нажатия таким +образом может быть утомительно. Если вы ищете более высокоуровневую реализацию +встроенных клавиатур, обратите внимание на плагин [menu](./menu). Он упрощает +создание интерактивных меню. +::: + +К каждой кнопке `text` прикреплена строка в качестве данных callback вызова. +Если вы не прикрепите данные callback вызова, grammY будет использовать текст +кнопки в качестве данных. + +Как только пользователь нажмет на кнопку `text`, ваш бот получит обновление, +содержащее данные callback вызова соответствующей кнопки. Вы можете прослушать +данные callback вызова через `bot.callbackQuery()`. + +```ts +// Сконструируйте клавиатуру. +const inlineKeyboard = new InlineKeyboard().text("клик", "click-payload"); + +// Отправьте клавиатуру вместе с сообщением. +bot.command("start", async (ctx) => { + await ctx.reply("Любопытно? Нажмите на меня!", { + reply_markup: inlineKeyboard, + }); +}); + +// Подождите события клика с определённым названием +bot.callbackQuery("click-payload", async (ctx) => { + await ctx.answerCallbackQuery({ + text: "Вы были очень любопытны!", + }); +}); +``` + +::: tip Ответы на все callback запросы `bot.callbackQuery()` полезен для +прослушивания событий нажатия определенных кнопок. Вы можете использовать +`bot.on("callback_query:data")` для прослушивания событий любой кнопки. + +```ts +bot.callbackQuery("click-payload" /* , ... */); + +bot.on("callback_query:data", async (ctx) => { + console.log("Неизвестное событие кнопки с payload", ctx.callbackQuery.data); + await ctx.answerCallbackQuery(); // убрать анимацию загрузки +}); +``` + +Имеет смысл определить `bot.on("callback_query:data")` в последнюю очередь, +чтобы всегда отвечать на все остальные запросы, которые ваши предыдущие +слушатели не обработали. В противном случае некоторые клиенты могут показывать +анимацию загрузки до минуты, когда пользователь нажимает кнопку, на которую ваш +бот не хочет реагировать. +::: + +## Пользовательские клавиатуры + +Прежде всего: пользовательские клавиатуры иногда называются просто клавиатурами, +иногда --- ответными клавиатурами. Даже собственная документация Telegram +непоследовательна в этом отношении. Простое эмпирическое правило: если это не +очевидно из контекста и не называется встроенной клавиатурой, то это, скорее +всего, пользовательская клавиатура. Это относится к способу замены системной +клавиатуры набором кнопок, которые вы можете определить. + +> Пересмотрите раздел о пользовательской клавиатуре в статье +> [Telegram Bot Features](https://core.telegram.org/bots/features#keyboards), +> написанной командой Telegram. + +В grammY есть простой и интуитивно понятный способ создания пользовательских +клавиатур, которые ваш бот может использовать для замены системной клавиатуры. +Для этого он предоставляет класс `Keyboard`. + +Как только пользователь нажмет кнопку `text`, ваш бот получит отправленный текст +в виде обычного текстового сообщения. Помните, что вы можете прослушать +текстовое сообщение через `bot.on("message:text")` или `bot.hears()`. + +### Построение пользовательских клавиатур + +Вы можете создать собственную клавиатуру, создав новый экземпляр класса +`Keyboard`, а затем добавив к нему кнопки типа `.text()` и другие. Вызовите +`.row()`, чтобы начать новый ряд кнопок. + +Вот пример: + +![Пример](/images/keyboard-example.png) + +```ts +const keyboard = new Keyboard() + .text("Да, это точно").row() + .text("Я не совсем уверен").row() + .text("Не-а. 😈") + .resized(); +``` + +Вы также можете отправлять более мощные кнопки, которые запрашивают номер +телефона пользователя, его местоположение или делают другие интересные вещи. +Обязательно ознакомьтесь со [всеми методами](/ref/core/keyboard#methods) класса +`Keyboard`. + +Если у вас уже есть массив строк, который вы хотите превратить в клавиатуру, вы +можете использовать второй, альтернативный стиль создания экземпляров +клавиатуры. Класс `Keyboard` имеет статические методы, такие как +`Keyboard.text`, которые позволяют создавать объекты кнопок. В свою очередь, вы +можете создать экземпляр клавиатуры из массива объектов кнопок с помощью +`Keyboard.from`. + +Таким образом, вы можете построить описанную выше клавиатуру функциональным +способом. + +```ts +const labels = [ + "Да, это точно", + "Я не совсем уверен", + "Не-а. 😈", +]; +const buttonRows = labels + .map((label) => [Keyboard.text(label)]); +const keyboard = Keyboard.from(buttonRows).resized(); +``` + +### Отправка пользовательских клавиатур + +Вы можете отправить пользовательскую клавиатуру прямо вместе с сообщением, +независимо от того, используете ли вы `bot.api.sendMessage`, +`ctx.api.sendMessage` или `ctx.reply`: + +```ts +// Отправить клавиатуру с сообщением +await ctx.reply(text, { + reply_markup: keyboard, +}); +``` + +Естественно, все остальные методы, отправляющие сообщения, отличные от +текстовых, поддерживают те же опции, как указано в +[Telegram Bot API](https://core.telegram.org/bots/api). + +Вы также можете придать клавиатуре одно или несколько дополнительных свойств, +вызвав для нее специальные методы. Они не добавляют никаких кнопок, а скорее +определяют поведение клавиатуры. Мы уже видели `resized` в примере выше --- вот +еще несколько вещей, которые вы можете сделать. + +#### Постоянные клавиатуры + +По умолчанию пользователи видят значок, который позволяет им показывать или +скрывать пользовательскую клавиатуру, установленную вашим ботом. + +Вы можете вызвать `persistent`, если хотите, чтобы пользовательская клавиатура +всегда отображалась, когда обычная системная клавиатура скрыта. Таким образом, +пользователи всегда будут видеть либо пользовательскую, либо системную +клавиатуру. + +```ts +new Keyboard() + .text("Пропустить") + .persistent(); +``` + +#### Изменение размера пользовательской клавиатуры + +Вы можете вызвать `resized`, если хотите, чтобы размер пользовательской +клавиатуры был изменен в соответствии с содержащимися в ней кнопками. Это +позволит сделать клавиатуру меньше. (Обычно клавиатура всегда имеет размер +стандартной клавиатуры приложения). + +```ts +new Keyboard() + .text("Да").row() + .text("Нет") + .resized(); +``` + +Неважно, вызываете ли вы `resized` первым, последним или где-то между ними. +Результат всегда будет одинаковым. + +#### Одноразовые пользовательские клавиатуры + +Вы можете вызвать `oneTime`, если хотите, чтобы пользовательская клавиатура была +скрыта сразу после нажатия первой кнопки. + +```ts +new Keyboard() + .text("Да").row() + .text("Нет") + .oneTime(); +``` + +Неважно, вызываете ли вы `oneTime` первым, последним или где-то между ними. +Результат всегда будет одинаковым. + +#### Заполнитель в поле ввода + +Вы можете вызвать `placeholder`, если хотите, чтобы в поле ввода отображался +заполнитель, пока видна пользовательская клавиатура. + +```ts +new Keyboard() + .text("Да").row() + .text("Нет") + .placeholder("Выбирай сейчас же!"); +``` + +Неважно, называете ли вы `placeholder` первым, последним или где-то между ними. +Результат всегда будет одинаковым. + +#### Выборочная отправка пользовательских клавиатур + +Вы можете вызвать `selected`, если хотите показать пользовательскую клавиатуру +только тем пользователям, которые упомянуты (@) в тексте объекта сообщения, и +отправителю исходного сообщения в случае, если ваше сообщение является +[ответом на сообщение](../guide/basics#отправка-сообщении-с-ответом-на-сообщение). + +```ts +new Keyboard() + .text("Да").row() + .text("Нет") + .selected(); +``` + +Неважно, вызовете ли вы `selected` первым, последним или где-то между ними. +Результат всегда будет одинаковым. + +### Ответ на клик по пользовательское клавиатуре + +Как уже говорилось, все, что делают пользовательские клавиатуры, --- это отправка +обычных текстовых сообщений. Ваш бот не сможет отличить обычные текстовые +сообщения от сообщений, которые были отправлены нажатием кнопки. + +Более того, кнопки всегда будут отправлять именно то сообщение, которое на них +написано. Telegram не позволяет создавать кнопки, которые отображают один текст, +а отправляют другой. Если вам нужно сделать это, используйте +[встроенную клавиатуру](#встроенные-клавиатуры). + +Чтобы обработать нажатие определенной кнопки, вы можете использовать `bot.hears` +с тем же текстом, который вы поместили на кнопку. Если вы хотите обработать все +нажатия на кнопки сразу, используйте `bot.on("message:text")` и проверьте +`ctx.msg.text`, чтобы выяснить, какая кнопка была нажата, или было ли отправлено +обычное текстовое сообщение. + +### Удаление пользовательской клавиатуры + +Если не указать `one_time_keyboard`, как описано +[выше](#одноразовые-пользовательские-клавиатуры), пользовательская клавиатура +будет оставаться открытой для пользователя (но пользователь может свернуть ее). + +Удалить пользовательскую клавиатуру можно только при отправке нового сообщения в +чате, так же как и указать новую клавиатуру можно только при отправке сообщения. +Передайте `{ remove_keyboard: true }` в качестве `reply_markup` следующим +образом: + +```ts +await ctx.reply(text, { + reply_markup: { remove_keyboard: true }, +}); +``` + +Рядом с `remove_keyboard` можно установить значение `selective: true`, чтобы +удалить пользовательскую клавиатуру только для выбранных пользователей. Это +работает аналогично +[выборочной отправке пользовательской клавиатуры](#выборочная-отправка-пользовательских-клавиатур). + +## Краткая информация о плагине + +Этот плагин встроен в ядро grammY. Вам не нужно ничего устанавливать, чтобы +использовать его. Просто импортируйте все из самого grammY. + +Кроме того, документация и ссылка на API этого плагина объединены с основным +пакетом. diff --git a/site/docs/ru/plugins/media-group.md b/site/docs/ru/plugins/media-group.md new file mode 100644 index 000000000..be2182564 --- /dev/null +++ b/site/docs/ru/plugins/media-group.md @@ -0,0 +1,91 @@ +--- +prev: false +next: false +--- + +# Медиагруппы (встроенный) + +Плагин медиагрупп помогает вам отправлять медиагруппы, позволяя создавать объекты `InputMedia`. +Кстати, объекты `InputMedia` также используются при редактировании медиа-сообщений, так что этот плагин также поможет вам редактировать медиа. + +Помните, что объекты `InputMedia` описываются [здесь](https://core.telegram.org/bots/api#inputmedia). + +## Создание объекта `InputMedia` + +Вы можете использовать этот плагин следующим образом: + +::: code-group + +```ts [TypeScript] +import { InputMediaBuilder } from "grammy"; + +const photo = InputMediaBuilder.photo(new InputFile("/tmp/photo.mp4")); +const video = InputMediaBuilder.video(new InputFile("/tmp/video.mp4")); +// т.д. +``` + +```js [JavaScript] +const { InputMediaBuilder } = require("grammy"); + +const photo = InputMediaBuilder.photo(new InputFile("/tmp/photo.mp4")); +const video = InputMediaBuilder.video(new InputFile("/tmp/video.mp4")); +// т.д. +``` + +```ts [Deno] +import { InputMediaBuilder } from "https://deno.land/x/grammy/mod.ts"; + +const photo = InputMediaBuilder.photo(new InputFile("/tmp/photo.mp4")); +const video = InputMediaBuilder.video(new InputFile("/tmp/video.mp4")); +// т.д. +``` + +::: + +Ознакомьтесь со всеми методами `InputMediaBuilder` в [документации API](/ref/core/inputmediabuilder). + +Вы также можете напрямую передавать публичные ссылки, которые считывает Telegram. + +```ts +const photo = InputMediaBuilder.photo("https://grammy.dev/images/grammY.png"); +``` + +Дополнительные параметры могут быть предоставлены в объекте options в конце. + +```ts +const photo = InputMediaBuilder.photo("https://grammy.dev/images/grammY.png", { + caption: "grammY заметелен", + // т.д. +}); +``` + +## Отправка медиагруппы + +Вы можете отправить медиагруппу следующим образом: + +```ts +await ctx.replyWithMediaGroup([photo0, photo1, photo2, video]); +``` + +Аналогичным образом вы можете передать массив объектов `InputMedia` в `ctx.api.sendMediaGroup` или `bot.api.sendMediaGroup`. + +## Редактирование медиасообщения + +Поскольку объекты `InputMedia` также используются для редактирования медиа сообщений, этот плагин поможет вам и здесь: + +```ts +const newMedia = InputMediaBuilder.photo( + "https://grammy.dev/images/grammY.png", +); +await ctx.editMessageMedia(newMedia); +``` + +Как обычно, это работает и для `ctx.api.editMessageMedia` и `bot.api.editMessageMedia`. + +## Краткая информация о плагине + +Этот плагин встроен в библиотеку grammY. +Вам не нужно ничего устанавливать, чтобы использовать его. +Просто импортируйте всё из самого grammY. + +Кроме того, документация и ссылки на API этого плагина объединены с основным пакетом. diff --git a/site/docs/ru/plugins/menu.md b/site/docs/ru/plugins/menu.md new file mode 100644 index 000000000..2eae494c1 --- /dev/null +++ b/site/docs/ru/plugins/menu.md @@ -0,0 +1,605 @@ +--- +prev: false +next: false +--- + +# Интерактивные меню (`menu`) + +Легко создавайте интерактивные меню. + +## Ведение + +Встроенная клавиатура --- это массив кнопок под сообщением. В grammY есть +[встроенный плагин](./keyboard#встроенные-клавиатуры) для создания базовых +встроенных клавиатур. + +Плагин меню развивает эту идею и позволяет создавать технологичные меню прямо в +чате. В них могут быть интерактивные кнопки, несколько страниц с навигацией +между ними и многое другое. + +Вот простой пример, который говорит сам за себя. + +::: code-group + +```ts [TypeScript] +import { Bot } from "grammy"; +import { Menu } from "@grammyjs/menu"; + +// Создайте бота. +const bot = new Bot(""); + +// Создайте простое меню. +const menu = new Menu("my-menu-identifier") + .text("A", (ctx) => ctx.reply("Вы нажали A!")).row() + .text("B", (ctx) => ctx.reply("Вы нажали B!")); + +// Сделайте его интерактивным +bot.use(menu); + +bot.command("start", async (ctx) => { + // Отправьте меню. + await ctx.reply("Посмотрите на это меню:", { reply_markup: menu }); +}); + +bot.start(); +``` + +```js [JavaScript] +const { Bot } = require("grammy"); +const { Menu } = require("@grammyjs/menu"); + +// Создайте бота. +const bot = new Bot(""); + +// Создайте простое меню. +const menu = new Menu("my-menu-identifier") + .text("A", (ctx) => ctx.reply("Вы нажали A!")).row() + .text("B", (ctx) => ctx.reply("Вы нажали B!")); + +// Сделайте его интерактивным +bot.use(menu); + +bot.command("start", async (ctx) => { + // Отправьте меню. + await ctx.reply("Посмотрите на это меню:", { reply_markup: menu }); +}); + +bot.start(); +``` + +```ts [Deno] +import { Bot } from "https://deno.land/x/grammy/mod.ts"; +import { Menu } from "https://deno.land/x/grammy_menu/mod.ts"; + +// Создайте бота. +const bot = new Bot(""); + +// Создайте простое меню. +const menu = new Menu("my-menu-identifier") + .text("A", (ctx) => ctx.reply("Вы нажали A!")).row() + .text("B", (ctx) => ctx.reply("Вы нажали B!")); + +// Сделайте его интерактивным +bot.use(menu); + +bot.command("start", async (ctx) => { + // Отправьте меню. + await ctx.reply("Посмотрите на это меню:", { reply_markup: menu }); +}); + +bot.start(); +``` + +::: + +> Убедитесь, что вы установили все меню перед другими middleware, особенно перед +> middleware, использующим данные `callback_query`. Также, если вы используете +> пользовательскую конфигурацию для `allowed_updates`, не забудьте включить +> обновления `callback_query`. + +Естественно, если вы используете +[пользовательский тип контекста](../guide/context#кастомизация-объекта-контекста), +вы можете передать его и в `Menu`. + +```ts +const menu = new Menu("id"); +``` + +## Добавление кнопок + +Плагин меню выстраивает клавиатуру точно так же, как это делает +[плагин для встроенных клавиатур](./keyboard#построение-встроенных-клавиатур). +Класс `Menu` заменяет класс `InlineKeyboard`. + +Вот пример меню, состоящего из четырех кнопок в форме ряда 1-2-1. + +```ts +const menu = new Menu("movements") + .text("^", (ctx) => ctx.reply("Вперед!")).row() + .text("<", (ctx) => ctx.reply("Налево!")) + .text(">", (ctx) => ctx.reply("Направо!")).row() + .text("v", (ctx) => ctx.reply("Назад!")); +``` + +Используйте `text` для добавления новых текстовых кнопок. Вы можете передать +название в функцию обработчик. + +Используйте `row`, чтобы завершить текущую строку и добавить все последующие +кнопки в новую. + +Существует множество других типов кнопок, например, для открытия URL. Посмотрите +[API](/ref/menu/menurange) этого плагина для `MenuRange`, а также +[Telegram Bot API](https://core.telegram.org/bots/api#inlinekeyboardbutton) для +`InlineKeyboardButton`. + +## Отправка меню + +Сначала необходимо установить меню. Это сделает его интерактивным. + +```ts +bot.use(menu); +``` + +Теперь вы можете просто передать меню в качестве `reply_markup` при отправке +сообщения. + +```ts +bot.command("menu", async (ctx) => { + await ctx.reply("Вот ваше меню", { reply_markup: menu }); +}); +``` + +## Динамически названия + +Когда вы называете кнопку, вы также можете передать функцию +`(ctx: Context) => string` для получения динамического текста на кнопке. Эта +функция может быть `async`, а может и не быть. + +```ts +// Создайте кнопку с именем пользователя, которая будет приветствовать его при нажатии. +const menu = new Menu("greet-me") + .text( + (ctx) => `Приветствуйте ${ctx.from?.first_name ?? "меня"}!`, // динамическое название кнопки + (ctx) => ctx.reply(`Привет, ${ctx.from.first_name}!`), // обработчик + ); +``` + +Строка, сгенерированная такой функцией, называется _динамической строкой_. +Динамические строки идеально подходят для таких вещей, как кнопки переключения. + +```ts +// Набор идентификаторов пользователей, у которых включены уведомления. +const notifications = new Set(); + +function toggleNotifications(id: number) { + if (!notifications.delete(id)) notifications.add(id); +} + +const menu = new Menu("toggle") + .text( + (ctx) => ctx.from && notifications.has(ctx.from.id) ? "🔔" : "🔕", + (ctx) => { + toggleNotifications(ctx.from.id); + ctx.menu.update(); // обновите меню! + }, + ); +``` + +Обратите внимание, что вы должны обновлять меню каждый раз, когда хотите, чтобы +ваши кнопки менялись. Вызовите `ctx.menu.update()`, чтобы убедиться, что ваше +меню будет перерисовано. + +::: tip Хранение данных В примере выше показано, как использовать плагин меню. +Не стоит хранить пользовательские настройки в объекте `Set`, так как в этом +случае все данные будут потеряны при остановке сервера. + +Вместо этого лучше использовать базу данных или плагин [сессия](./session), если +вы хотите хранить данные. +::: + +## Обновление или закрытие меню + +Когда вызывается обработчик кнопки, в `ctx.menu` появляется ряд полезных +функций. + +Если вы хотите, чтобы ваше меню перерисовалось, вы можете вызвать +`ctx.menu.update()`. Эта функция будет работать только внутри обработчиков, +которые вы установили в своем меню. Она не будет работать при вызове из другого +middleware бота, поскольку в таких случаях нет возможности узнать, _какое_ меню +должно быть обновлено. + +```ts +const menu = new Menu("time", { onMenuOutdated: false }) + .text( + () => new Date().toLocaleString(), // название кнопки - это текущее время + (ctx) => ctx.menu.update(), // обновить время по нажатию + ); +``` + +> Назначение `onMenuOutdated` объясняется [ниже](#устаревшие-меню-и-отпечатки). +> Пока что вы можете игнорировать его. + +Вы также можете обновить меню неявно, отредактировав соответствующее сообщение. + +```ts +const menu = new Menu("time") + .text( + "Какое сейчас время?", + (ctx) => ctx.editMessageText("Сейчас " + new Date().toLocaleString()), + ); +``` + +Меню определит, что вы собираетесь редактировать текст сообщения, и +воспользуется этой возможностью, чтобы обновить и кнопки под ним. В результате +часто можно избежать явного вызова `ctx.menu.update()`. + +Вызов `ctx.menu.update()` не приводит к немедленному обновлению меню. Вместо +этого он устанавливает флаг и запоминает, что нужно обновить его в какой-то +момент во время выполнения вашего middleware. Это называется _ленивым +обновлением_. Если вы позже отредактируете само сообщение, плагин может просто +использовать тот же вызов API для обновления кнопок. Это очень эффективно, и +гарантирует, что и сообщение, и клавиатура будут обновлены одновременно. + +Естественно, если вы вызовете `ctx.menu.update()`, но не запросите никаких +изменений в сообщении, плагин меню сам обновит клавиатуру до завершения работы +вашего middleware. + +Вы можете заставить меню обновляться немедленно с помощью +`await ctx.menu.update({ immediate: true })`. Обратите внимание, что +`ctx.menu.update()` вернет `Promise`, поэтому вам нужно использовать `await`! +Использование параметр `immediate` также работает для всех других операций, +которые вы можете вызвать в `ctx.menu`. Его следует использовать только в случае +необходимости. + +Если вы хотите закрыть меню, то есть убрать все кнопки, вы можете вызвать +`ctx.menu.close()`. Опять же, это будет выполнено лениво. + +## Навигация между меню + +Вы можете легко создавать меню с несколькими страницами и навигацией между ними. +Каждая страница имеет свой собственный экземпляр `Menu`. Кнопка `submenu` --- +это кнопка, позволяющая переходить на другие страницы. Навигация назад +осуществляется с помощью кнопки `back`. + +```ts +const main = new Menu("root-menu") + .text("Добро пожаловать", (ctx) => ctx.reply("Привет!")).row() + .submenu("Авторы", "credits-menu"); + +const settings = new Menu("credits-menu") + .text("Показать авторов", (ctx) => ctx.reply("Разработано grammY")) + .back("Назад"); +``` + +Обе кнопки опционально берут обработчики middleware, чтобы вы могли реагировать +на события навигации. + +Вместо того чтобы использовать кнопки `submenu` и `back` для навигации между +страницами, вы можете делать это вручную с помощью `ctx.menu.nav()`. Эта функция +принимает строку идентификатора меню и лениво выполняет навигацию. Аналогично, +обратная навигация осуществляется с помощью `ctx.menu.back()`. + +Далее необходимо связать меню, зарегистрировав их друг с другом. Регистрация +одного меню другим подразумевает их иерархию. Меню, которое регистрируется, +является родительским, а регистрируемое меню - дочерним. Ниже, `main` является +родителем `settings`, если явно не определен другой родитель. Родительское меню +используется при обратной навигации. + +```ts +// Зарегистрируйте меню настроек в главном меню. +main.register(settings); +// По желанию установите другого родителя. +main.register(settings, "back-from-settings-menu"); +``` + +Вы можете зарегистрировать столько меню, сколько захотите, и вложить их так +глубоко, как вам захочется. Идентификаторы меню позволяют легко перейти на любую +страницу. + +**Вы должны сделать интерактивным только одно меню вашей вложенной структуры +меню.** Например, передайте только корневое меню в `bot.use`. + +```ts +// Если у вас есть: +main.register(settings); + +// Сделайте это: +bot.use(main); + +// НЕ делайте это: +bot.use(main); +bot.use(settings); +``` + +**Вы можете создать несколько независимых меню и сделать их все +интерактивными.** Например, если вы создадите два несвязанных меню и вам никогда +не понадобится перемещаться между ними, то вам следует установить их независимо +друг от друга. + +```ts +// Если у вас есть независимое меню, как это: +const menuA = new Menu("menu-a"); +const menuB = new Menu("menu-b"); + +// Вы можете сделать так: +bot.use(menuA); +bot.use(menuB); +``` + +## Payload + +Вы можете хранить короткие текстовые payloads вместе со всеми навигационными и +текстовыми кнопками. Когда соответствующие обработчики будут вызваны, текстовый +payload будет доступна в разделе `ctx.match`. Это полезно, поскольку позволяет +хранить в меню немного данных. + +Вот пример меню, в котором в payload хранится текущее время. Другим вариантом +использования может быть, например, хранение индекса в пагинационном меню. + +```ts +function generatePayload() { + return Date.now().toString(); +} + +const menu = new Menu("store-current-time-in-payload") + .text( + { text: "ОТМЕНА!", payload: generatePayload }, + async (ctx) => { + // Дайте пользователю 5 секунд, чтобы отменить действие. + const text = Date.now() - Number(ctx.match) < 5000 + ? "Операция была успешно отменена." + : "Слишком поздно. Ваши видео с кошками уже стали вирусными в интернете."; + await ctx.reply(text); + }, + ); + +bot.use(menu); +bot.command("publish", async (ctx) => { + await ctx.reply( + "Видео будет отправлено. У вас есть 5 секунд, чтобы отменить это.", + { + reply_markup: menu, + }, + ); +}); +``` + +::: tip Лимиты Payload нельзя использовать для хранения значительных объемов +данных. Единственное, что вы можете хранить --- это короткие строки, обычно не +превышающие 50 байт, такие как индекс или идентификатор. Если вы действительно +хотите хранить пользовательские данные, такие как идентификатор файла, URL или +что-то еще, вам следует использовать [сессии](./session). + +Также обратите внимание, что payload всегда генерируется на основе текущего +контекстного объекта. Это означает, что имеет значение, _откуда_ вы переходите к +меню, что может привести к неожиданным результатам. Например, когда меню +[устарело](#устаревшие-меню-и-отпечатки), оно будет перерисовано _на основе нажатия кнопки устаревшего меню_. +::: + +Payload'ы также хорошо сочетаются с динамическими диапазонами. + +## Динамические диапазоны + +До сих пор мы рассматривали только динамическое изменение текста на кнопке. Вы +также можете динамически изменять структуру меню, чтобы добавлять и удалять +кнопки на лету. + +::: danger Изменение меню во время обработки сообщений +Вы не можете создавать или изменять меню во время обработки сообщений. Все меню должны быть полностью +созданы и зарегистрированы до запуска вашего бота. Это означает, что вы не +можете сделать `new Menu(«id»)` в обработчике вашего бота. Вы не можете вызвать +`menu.text` или т.п. в обработчике вашего бота. + +Добавление новых меню во время работы бота приведет к утечке памяти. Ваш бот +будет все больше и больше замедляться и в конце концов упадет. + +Однако вы можете воспользоваться динамическими диапазонами, описанными в этом +разделе. Они позволяют произвольно изменять структуру существующего экземпляра +меню, поэтому они не менее эффективны. Используйте динамические диапазоны! +::: + +Вы можете позволить генерировать часть кнопок меню на лету (или все кнопки, если +хотите). Мы называем эту часть меню _динамическим диапазоном_. Другими словами, +вместо того чтобы определять кнопки непосредственно в меню, вы можете передать +функцию, которая создаст кнопки при рендеринге меню. Самый простой способ +создать динамический диапазон в этой функции --- использовать класс `MenuRange`, +который предоставляет этот плагин. Класс `MenuRange` предоставляет вам точно +такие же функции, как и меню, но у него нет идентификатора, и его нельзя +зарегистрировать. + +```ts +const menu = new Menu("dynamic"); +menu + .url("О нас", "https://grammy.dev/plugins/menu").row() + .dynamic(() => { + // Создайте часть меню динамически! + const range = new MenuRange(); + for (let i = 0; i < 3; i++) { + range + .text(i.toString(), (ctx) => ctx.reply(`Вы выбрали ${i}`)) + .row(); + } + return range; + }) + .text("Отмена", (ctx) => ctx.deleteMessage()); +``` + +Функция построения диапазона, которую вы передаете `dynamic`, может быть +`async`, так что вы можете даже считывать данные из API или базы данных, прежде +чем вернуть новый диапазон меню. **Во многих случаях имеет смысл генерировать +динамический диапазон на основе данных [сессии](./session).**. + +Функция построения диапазона принимает в качестве первого аргумента объект +контекста. (В приведенном примере он не указан). По желанию, в качестве второго +аргумента после `ctx`, вы можете получить свежий экземпляр `MenuRange`. Вы +можете изменить его вместо того, чтобы возвращать свой собственный экземпляр, +если вам так больше нравится. Вот как можно использовать два параметра функции +построения диапазона. + +```ts +menu.dynamic((ctx, range) => { + for (const text of ctx.session.items) { + range // Нет необходимости в `new MenuRange()` или `return`. + .text(text, (ctx) => ctx.reply(text)) + .row(); + } +}); +``` + +Важно, чтобы ваша функция работала определенным образом, иначе ваши меню могут +показать странное поведение или даже выдать ошибку. Поскольку меню всегда +[отображается дважды](#как-это-работает) (один раз при отправке меню и один раз +при нажатии кнопки), вам нужно убедиться, что: + +1. **У вас нет никаких побочных эффектов в функции, которая строит динамический + диапазон**. Не отправляйте сообщения. Не записывайте данные сессии. Не + изменяйте переменные за пределами функции. Посмотрите + [Википедию о побочных эффектах](https://en.wikipedia.org/wiki/Side_effect_(computer_science)). +2. **Ваша функция стабильна**, т.е. не зависит от случайности, текущего времени + или других быстро меняющихся источников данных. Она должна генерировать одни + и те же кнопки при первом и втором рендеринге меню. В противном случае плагин + меню не сможет сопоставить правильный обработчик с нажатой кнопкой. Вместо + этого он [определит](#устаревшие-меню-и-отпечатки), что ваше меню устарело, и + откажется вызывать обработчики. + +## Ответы на callback запросы вручную + +Плагин меню будет автоматически вызывать `answerCallbackQuery` для своих +собственных кнопок. Вы можете установить значение `autoAnswer: false`, если +хотите отключить это. + +```ts +const menu = new Menu("id", { autoAnswer: false }); +``` + +Теперь вам придется самостоятельно вызывать `answerCallbackQuery`. Это позволит +вам передавать пользовательские сообщения, которые будут отображаться +пользователю. + +## Устаревшие меню и отпечатки + +Допустим, у вас есть меню, в котором пользователь может включать и выключать +уведомления, как в примере [вверху](#динамически-названия). Если пользователь +дважды отправит `/settings`, он получит одно и то же меню дважды. Но изменение +настроек уведомления в одном из двух сообщений не приведет к обновлению другого! + +Очевидно, что мы не можем отслеживать все сообщения о настройках в чате и +обновлять все старые меню по всей истории чата. Для этого пришлось бы +использовать так много вызовов API, что Telegram ограничил бы скорость вашего +бота. Кроме того, для запоминания всех идентификаторов сообщений каждого меню во +всех чатах потребуется много места. Это непрактично. + +Решение заключается в том, чтобы проверять, не устарело ли меню, _до_ выполнения +каких-либо действий. Таким образом, мы будем обновлять устаревшие меню только в +том случае, если пользователь действительно начнет нажимать на кнопки в них. +Плагин меню делает это автоматически, так что вам не нужно об этом беспокоиться. + +Вы можете настроить, что именно произойдет при обнаружении устаревшего меню. По +умолчанию пользователю будет показано сообщение "Меню устарело, попробуйте еще +раз!", и меню будет обновлено. Вы можете определить пользовательское поведение в +конфигурации в разделе `onMenuOutdated`. + +```ts +// Отображаемое пользовательское сообщение +const menu0 = new Menu("id", { + onMenuOutdated: "Обновлено, попробуйте теперь.", +}); +// Пользовательская функция обработчика +const menu1 = new Menu("id", { + onMenuOutdated: async (ctx) => { + await ctx.answerCallbackQuery(); + await ctx.reply("Вот ваше новое меню", { reply_markup: menu1 }); + }, +}); +// Полностью отключите проверку на устаревание (могут запускаться неправильные обработчики кнопок). +const menu2 = new Menu("id", { onMenuOutdated: false }); +``` + +У нас есть система для проверки того, устарело ли меню. Мы считаем его +устаревшим, если: + +- Изменилась форма меню (количество строк или количество кнопок в любой строке). +- Позиция строки/столбца нажатой кнопки вышла за пределы диапазона. +- Изменился ярлык нажатой кнопки. +- Нажатая кнопка не содержит обработчика. + +Возможно, что ваше меню изменится, а все вышеперечисленное останется неизменным. +Также возможно, что меню принципиально не меняется (т.е. поведение обработчиков +не меняется), хотя вышеуказанная система указывает на то, что меню устарело. Оба +сценария маловероятны для большинства ботов, но если вы создаете меню, в котором +такое возможно, вам следует использовать функцию отпечатков. + +```ts +function ident(ctx: Context): string { + // Возвращаем строку, которая будет меняться тогда и только тогда, когда ваше меню изменится + // настолько существенно, что его следует считать устаревшим. + return ctx.session.myStateIdentifier; +} +const menu = new Menu("id", { fingerprint: (ctx) => ident(ctx) }); +``` + +Строка отпечатков заменит вышеупомянутую систему. Таким образом, вы можете быть +уверены, что устаревшие меню всегда будут обнаружены. + +## Как это работает + +Плагин меню работает полностью без хранения каких-либо данных. Это важно для +больших ботов с миллионами пользователей. Сохранение состояния всех меню заняло +бы слишком много памяти. + +Когда вы создаете объекты меню и связываете их вместе с помощью вызова +`register`, никакие меню на самом деле не создаются. Вместо этого плагин меню +запоминает, как собирать новые меню на основе ваших операций. При отправке меню +он будет воспроизводить эти операции для визуализации вашего меню. Это включает +в себя прокладку всех динамических диапазонов и генерацию всех динамических +названий. После отправки меню отрисованный массив кнопок будет снова забыт. + +При отправке меню каждая кнопка содержит callback запрос, который хранит: + +- Идентификатор меню. +- Позиция кнопки в строке/столбце. +- Необязательный payload. +- Флаг отпечатка, который хранит информацию о том, был ли использован отпечаток + в меню. +- 4-байтовый хэш, который кодирует либо отпечаток, либо схему меню и метку + кнопки. + +Таким образом, мы можем определить, какая именно кнопка меню была нажата. Меню +будет обрабатывать нажатия кнопок только в том случае, если: + +- Идентификаторы меню совпадают. +- Указана строка/колонка. +- Существует флаг отпечатка. + +Когда пользователь нажимает кнопку меню, нам нужно найти обработчик, который был +добавлен к этой кнопке во время рендеринга меню. Таким образом, мы просто снова +отображаем старое меню. Однако на этот раз нам не нужен полный макет --- нам +нужна только общая структура и одна конкретная кнопка. Следовательно, плагин +меню будет выполнять неглубокий рендеринг, чтобы быть более эффективным. Другими +словами, меню будет отрисовываться только частично. + +Как только нажатая кнопка снова становится известна (и мы проверили, что меню не +является [устаревшим](#устаревшие-меню-и-отпечатки)), мы вызываем обработчик. + +Внутри плагин меню активно использует +[трансформирующие API функции](../advanced/transformers), например, для быстрого +рендеринга исходящих меню на лету. + +Когда вы регистрируете меню в большой навигационной иерархии, они фактически не +будут хранить эти ссылки явно. Под капотом все меню одной структуры добавляются +в один большой пул, и этот пул разделяется между всеми содержащимися +экземплярами. Каждое меню отвечает за каждое другое в индексе, и они могут +обрабатывать и отображать друг друга. (Чаще всего только корневое меню +передается в `bot.use` и получает любые обновления. В таких случаях один +экземпляр будет обрабатывать весь пул.) В результате вы можете перемещаться +между произвольными меню без ограничений, при этом обработка обновлений может +происходить за +[`O(1)` временной сложности](https://en.wikipedia.org/wiki/Time_complexity#Constant_time), +поскольку нет необходимости искать по всей иерархии нужное меню для обработки +любого нажатия кнопки. + +## Краткая информация о плагине + +- Название: `menu` +- [Исходник](https://github.com/grammyjs/menu) +- [Ссылка](/ref/menu/) diff --git a/site/docs/ru/plugins/middlewares.md b/site/docs/ru/plugins/middlewares.md new file mode 100644 index 000000000..44eddc015 --- /dev/null +++ b/site/docs/ru/plugins/middlewares.md @@ -0,0 +1,79 @@ +--- +prev: false +next: false +--- + +# Набор полезных middleware + +Я продолжал переписывать одни и те же middleware снова и снова для всех моих +ботов, поэтому я решил извлечь их все в отдельный пакет. + +## Установка + +`yarn add grammy-middlewares` + +## Использование + +Все middleware являются фабриками, хотя не все из них должны быть таковыми. Я +решил сделать API однородным. + +Некоторые из фабрик потребляют необязательные или обязательные параметры. + +```ts +import { + ignoreOld, + onlyAdmin, + onlyPublic, + onlySuperAdmin, + sequentialize, +} from "grammy-middlewares"; + +// ... + +bot.use( + ignoreOld(), + onlyAdmin((ctx) => ctx.reply("Только админы могут это делать")), + onlyPublic((ctx) => + ctx.reply("Вы можете использовать только публичные чаты") + ), + onlySuperAdmin(env.SUPER_ADMIN_ID), + sequentialize(), +); +``` + +## Middlewares + +### `ignoreOld` + +Игнорирует старые обновления, что полезно, когда бот долгое время не работает. +Вы можете дополнительно указать таймаут в секундах, который по умолчанию равен +`5 * 60`. + +### `onlyAdmin` + +Проверяет, является ли пользователь администратором. При желании можно указать +`errorHandler`, который будет вызван с контекстом, если пользователь не является +администратором. + +### `onlyPublic` + +Проверяет, является ли чат групповым или каналом. При желании можно указать +`errorHandler`, который будет вызван с контекстом, если это не групповой чат или +канал. + +### `onlySuperAdmin` + +Проверяет, является ли пользователь суперадминистратором. Необходимо указать +идентификатор суперадминистратора. + +### `sequentialize` + +Основной [последовательностью](../advanced/scaling#параллельность-это-сложно) +middleware который принимает идентификатор чата в качестве последовательного +идентификатора. + +## Краткая информация о плагине + +- Название: `grammy-middlewares` +- [Исходник](https://github.com/backmeupplz/grammy-middlewares) +- Документация: diff --git a/site/docs/ru/plugins/parse-mode.md b/site/docs/ru/plugins/parse-mode.md new file mode 100644 index 000000000..bf4a5b5bb --- /dev/null +++ b/site/docs/ru/plugins/parse-mode.md @@ -0,0 +1,228 @@ +--- +prev: false +next: false +--- + +# Плагин Parse Mode (`parse-mode`) + +Этот плагин предоставляет трансформатор для установки `parse_mode` по умолчанию, +а также middleware для взаимодействия `Context` с привычными методами `reply`, +`replyWithHTML`, `replyWithMarkdown` и т.д. + +## Использование (Улучшение опыта форматирования) + +::: code-group + +```ts [TypeScript] +import { Bot, Context } from "grammy"; +import { bold, fmt, hydrateReply, italic, link } from "@grammyjs/parse-mode"; + +import type { ParseModeFlavor } from "@grammyjs/parse-mode"; + +const bot = new Bot>(""); + +// Установка плагина +bot.use(hydrateReply); + +bot.command("demo", async (ctx) => { + await ctx.replyFmt(fmt`${bold("полужирный!")} +${bold(italic("полужирно-курсивный!"))} +${ + bold(fmt`полужирный ${link("полужирная ссылка", "example.com")} полужирный`) + }`); + + // fmt также может быть вызвана как любая другая функция. + await ctx.replyFmt( + fmt( + ["", " и ", " и ", ""], + fmt`${bold("полужирный")}`, + fmt`${bold(italic("полужирно-курсивный"))}`, + fmt`${italic("курсивный")}`, + ), + ); +}); + +bot.start(); +``` + +```js [JavaScript] +const { Bot, Context } = require("grammy"); +const { bold, fmt, hydrateReply, italic, link } = require( + "@grammyjs/parse-mode", +); + +const bot = new Bot(""); + +// Установка плагина +bot.use(hydrateReply); + +bot.command("demo", async (ctx) => { + await ctx.replyFmt(fmt`${bold("полужирный!")} +${bold(italic("жирно-курсивный!"))} +${ + bold( + fmt`полужирный ${link("полу-жирная ссылка", "example.com")} полужирный`, + ) + }`); + + // fmt также может быть вызвана как любая другая функция. + await ctx.replyFmt( + fmt( + ["", " и ", " и ", ""], + fmt`${bold("полужирный")}`, + fmt`${bold(italic("полужирно-курсивный"))}`, + fmt`${italic("курсивный")}`, + ), + ); +}); + +bot.start(); +``` + +```ts [Deno] +import { Bot, Context } from "https://deno.land/x/grammy/mod.ts"; +import { + bold, + fmt, + hydrateReply, + italic, + link, +} from "https://deno.land/x/grammy_parse_mode/mod.ts"; + +import type { ParseModeFlavor } from "https://deno.land/x/grammy_parse_mode/mod.ts"; + +const bot = new Bot>(""); + +// Установка плагина +bot.use(hydrateReply); + +bot.command("demo", async (ctx) => { + await ctx.replyFmt(fmt`${bold("полужирный!")} +${bold(italic("жирно-курсивный!"))} +${ + bold( + fmt`полужирный ${link("полу-жирная ссылка", "example.com")} полужирный`, + ) + }`); + + // fmt также может быть вызвана как любая другая функция. + await ctx.replyFmt( + fmt( + ["", " и ", " и ", ""], + fmt`${bold("полужирный")}`, + fmt`${bold(italic("полужирно-курсивный"))}`, + fmt`${italic("курсивный")}`, + ), + ); +}); + +bot.start(); +``` + +::: + +## Использование (parse mode и методы ответа по умолчанию) + +::: code-group + +```ts [TypeScript] +import { Bot, Context } from "grammy"; +import { hydrateReply, parseMode } from "@grammyjs/parse-mode"; + +import type { ParseModeFlavor } from "@grammyjs/parse-mode"; + +const bot = new Bot>(""); + +// Установка плагина +bot.use(hydrateReply); + +// Установите parse_mod по умолчанию для ctx.reply. +bot.api.config.use(parseMode("MarkdownV2")); + +bot.command("demo", async (ctx) => { + await ctx.reply("*Это* `форматирование` _по_ умолчанию"); + await ctx.replyWithHTML( + "Это форматирование с помощью HTML", + ); + await ctx.replyWithMarkdown("*Это* `форматирование` с помощью _Markdown_"); + await ctx.replyWithMarkdownV1( + "*Это* `форматирование` с помощью _MarkdownV1_", + ); + await ctx.replyWithMarkdownV2( + "*Это* `форматирование` с помощью _MarkdownV2_", + ); +}); + +bot.start(); +``` + +```js [JavaScript] +const { Bot, Context } = require("grammy"); +const { hydrateReply, parseMode } = require("@grammyjs/parse-mode"); + +const bot = new Bot(""); + +// Установка плагина +bot.use(hydrateReply); + +// Установите parse_mod по умолчанию для ctx.reply. +bot.api.config.use(parseMode("MarkdownV2")); + +bot.command("demo", async (ctx) => { + await ctx.reply("*Это* `форматирование` _по_ умолчанию"); + await ctx.replyWithHTML( + "Это форматирование с помощью HTML", + ); + await ctx.replyWithMarkdown("*Это* `форматирование` с помощью _Markdown_"); + await ctx.replyWithMarkdownV1( + "*Это* `форматирование` с помощью _MarkdownV1_", + ); + await ctx.replyWithMarkdownV2( + "*Это* `форматирование` с помощью _MarkdownV2_", + ); +}); + +bot.start(); +``` + +```ts [Deno] +import { Bot, Context } from "https://deno.land/x/grammy/mod.ts"; +import { + hydrateReply, + parseMode, +} from "https://deno.land/x/grammy_parse_mode/mod.ts"; + +import type { ParseModeFlavor } from "https://deno.land/x/grammy_parse_mode/mod.ts"; + +const bot = new Bot>(""); + +// Установка плагина +bot.use(hydrateReply); + +// Установите parse_mod по умолчанию для ctx.reply. +bot.api.config.use(parseMode("MarkdownV2")); + +bot.command("demo", async (ctx) => { + await ctx.reply("*Это* `форматирование` _по_ умолчанию"); + await ctx.replyWithHTML( + "Это форматирование с помощью HTML", + ); + await ctx.replyWithMarkdown("*Это* `форматирование` с помощью _Markdown_"); + await ctx.replyWithMarkdownV1( + "*Это* `форматирование` с помощью _MarkdownV1_", + ); + await ctx.replyWithMarkdownV2( + "*Это* `форматирование` с помощью _MarkdownV2_", + ); +}); + +bot.start(); +``` + +::: + +## Краткая информация о плагине + +- Название: `parse-mode` +- [Исходник](https://github.com/grammyjs/parse-mode) +- [Ссылка](/ref/parse-mode/) diff --git a/site/docs/ru/plugins/ratelimiter.md b/site/docs/ru/plugins/ratelimiter.md new file mode 100644 index 000000000..5f570653c --- /dev/null +++ b/site/docs/ru/plugins/ratelimiter.md @@ -0,0 +1,238 @@ +--- +prev: false +next: false +--- + +# Лимит запросов пользователей (`ratelimiter`) + +ratelimiter --- это middleware для ограничения скорости ботов Telegram, созданных с помощью grammY или [Telegraf](https://github.com/telegraf/telegraf). +Проще говоря, это плагин, который поможет вам предотвратить сильную спам рассылку в ваших ботах. +Чтобы лучше понять суть ratelimiter, вы можете взглянуть на следующую иллюстрацию: + +![Роль ratelimiter в борьбе со спамом](/images/ratelimiter-role.png) + +## Как именно это работает? + +При нормальных обстоятельствах каждый запрос будет обработан и получит ответ от вашего бота, а значит, заспамить его будет не так уж сложно. +Каждый пользователь может отправлять несколько запросов в секунду, и вашему коду придется обрабатывать каждый запрос, но как это остановить? +С помощью ratelimiter! + +::: warning Ограничение скорости пользователей, а не серверов Telegram! +Обратите внимание, что этот пакет **НЕ** ограничивает входящие запросы от серверов Telegram, вместо этого он отслеживает входящие запросы по `from.id` и отклоняет их по прибытии, поэтому на ваши сервера не ложится дополнительная нагрузка по обработке. +::: + +## Настройка + +Этот плагин предоставляет 5 настраиваемых опций: + +- `timeFrame`: Временной интервал, в течение которого будут отслеживаться запросы (по умолчанию `1000` мс). +- `limit`: Количество запросов, разрешенных в каждом `таймфрейме` (по умолчанию `1`). +- `storageClient`: Тип хранилища, которое будет использоваться для отслеживания пользователей и их запросов. + По умолчанию используется `MEMORY_STORE`, который использует in-memory [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), но вы также можете передать клиент Redis (подробнее в [О storageClient](#о-storageclient)). +- `onLimitExceeded`: Функция, описывающая, что делать, если пользователь превысил лимит (по умолчанию игнорирует дополнительные запросы). +- `keyGenerator`: Функция, возвращающая уникальный ключ, сгенерированный для каждого пользователя (по умолчанию используется `from.id`). + Этот ключ используется для идентификации пользователя, поэтому он должен быть уникальным, специфичным для пользователя и иметь строковый формат. + +### О `storageClient` + +Вариант `MEMORY_STORE` или отслеживание в памяти подходит для большинства ботов, однако если вы реализуете кластеризацию для своего бота, вы не сможете эффективно использовать хранилище в памяти. +Именно поэтому предусмотрена возможность использования Redis. +Вы можете передать клиент Redis из [ioredis](https://github.com/redis/ioredis) или [redis](https://deno.land/x/redis), если вы используете Deno. +В действительности, любой драйвер Redis, реализующий методы `incr` и `pexpire`, должен работать просто отлично. +ratelimiter не зависит от драйвера. + +> Примечание: Для использования клиента хранилища Redis с ratelimiter на вашем сервере должен быть установлен redis-server **2.6.0** и выше. +> Более старые версии Redis не поддерживаются. + +## Как использовать + +Существует два способа использования ratelimiter: + +- Принятие настроек по умолчанию ([Настройки по умолчанию](#настроики-по-умолчанию)). +- Передача пользовательского объекта, содержащего ваши настройки ([Ручная настройка](#ручная-настроика)). + +### Настройки по умолчанию + +Этот фрагмент демонстрирует самый простой способ использования ratelimiter, который принимает поведение по умолчанию: + +::: code-group + +```ts [TypeScript] +import { limit } from "@grammyjs/ratelimiter"; + +// Ограничивает обработку сообщений до одного сообщения в секунду для каждого пользователя. +bot.use(limit()); +``` + +```js [JavaScript] +const { limit } = require("@grammyjs/ratelimiter"); + +// Ограничивает обработку сообщений до одного сообщения в секунду для каждого пользователя. +bot.use(limit()); +``` + +```ts [Deno] +import { limit } from "https://deno.land/x/grammy_ratelimiter/mod.ts"; + +// Ограничивает обработку сообщений до одного сообщения в секунду для каждого пользователя. +bot.use(limit()); +``` + +::: + +### Ручная настройка + +Как упоминалось ранее, вы можете передать объект `Options` в метод `limit()`, чтобы изменить поведение ratelimiter. + +::: code-group + +```ts [TypeScript] +import Redis from "ioredis"; +import { limit } from "@grammyjs/ratelimiter"; + +const redis = new Redis(...); + +bot.use( + limit({ + // Разрешите обрабатывать только 3 сообщения каждые 2 секунды. + timeFrame: 2000, + limit: 3, + + // По умолчанию используется значение «MEMORY_STORE». Если вы не хотите использовать Redis, не передавайте storageClient вообще. + storageClient: redis, + + // Эта функция вызывается при превышении лимита. + onLimitExceeded: async (ctx) => { + await ctx.reply("Пожалуйста, воздержитесь от отправки слишком большого количества запросов!"); + }, + + // Обратите внимание, что ключ должен быть числом в строковом формате, например «123456789». + keyGenerator: (ctx) => { + return ctx.from?.id.toString(); + }, + }) +); +``` + +```js [JavaScript] +const Redis = require("ioredis"); +const { limit } = require("@grammyjs/ratelimiter"); + +const redis = new Redis(...); + +bot.use( + limit({ + // Разрешите обрабатывать только 3 сообщения каждые 2 секунды. + timeFrame: 2000, + limit: 3, + + // По умолчанию используется значение «MEMORY_STORE». Если вы не хотите использовать Redis, не передавайте storageClient вообще. + storageClient: redis, + + // Эта функция вызывается при превышении лимита. + onLimitExceeded: async (ctx) => { + await ctx.reply("Пожалуйста, воздержитесь от отправки слишком большого количества запросов!"); + }, + + // Обратите внимание, что ключ должен быть числом в строковом формате, например «123456789». + keyGenerator: (ctx) => { + return ctx.from?.id.toString(); + }, + }) +); +``` + +```ts [Deno] +import { connect } from "https://deno.land/x/redis/mod.ts"; +import { limit } from "https://deno.land/x/grammy_ratelimiter/mod.ts"; + +const redis = await connect(...); + +bot.use( + limit({ + // Разрешите обрабатывать только 3 сообщения каждые 2 секунды. + timeFrame: 2000, + limit: 3, + + // По умолчанию используется значение «MEMORY_STORE». Если вы не хотите использовать Redis, не передавайте storageClient вообще. + storageClient: redis, + + // Эта функция вызывается при превышении лимита. + onLimitExceeded: async (ctx) => { + await ctx.reply("Пожалуйста, воздержитесь от отправки слишком большого количества запросов!"); + }, + + // Обратите внимание, что ключ должен быть числом в строковом формате, например «123456789». + keyGenerator: (ctx) => { + return ctx.from?.id.toString(); + }, + }) +); +``` + +::: + +Как видно из примера выше, каждому пользователю разрешено отправлять 3 запроса каждые 2 секунды. +Если пользователь отправляет больше запросов, бот отвечает _Пожалуйста, воздержитесь от отправки слишком большого количества запросов_. +Этот запрос не будет отправлен дальше и сразу же будет пропущен, так как мы не вызываем [next()](../guide/middleware#стек-middleware) в middleware. + +> Примечание: Чтобы избежать переполнения серверов Telegram, `onLimitExceeded` выполняется только один раз в каждом `таймфрейме`. + +Другим вариантом использования может быть ограничение входящих запросов от чата, а не от конкретного пользователя: + +::: code-group + +```ts [TypeScript] +import { limit } from "@grammyjs/ratelimiter"; + +bot.use( + limit({ + keyGenerator: (ctx) => { + if (ctx.hasChatType(["group", "supergroup"])) { + // Обратите внимание, что ключ должен быть числом в формате строки, например «123456789». + return ctx.chat.id.toString(); + } + }, + }), +); +``` + +```js [JavaScript] +const { limit } = require("@grammyjs/ratelimiter"); + +bot.use( + limit({ + keyGenerator: (ctx) => { + if (ctx.hasChatType(["group", "supergroup"])) { + // Обратите внимание, что ключ должен быть числом в формате строки, например «123456789». + return ctx.chat.id.toString(); + } + }, + }), +); +``` + +```ts [Deno] +import { limit } from "https://deno.land/x/grammy_ratelimiter/mod.ts"; + +bot.use( + limit({ + keyGenerator: (ctx) => { + if (ctx.hasChatType(["group", "supergroup"])) { + // Обратите внимание, что ключ должен быть числом в формате строки, например «123456789». + return ctx.chat.id.toString(); + } + }, + }), +); +``` + +::: + +В этом примере мы использовали `chat.id` в качестве уникального ключа для ограничения скорости. + +## Краткая информация о плагине + +- Название: `ratelimiter` +- [Исходник](https://github.com/grammyjs/ratelimiter) +- [Ссылка](/ref/ratelimiter/) diff --git a/site/docs/ru/plugins/router.md b/site/docs/ru/plugins/router.md new file mode 100644 index 000000000..3f1c9c797 --- /dev/null +++ b/site/docs/ru/plugins/router.md @@ -0,0 +1,491 @@ +--- +prev: false +next: false +--- + +# Роутер (`router`) + +Класс `Router` ([документация API](/ref/router/)) предоставляет возможность +структурировать вашего бота, направляя объекты контекста в различные части +вашего кода. Это более сложная версия `bot.route` в `Composer` +([grammY API](/ref/core/composer#route)). + +## Пример + +Вот пример использования роутера, который говорит сам за себя. + +```ts +const router = new Router((ctx) => { + // Определите, какой маршрут выбрать здесь. + return "key"; +}); + +router.route("key", async (ctx) => {/* ... */}); +router.route("other-key", async (ctx) => {/* ... */}); +router.otherwise((ctx) => {/* ... */}); // вызывается, если ни один маршрут не соответствует + +bot.use(router); +``` + +## Связь с Middleware + +Естественно, плагин роутера легко интегрируется с +[деревьями middleware](../advanced/middleware). Например, вы можете фильтровать +обновления после их маршрутизации. + +```ts +router.route("key").on("message:text", async (ctx) => {/* ... */}); + +const other = router.otherwise(); +other.on(":text", async (ctx) => {/* ... */}); +other.use((ctx) => {/* ... */}); +``` + +Возможно, вы также захотите просмотреть этот +[раздел](../guide/filter-queries#комбинирование-запросов-с-другими-методами) о +комбинировании обработчиков middlware. + +## Объединение роутера с сессиями + +Роутеры хорошо работают вместе с [сессиями](./session). Например, объединение +этих двух концепций позволяет воссоздать формы в интерфейсе чата. + +> Обратите внимание, что гораздо лучшим решением является использование плагина +> [conversations](./conversations). Оставшаяся часть этой страницы устарела с +> тех пор, как был создан этот плагин. Мы оставим эту страницу в качестве +> справочника для тех, кто использовал маршрутизатор для форм. + +Допустим, вы хотите создать бота, который будет сообщать пользователям, сколько +дней осталось до их дня рождения. Для того чтобы вычислить количество дней, бот +должен знать месяц (например, июнь) и день месяца (например, 15), когда у +пользователя день рождения. + +Поэтому бот должен задать два вопроса: + +1. В каком месяце родился пользователь? +2. В какой день месяца родился пользователь? + +Только если оба значения известны, бот может сказать пользователю, сколько дней +осталось. + +Вот как может быть реализован подобный бот: + +::: code-group + +```ts [TypeScript] +import { Bot, Context, Keyboard, session, SessionFlavor } from "grammy"; +import { Router } from "@grammyjs/router"; + +interface SessionData { + step: "idle" | "day" | "month"; // на каком этапе формы мы находимся + dayOfMonth?: number; // день, в котором родился пользователь + month?: number; // месяц, в котором родился пользователь +} +type MyContext = Context & SessionFlavor; + +const bot = new Bot(""); + +// Используйте сессии. +bot.use(session({ initial: (): SessionData => ({ step: "idle" }) })); + +// Определите некоторые команды. +bot.command("start", async (ctx) => { + await ctx.reply(`Добро пожаловать! +Я могу сказать, сколько дней осталось до твоего рождения! +Отправь /birthday чтобы начать`); +}); + +bot.command("birthday", async (ctx) => { + const day = ctx.session.dayOfMonth; + const month = ctx.session.month; + if (day !== undefined && month !== undefined) { + // Информация уже предоставлена! + await ctx.reply(`Ваш день рождения через ${getDays(month, day)} дней!`); + } else { + // Отсутствующая информация, войдите в форму на основе роутера + ctx.session.step = "day"; + await ctx.reply( + "Пожалуйста, отправьте мне день месяца \ +в который вы родились в виде числа!", + ); + } +}); + +// Используйте роутер +const router = new Router((ctx) => ctx.session.step); + +// Определите этап, на который будет обрабатывать день. +const day = router.route("day"); +day.on("message:text", async (ctx) => { + const day = parseInt(ctx.msg.text, 10); + if (isNaN(day) || day < 1 || 31 < day) { + await ctx.reply("Это не верный день, попробуйте снова!"); + return; + } + ctx.session.dayOfMonth = day; + // Форма для перехода к месяцу + ctx.session.step = "month"; + await ctx.reply("Получил, теперь назовите мне месяц!", { + reply_markup: { + one_time_keyboard: true, + keyboard: new Keyboard() + .text("Янв").text("Февр").text("Март").row() + .text("Апр").text("Май").text("Июнь").row() + .text("Июль").text("Авг").text("Сент").row() + .text("Окт").text("Нояб").text("Дек").build(), + }, + }); +}); +day.use((ctx) => + ctx.reply("Пожалуйста, пришлите мне день в виде текстового сообщения!") +); + +// Определите шаг, который обрабатывает месяц. +const month = router.route("month"); +month.on("message:text", async (ctx) => { + // Не должно происходить, если только данные сессии не повреждены. + const day = ctx.session.dayOfMonth; + if (day === undefined) { + await ctx.reply("Мне нужен день, когда вы родились!"); + ctx.session.step = "day"; + return; + } + + const month = months.indexOf(ctx.msg.text); + if (month === -1) { + await ctx.reply( + "Это неправильный месяц, \ +используйте одну из кнопок!", + ); + return; + } + + ctx.session.month = month; + const diff = getDays(month, day); + await ctx.reply( + `Ваш день рождения ${months[month]} ${day}. +Это через ${diff} дней!`, + { reply_markup: { remove_keyboard: true } }, + ); + ctx.session.step = "idle"; +}); +month.use((ctx) => ctx.reply("Пожалуйста, нажмите одну из кнопок!")); + +// Определите шаг, на котором обрабатываются все остальные случаи. +router.otherwise(async (ctx) => { // idle + await ctx.reply( + "Отправьте /birthday чтобы понять, сколько вам осталось ждать.", + ); +}); + +bot.use(router); // используйте роутер +bot.start(); + +// Утилиты для преобразования даты +const months = [ + "Янв", + "Февр", + "Март", + "Апр", + "Май", + "Июнь", + "Июль", + "Авг", + "Сент", + "Окт", + "Нояб", + "Дек", +]; +function getDays(month: number, day: number) { + const bday = new Date(); + const now = Date.now(); + bday.setMonth(month); + bday.setDate(day); + if (bday.getTime() < now) bday.setFullYear(bday.getFullYear() + 1); + const diff = (bday.getTime() - now) / (1000 * 60 * 60 * 24); + return diff; +} +``` + +```js [JavaScript] +const { Bot, Context, Keyboard, session, SessionFlavor } = require("grammy"); +const { Router } = require("@grammyjs/router"); + +const bot = new Bot(""); + +// Используйте сессии. +bot.use(session({ initial: () => ({ step: "idle" }) })); + +// Определите некоторые команды. +bot.command("start", async (ctx) => { + await ctx.reply(`Добро пожаловать! +Я могу сказать, сколько дней осталось до твоего рождения! +Отправь /birthday чтобы начать`); +}); + +bot.command("birthday", async (ctx) => { + const day = ctx.session.dayOfMonth; + const month = ctx.session.month; + if (day !== undefined && month !== undefined) { + // Информация уже предоставлена! + await ctx.reply(`Ваш день рождения через ${getDays(month, day)} дней!`); + } else { + // Отсутствующая информация, войдите в форму на основе роутера + ctx.session.step = "day"; + await ctx.reply( + "Пожалуйста, отправьте мне день месяца \ +в который вы родились в виде числа!", + ); + } +}); + +// Используйте роутер +const router = new Router((ctx) => ctx.session.step); + +// Определите этап, на который будет обрабатывать день. +const day = router.route("day"); +day.on("message:text", async (ctx) => { + const day = parseInt(ctx.msg.text, 10); + if (isNaN(day) || day < 1 || 31 < day) { + await ctx.reply("Это не верный день, попробуйте снова!"); + return; + } + ctx.session.dayOfMonth = day; + // Форма для перехода к месяцу + ctx.session.step = "month"; + await ctx.reply("Получил, теперь назовите мне месяц!", { + reply_markup: { + one_time_keyboard: true, + keyboard: new Keyboard() + .text("Янв").text("Февр").text("Март").row() + .text("Апр").text("Май").text("Июнь").row() + .text("Июль").text("Авг").text("Сент").row() + .text("Окт").text("Нояб").text("Дек").build(), + }, + }); +}); +day.use((ctx) => + ctx.reply("Пожалуйста, пришлите мне день в виде текстового сообщения!") +); + +// Определите шаг, который обрабатывает месяц. +const month = router.route("month"); +month.on("message:text", async (ctx) => { + // Не должно происходить, если только данные сессии не повреждены. + const day = ctx.session.dayOfMonth; + if (day === undefined) { + await ctx.reply("Мне нужен день, когда вы родились!"); + ctx.session.step = "day"; + return; + } + + const month = months.indexOf(ctx.msg.text); + if (month === -1) { + await ctx.reply( + "Это неправильный месяц, \ +используйте одну из кнопок!", + ); + return; + } + + ctx.session.month = month; + const diff = getDays(month, day); + await ctx.reply( + `Ваш день рождения ${months[month]} ${day}. +Это через ${diff} дней!`, + { reply_markup: { remove_keyboard: true } }, + ); + ctx.session.step = "idle"; +}); +month.use((ctx) => ctx.reply("Пожалуйста, нажмите одну из кнопок!")); + +// Определите шаг, на котором обрабатываются все остальные случаи. +router.otherwise(async (ctx) => { // idle + await ctx.reply( + "Отправьте /birthday чтобы понять, сколько вам осталось ждать.", + ); +}); + +bot.use(router); // используйте роутер +bot.start(); + +// Утилиты для преобразования даты +const months = [ + "Янв", + "Февр", + "Март", + "Апр", + "Май", + "Июнь", + "Июль", + "Авг", + "Сент", + "Окт", + "Нояб", + "Дек", +]; +function getDays(month, day) { + const bday = new Date(); + const now = Date.now(); + bday.setMonth(month); + bday.setDate(day); + if (bday.getTime() < now) bday.setFullYear(bday.getFullYear() + 1); + const diff = (bday.getTime() - now) / (1000 * 60 * 60 * 24); + return diff; +} +``` + +```ts [Deno] +import { + Bot, + Context, + Keyboard, + session, + SessionFlavor, +} from "https://deno.land/x/grammy/mod.ts"; +import { Router } from "https://deno.land/x/grammy_router/router.ts"; + +interface SessionData { + step: "idle" | "day" | "month"; // на каком этапе формы мы находимся + dayOfMonth?: number; // день, в котором родился пользователь + month?: number; // месяц, в котором родился пользователь +} +type MyContext = Context & SessionFlavor; + +const bot = new Bot(""); + +// Используйте сессии. +bot.use(session({ initial: (): SessionData => ({ step: "idle" }) })); + +// Определите некоторые команды. +bot.command("start", async (ctx) => { + await ctx.reply(`Добро пожаловать! +Я могу сказать, сколько дней осталось до твоего рождения! +Отправь /birthday чтобы начать`); +}); + +bot.command("birthday", async (ctx) => { + const day = ctx.session.dayOfMonth; + const month = ctx.session.month; + if (day !== undefined && month !== undefined) { + // Информация уже предоставлена! + await ctx.reply(`Ваш день рождения через ${getDays(month, day)} дней!`); + } else { + // Отсутствующая информация, войдите в форму на основе роутера + ctx.session.step = "day"; + await ctx.reply( + "Пожалуйста, отправьте мне день месяца \ +в который вы родились в виде числа!", + ); + } +}); + +// Используйте роутер +const router = new Router((ctx) => ctx.session.step); + +// Определите этап, на который будет обрабатывать день. +const day = router.route("day"); +day.on("message:text", async (ctx) => { + const day = parseInt(ctx.msg.text, 10); + if (isNaN(day) || day < 1 || 31 < day) { + await ctx.reply("Это не верный день, попробуйте снова!"); + return; + } + ctx.session.dayOfMonth = day; + // Форма для перехода к месяцу + ctx.session.step = "month"; + await ctx.reply("Получил, теперь назовите мне месяц!", { + reply_markup: { + one_time_keyboard: true, + keyboard: new Keyboard() + .text("Янв").text("Февр").text("Март").row() + .text("Апр").text("Май").text("Июнь").row() + .text("Июль").text("Авг").text("Сент").row() + .text("Окт").text("Нояб").text("Дек").build(), + }, + }); +}); +day.use((ctx) => + ctx.reply("Пожалуйста, пришлите мне день в виде текстового сообщения!") +); + +// Определите шаг, который обрабатывает месяц. +const month = router.route("month"); +month.on("message:text", async (ctx) => { + // Не должно происходить, если только данные сессии не повреждены. + const day = ctx.session.dayOfMonth; + if (day === undefined) { + await ctx.reply("Мне нужен день, когда вы родились!"); + ctx.session.step = "day"; + return; + } + + const month = months.indexOf(ctx.msg.text); + if (month === -1) { + await ctx.reply( + "Это неправильный месяц, \ +используйте одну из кнопок!", + ); + return; + } + + ctx.session.month = month; + const diff = getDays(month, day); + await ctx.reply( + `Ваш день рождения ${months[month]} ${day}. +Это через ${diff} дней!`, + { reply_markup: { remove_keyboard: true } }, + ); + ctx.session.step = "idle"; +}); +month.use((ctx) => ctx.reply("Пожалуйста, нажмите одну из кнопок!")); + +// Определите шаг, на котором обрабатываются все остальные случаи. +router.otherwise(async (ctx) => { // idle + await ctx.reply( + "Отправьте /birthday чтобы понять, сколько вам осталось ждать.", + ); +}); + +bot.use(router); // используйте роутер +bot.start(); + +// Утилиты для преобразования даты +const months = [ + "Янв", + "Февр", + "Март", + "Апр", + "Май", + "Июнь", + "Июль", + "Авг", + "Сент", + "Окт", + "Нояб", + "Дек", +]; +function getDays(month: number, day: number) { + const bday = new Date(); + const now = Date.now(); + bday.setMonth(month); + bday.setDate(day); + if (bday.getTime() < now) bday.setFullYear(bday.getFullYear() + 1); + const diff = (bday.getTime() - now) / (1000 * 60 * 60 * 24); + return diff; +} +``` + +::: + +Обратите внимание, что сессия имеет свойство `step`, которое хранит шаг формы, +т.е. какое значение заполняется в данный момент. Роутер используется для +перехода между различными middleware, которые заполняют поля `month` и +`dayOfMonth` в сессии. Если оба значения известны, бот вычисляет оставшиеся дни +и отправляет их обратно пользователю. + +## Краткая информация о плагине + +- Название: `router` +- [Исходник](https://github.com/grammyjs/router) +- [Ссылка](/ref/router/) diff --git a/site/docs/ru/plugins/runner.md b/site/docs/ru/plugins/runner.md new file mode 100644 index 000000000..8ac3f8720 --- /dev/null +++ b/site/docs/ru/plugins/runner.md @@ -0,0 +1,418 @@ +--- +prev: false +next: false +--- + +# Параллельность c grammY runner (`runner`) + +Этот пакет можно использовать, если вы запускаете бота [с использованием long polling](../guide/deployment-types), и хотите, чтобы сообщения обрабатывались параллельно. + +> Обязательно изучите [Масштабирование II](../advanced/scaling#long-polling), прежде чем использовать grammY runner. + +## Почему нам нужен runner + +Если вы размещаете своего бота на хостинге с long polling и хотите увеличить его масштабы, вам не обойтись без одновременной обработки обновлений, поскольку последовательная обработка обновлений слишком медленная. +В результате боты сталкиваются с рядом проблем. + +- Существуют ли условия гонки? +- Можем ли мы по-прежнему "ожидать" стек middleware? Это необходимо для обработки ошибок! +- Что, если middleware по какой-то причине не проходит дальше, блокирует ли это работу бота? +- Можем ли мы обрабатывать некоторые выбранные обновления последовательно? +- Можем ли мы ограничить нагрузку на сервер? +- Можем ли мы обрабатывать обновления на нескольких ядрах? + +Как видите, нам нужно решение, способное решить все вышеперечисленные проблемы, чтобы добиться правильного long polling бота. +Эта проблема совершенно отлична от создания middleware или отправки сообщений в Telegram. +Следовательно, она не решается в основном пакете grammY. +Вместо этого вы можете использовать [grammY runner](https://github.com/grammyjs/runner). +У него также есть своя [API документация](/ref/runner/). + +## Использование + +Вот простой пример. + +::: code-group + +```ts [TypeScript] +import { Bot } from "grammy"; +import { run } from "@grammyjs/runner"; + +// Создайте бота +const bot = new Bot(""); + +// Добавьте обычный middleware и бла-бла-бла +bot.on("message", (ctx) => ctx.reply("Получил твое сообщение.")); + +// Правильно запустите это! +run(bot); +``` + +```js [JavaScript] +const { Bot } = require("grammy"); +const { run } = require("@grammyjs/runner"); + +// Создайте бота +const bot = new Bot(""); + +// Добавьте обычный middleware и бла-бла-бла +bot.on("message", (ctx) => ctx.reply("Получил твое сообщение.")); + +// Правильно запустите это! +run(bot); +``` + +```ts [Deno] +import { Bot } from "https://deno.land/x/grammy/mod.ts"; +import { run } from "https://deno.land/x/grammy_runner/mod.ts"; + +// Создайте бота +const bot = new Bot(""); + +// Добавьте обычный middleware и бла-бла-бла +bot.on("message", (ctx) => ctx.reply("Получил твое сообщение.")); + +// Правильно запустите это! +run(bot); +``` + +::: + +## Последовательная обработка при необходимости + +Скорее всего, вам нужна гарантия того, что сообщения из одного и того же чата будут обрабатываться по порядку. +Это полезно при установке [middleware сессии](./session), а также для того, чтобы ваш бот не перепутал порядок сообщений в одном и том же чате. + +grammY runner экспортирует middleware `sequentialize`, который заботится об этом. +Вы можете посмотреть этот [раздел](../advanced/scaling#параллельность-это-сложно), чтобы узнать, как его использовать. + +Теперь мы рассмотрим более продвинутые возможности использования плагина. + +Функцию ограничитель можно использовать не только для указания идентификатора чата или пользователя. +Вместо этого вы можете возвращать _список строк идентификаторов ограничений_, которые определяют для каждого обновления в отдельности, каких еще вычислений оно должно дождаться, прежде чем начнется обработка. + +Например, можно вернуть идентификатор чата и идентификатор пользователя автора сообщения. + +```ts +bot.use(sequentialize((ctx) => { + const chat = ctx.chat?.id.toString(); + const user = ctx.from?.id.toString(); + return [chat, user].filter((con) => con !== undefined); +})); +``` + +Это гарантирует, что сообщения в одном и том же чате будут упорядочены правильно. +Кроме того, если Алиса отправляет сообщение в группе, а затем посылает сообщение вашему боту в личном чате, то эти два сообщения будут упорядочены правильно. + +В некотором смысле, вы можете задать граф зависимостей между обновлениями. +grammY runner будет решать все необходимые ограничения на лету и блокировать обновления столько, сколько необходимо для обеспечения правильного упорядочивания сообщений. + +Реализация этого очень эффективна. +Ей требуется постоянная память (если вы не зададите бесконечную параллельность), и (амортизированное) постоянное время обработки одного обновления. + +## Правильно выключение + +Для того чтобы бот корректно завершил свою работу, вы [должны подать ему сигнал](../advanced/reliability#использование-grammy-runner) на остановку, когда процесс будет уничтожен. + +Заметьте, что вы можете дождаться завершения работы runner, `ожидая` задачи в [`RunnerHandle`](/ref/runner/runnerhandle), возвращаемой из `run`. + +```ts +const handle = run(bot); + +// Эта функция будет вызвана, когда бот остановится. +handle.task().then(() => { + console.log("Бот закончил обработку!"); +}); + +// Позже остановите бота с помощью обработчика runner. +await handle.stop(); +``` + +## Расширенные настройки + +grammY runner состоит из трех частей: источника, поглотителя и runner'а. +Источник получает обновления, поглотитель потребляет обновления, а runner настраивает и соединяет их. + +> Подробное описание внутренней работы runner'а можно найти [здесь](#как-это-работает-под-капотом). + +Каждая из этих трех частей может быть настроена с помощью различных параметров. +Это может уменьшить сетевой трафик, позволить вам указать разрешенные обновления и многое другое. + +Каждая часть runner'а получает свои настройки через специальный объект options. + +```ts +run(bot, { + source: {}, + runner: {}, + sink: {}, +}); +``` + +Вы должны посмотреть `RunOptions` в [документации API](/ref/runner/runoptions), чтобы узнать, какие параметры доступны. + +Например, вы узнаете, что `allowed_updates` могут быть включены с помощью следующего фрагмента кода. + +```ts +run(bot, { runner: { fetch: { allowed_updates: [] } } }); +``` + +## Многопоточность + +> Нет смысла в многопоточности, если ваш бот не обрабатывает хотя бы 50 миллионов обновлений в день (>500 в секунду). +> [Пропустите этот раздел](#как-это-работает-под-капотом), если ваш бот обрабатывает меньше трафика, чем это. + +JavaScript является однопоточным. +Это удивительно, потому что [параллелизм --- это сложно](../advanced/scaling#параллельность-это-сложно), а значит, если есть только один поток, то много головной боли, естественно, снимается. + +Однако если нагрузка на бота очень высока (речь идет о 1000 обновлений в секунду и выше), то одного ядра может оказаться недостаточно. +В принципе, одно ядро начнет справляться с обработкой JSON всех сообщений, которые должен обработать ваш бот. + +### Workers бота для обработки обновлений + +Есть простой выход: workers бота! +grammY runner позволяет вам создать несколько worker'ов, которые могут обрабатывать ваши обновления параллельно на фактически разных ядрах (используя разные циклы событий и отдельную память). + +На Node.js grammY runner использует [Worker Threads](https://nodejs.org/api/worker_threads.html). +На Deno grammY runner использует [Web Workers](https://docs.deno.com/runtime/reference/web_platform_apis). + +Концептуально, grammY runner предоставляет вам класс `BotWorker`, который может обрабатывать обновления. +Он равносилен обычному классу `Bot` (фактически, он даже расширяет `Bot`). +Основное различие между `BotWorker` и `Bot` заключается в том, что `BotWorker` не может получать обновления. +Вместо этого он должен получать их от обычного `Bot`, который управляет своими worker'ами. + +```asciiart:no-line-numbers +1. получение обновлений Bot + __// \\__ + __/ / \ \__ +2. отправка worker'ам __/ / \ \__ + __/ / \ \__ + / / \ \ +3. обработка обновлений BotWorker BotWorker BotWorker BotWorker +``` + +grammY runner предоставляет вам middleware, который может отправлять обновления worker'ам бота. +Бот workers могут получать эти обновления и обрабатывать их. +Таким образом, центральный бот должен заниматься только получением и распределением обновлений между worker'ами, которыми он управляет. +Фактическая обработка обновлений (фильтрация сообщений, отправка ответов и т.д.) выполняется worker'ами бота. + +Давайте посмотрим, как это можно использовать. + +### Использование бот workers + +> Примеры этого можно найти в репозитории [grammY runner](https://github.com/grammyjs/runner/tree/main/examples). + +Мы начнем с создания центрального экземпляра бота, который будет получать обновления и распределять их между worker'ами. +Начнем с создания файла `bot.ts` со следующим содержанием. + +::: code-group + +```ts [TypeScript] +// bot.ts +import { Bot } from "grammy"; +import { distribute, run } from "@grammyjs/runner"; + +// Создайте бота. +const bot = new Bot(""); // <-- поместите токен бота между "" + +// По желанию, здесь можно выполнить последовательную обработку обновлений. +// bot.use(sequentialize(...)) + +// Распределите обновления между worker'ами бота +bot.use(distribute(__dirname + "/worker")); + +// Запускайте бота с многопоточностью. +run(bot); +``` + +```js [JavaScript] +// bot.js +const { Bot } = require("grammy"); +const { distribute, run } = require("@grammyjs/runner"); + +// Создайте бота. +const bot = new Bot(""); // <-- поместите токен бота между "" + +// По желанию, здесь можно выполнить последовательную обработку обновлений. +// bot.use(sequentialize(...)) + +// Распределите обновления между worker'ами бота +bot.use(distribute(__dirname + "/worker")); + +// Запускайте бота с многопоточностью. +run(bot); +``` + +```ts [Deno] +// bot.ts +import { Bot } from "https://deno.land/x/grammy/mod.ts"; +import { distribute, run } from "https://deno.land/x/grammy_runner/mod.ts"; + +// Создайте бота. +const bot = new Bot(""); // <-- поместите токен бота между "" + +// По желанию, здесь можно выполнить последовательную обработку обновлений. +// bot.use(sequentialize(...)) + +// Распределите обновления между worker'ами бота +bot.use(distribute(new URL("./worker.ts", import.meta.url))); + +// Запускайте бота с многопоточностью. +run(bot); +``` + +::: + +Рядом с `bot.ts` мы создаем второй файл под названием `worker.ts` (как указано в строке 12 в коде выше). +Он будет содержать фактическую логику бота. + +::: code-group + +```ts [TypeScript] +// worker.ts +import { BotWorker } from "@grammyjs/runner"; + +// Создайте нового BotWorker +const bot = new BotWorker(""); // <-- Снова поместите токен вашего бота между "" + +// Добавьте логику обработки +bot.on("message", (ctx) => ctx.reply("Ура!")); +``` + +```js [JavaScript] +// worker.js +const { BotWorker } = require("@grammyjs/runner"); + +// Создайте нового BotWorker +const bot = new BotWorker(""); // <-- Снова поместите токен вашего бота между "" + +// Добавьте логику обработки +bot.on("message", (ctx) => ctx.reply("Ура!")); +``` + +```ts [Deno] +// worker.ts +import { BotWorker } from "https://deno.land/x/grammy_runner/mod.ts"; + +// Создайте нового BotWorker +const bot = new BotWorker(""); // <-- Снова поместите токен вашего бота между "" + +// Добавьте логику обработки +bot.on("message", (ctx) => ctx.reply("Ура!")); +``` + +::: + +> Обратите внимание, что каждый worker может отправлять сообщения обратно в Telegram. +> Поэтому вы должны передать токен бота каждому worker'у. + +Вам не нужно запускать рабочих ботов или экспортировать что-либо из файла. +Достаточно создать экземпляр `BotWorker`. +Он будет автоматически слушать обновления. + +Важно понимать, что **только необработанные обновления** отправляются worker'ами бота. +Другими словами, объекты [контекста](../guide/context) создаются дважды для каждого обновления: один раз в `bot.ts`, чтобы оно могло быть передано worker'у, и один раз в `worker.ts`, чтобы оно могло быть обработано. +Более того: свойства, которые устанавливаются для объекта контекста в `bot.ts`, не отправляются worker'ам. +Это означает, что все плагины должны быть установлены в worker'ах бота. + +::: tip Распространяйте только некоторые обновления +В качестве оптимизации производительности вы можете отбрасывать обновления, которые не хотите обрабатывать. +Таким образом, вашему боту не придется отправлять обновление на worker'у, чтобы оно было проигнорировано. + +::: code-group + +```ts [Node.js] +// Наш бот обрабатывает только сообщения, изменения и callback запросы, +// поэтому мы можем игнорировать все остальные обновления и не распространять их. +bot.on( + ["message", "edited_message", "callback_query"], + distribute(__dirname + "/worker"), +); +``` + +```ts [Deno] +// Наш бот обрабатывает только сообщения, изменения и callback запросы, +// поэтому мы можем игнорировать все остальные обновления и не распространять их. +bot.on( + ["message", "edited_message", "callback_query"], + distribute(new URL("./worker.ts", import.meta.url)), +); +``` + +::: + +По умолчанию `distribute` создает 4 worker'а бота. +Вы можете легко изменить это число. + +```ts +// Распространите между 8 worker'ами бота +bot.use(distribute(workerFile, { count: 8 })); +``` + +Обратите внимание, что ваше приложение никогда не должно порождать больше потоков, чем имеется физических ядер на вашем процессоре. +Это не улучшит производительность, а скорее ухудшит ее. + +## Как это работает под капотом + +Конечно, хотя использование grammY runner выглядит очень просто, под капотом происходит очень многое. + +Каждый runner состоит из трех различных частей. + +1. **Источник** получает обновления из Telegram. +2. **Поглотитель** поставляет обновления экземпляру бота. +3. Компонент **runner** соединяет источник и поглотитель и позволяет запускать и останавливать бота. + +```asciiart:no-line-numbers +api.telegram.org <—> источник <—> runner <—> поглотитель <—> бот +``` + +### Исходник + +grammY runner поставляется с одним источником по умолчанию, который может работать с любым `UpdateSupplier` ([ссылка на API](/ref/runner/updatesupplier)). +Такой поставщик обновлений легко создать из экземпляра бота. +Если вы хотите создать его самостоятельно, обязательно ознакомьтесь с `createUpdateFetcher` ([ссылка на API](/ref/runner/createupdatefetcher)). + +Источник представляет собой асинхронный итератор обновлений пакетов, но он может быть активным или неактивным, и вы можете `закрыть` его, чтобы отключиться от серверов Telegram. + +### Поглотитель + +grammY runner поставляется с тремя возможными реализациями поглотителя: последовательной (то же поведение, что и у `bot.start()`), пакетной (в основном полезной для обратной совместимости с другими фреймворками) и полностью параллельной (используемой `run`). +Все они работают с объектами `UpdateConsumer` ([ссылка на API](/ref/runner/updateconsumer)), которые легко создать из экземпляра бота. +Если вы хотите создать такой объект самостоятельно, обязательно проверьте функцию `handleUpdate` на экземпляре `Bot` в grammY ([ссылка на API](/ref/core/bot#handleupdate)). + +В поглотителе содержится очередь ([ссылка на API](/ref/runner/decayingdeque)) отдельных обновлений, которые в данный момент обрабатываются. +Добавление новых обновлений в очередь немедленно заставит `UpdateConsumer` обработать их и вернет `Promise`, который будет решён, как только в очереди снова появится свободное место. +Разрешенное число определяет свободное место. +Таким образом, установка ограничения параллельности для grammY runner выполняется через базовый экземпляр очереди. + +Очередь также отбрасывает обновления, обработка которых занимает слишком много времени, и вы можете указать `timeoutHandler` при создании соответствующего поглотителя. +Разумеется, при создании поглотителя вы также должны указать обработчик ошибок. + +Если вы используете `run(bot)`, будет использован обработчик ошибок из `bot.catch`. + +### Runner + +Runner --- это обычный цикл, который получает обновления из источника и передает их в поглотитель. +Как только в поглотителе снова появится свободное место, runner получит следующую порцию обновлений из источника. + +Когда вы создаете runner с помощью `createRunner` ([ссылка на API](/ref/runner/createrunner)), вы получаете обработчик, который можно использовать для управления runner'ом. +Например, с его помощью можно запускать и останавливать его или получать `Promise`, который разрешится, если runner остановится. +(Этот обработчик также возвращается функцией `run`). +Посмотрите [документацию API](/ref/runner/runnerhandle) на `RunnerHandle`. + +### Функция `run` + +Функция `run` делает несколько вещей, которые помогут вам легко использовать описанную выше структуру. + +1. Она создает поставщика обновлений для вашего бота. +2. Создает [исходник](#исходник) из поставщика обновлений. +3. Создается `UpdateConsumer` от вашего бота. +4. Она создает [поглотитель](#поглотитель) от `UpdateConsumer`. +5. Создает [runner](#runner) из источника и поглотителя. +6. Запускает runner. + +Возвращается обработчик созданного runner'а, который позволяет управлять runner'ом. + +## Краткая информация о плагине + +- Название: `runner` +- [Исходник](https://github.com/grammyjs/runner) +- [Ссылка](/ref/runner/) diff --git a/site/docs/ru/plugins/session.md b/site/docs/ru/plugins/session.md new file mode 100644 index 000000000..e76b1b716 --- /dev/null +++ b/site/docs/ru/plugins/session.md @@ -0,0 +1,1032 @@ +--- +prev: false +next: false +--- + +# Сессии и хранение данных (встроенно) + +Хотя вы всегда можете просто написать свой собственный код для подключения к +хранилищу данных по вашему выбору, grammY поддерживает очень удобный паттерн +хранения данных, называемый _сессиями_. + +> [Перейдите вниз](#как-использовать-сессии), если вы знаете, как работают +> сессии. + +## Почему мы должны думать о хранении? + +В отличие от обычных пользовательских аккаунтов в Telegram, боты имеют +[ограниченное облачное хранилище](https://core.telegram.org/bots#how-are-bots-different-from-users). +В результате есть несколько вещей, которые вы не можете делать с помощью ботов: + +1. Вы не можете получить доступ к старым сообщениям, которые получил ваш бот. +2. Вы не можете получить доступ к старым сообщениям, которые ваш бот отправил. +3. Вы не можете получить список всех чатов с вашим ботом. +4. Другие проблемы, например, нет обзора медиа и т.д. + +По сути, все сводится к тому, что **бот имеет доступ только к информации +текущего входящего обновления** (например, сообщения), т.е. к той информации, +которая доступна в объекте контекста `ctx`. + +Следовательно, если вы хотите получить доступ к старым данным, вы должны хранить +их сразу же после поступления. Это означает, что у вас должно быть хранилище +данных, например, файл, база данных или хранилище в памяти. + +Конечно, grammY позаботился об этом: вам не нужно размещать это самостоятельно. +Вы можете просто использовать сессионное хранилище grammY, которое не нуждается +в настройке и остается бесплатным навсегда. + +> Естественно, существует множество других сервисов, предлагающих хранение +> данных как услугу, и grammY также легко интегрируется с ними. Если вы хотите +> запустить собственную базу данных, будьте уверены, что grammY поддерживает и +> это. [Прокрутите вниз](#известные-адаптеры-хранения), чтобы узнать, какие +> интеграции доступны в настоящее время. + +## Что такое сессии? + +Очень часто боты хранят некоторые данные в чате. Например, допустим, мы хотим +создать бота, который будет подсчитывать количество раз, когда в тексте +сообщения появляется эмодзи пиццы :pizza:. Этого бота можно добавить в группу, и +он сможет рассказать вам, насколько вы и ваши друзья любите пиццу. + +Когда наш пицца-бот получает сообщение, он должен вспомнить, сколько раз он +видел :pizza: в этом чате раньше. Количество пицц, конечно, не должно +измениться, когда ваша сестра добавит пицца-бота в свой групповой чат, так что +на самом деле мы хотим хранить _один счетчик на каждый чат_. + +Сессии --- это элегантный способ хранения данных _в каждом чате_. В качестве ключа +в базе данных используется идентификатор чата, а в качестве значения - счетчик. +В данном случае мы будем называть идентификатор чата _ключом сессии_. (Подробнее +о ключах сессий вы можете прочитать [здесь](#ключи-сессии)). По сути, ваш бот +будет хранить карту от идентификатора чата к некоторым пользовательским данным +сессии, т.е. что-то вроде этого: + +```json +{ + "424242": { "pizzaCount": 24 }, + "987654": { "pizzaCount": 1729 } +} +``` + +> Когда мы говорим "база данных", мы на самом деле имеем в виду любое решение +> для хранения данных. Это и файлы, и облачные хранилища, и все остальное. + +Хорошо, но что такое сессии сейчас? + +Мы можем установить на бота middleware, который будет предоставлять данные о +сессии чата в `ctx.session` при каждом обновлении. Установленный плагин будет +делать что-то до и после вызова наших обработчиков: + +1. **Перед нашим middleware.** Плагин сессии загружает данные сессии для + текущего чата из базы данных. Он сохраняет данные в объекте контекста под + именем `ctx.session`. +2. **Во время работы нашего middleware.** Мы можем _читать_ `ctx.session`, чтобы + узнать, какое значение было в базе данных. Например, если в чат отправлено + сообщение с идентификатором `424242`, то оно будет + `ctx.session = { pizzaCount: 24 }` во время работы нашего middleware (по + крайней мере, с приведенным выше примером состояния базы данных). Мы также + можем _модифицировать_ `ctx.session` произвольным образом, так что мы можем + добавлять, удалять и изменять поля по своему усмотрению. +3. **После нашего middleware.** Middleware сессии следит за тем, чтобы данные + были записаны обратно в базу данных. Каким бы ни было значение `ctx.session` + после завершения работы middleware, оно будет сохранено в базе данных. + +В результате нам больше не нужно беспокоиться о взаимодействии с хранилищем +данных. Мы просто изменяем данные в `ctx.session`, а плагин позаботится обо всем +остальном. + +## Когда использовать сессии? + +> [Пропустите](#как-использовать-сессии), если вы уже знаете, что хотите +> использовать сессии. + +Вы думаете: "Это здорово, мне больше не придется беспокоиться о базах данных!" И +вы будете правы, сессии --- это идеальное решение, но только для некоторых типов +данных. + +По нашему опыту, есть случаи, когда сессии действительно великолепны. С другой +стороны, есть случаи, когда традиционная база данных может быть более +подходящей. + +Это сравнение может помочь вам решить, стоит ли использовать сессии или нет. + +| | Сессии | База данных | +| ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------- | +| _Доступ_ | одно изолированное хранилище **на чат**. | Доступ к одним и тем же данным из **разных чатов**. | +| _Совместный доступ_ | данные **используются только ботом**. | данные **используются другими системами** (например, подключенным веб-сервером) | +| _Формат_ | любые объекты JavaScript: строки, числа, массивы и так далее | любые данные (бинарные, файлы, структурированные и т.д.) | +| _Размер одного чата_ | оптимально менее ~3 МБ на чат | любой размер | +| _Эксклюзивная функция_ | Требуется некоторым плагинам grammY. | Поддерживает транзакции базы данных. | + +Это не означает, что вещи _не могут работать_, если вы выбираете сессии/базы +данных вместо других. Например, вы, конечно, можете хранить большие бинарные +данные в сессии. Однако ваш бот будет работать не так хорошо, как мог бы, +поэтому мы рекомендуем использовать сессии только там, где это имеет смысл. + +## Как использовать сессии? + +Вы можете добавить поддержку сессий в grammY, используя middleware для сессий. + +### Пример использования + +Вот пример бота, который подсчитывает сообщения, содержащие эмодзи пиццы +:pizza:: + +::: code-group + +```ts [TypeScript] +import { Bot, Context, session, SessionFlavor } from "grammy"; + +// Определите форму нашей сессии. +interface SessionData { + pizzaCount: number; +} + +// Расширьте тип контекста, чтобы включить в него сессии. +type MyContext = Context & SessionFlavor; + +const bot = new Bot(""); + +// Установите middleware для сессии и определите начальное значение. +function initial(): SessionData { + return { pizzaCount: 0 }; +} +bot.use(session({ initial })); + +bot.command("hunger", async (ctx) => { + const count = ctx.session.pizzaCount; + await ctx.reply(`Ваш уровень голода ${count}!`); +}); + +bot.hears(/.*🍕.*/, (ctx) => ctx.session.pizzaCount++); + +bot.start(); +``` + +```js [JavaScript] +const { Bot, session } = require("grammy"); + +const bot = new Bot(""); + +// Установите middleware для сессии и определите начальное значение. +function initial() { + return { pizzaCount: 0 }; +} +bot.use(session({ initial })); + +bot.command("hunger", async (ctx) => { + const count = ctx.session.pizzaCount; + await ctx.reply(`Ваш уровень голода ${count}!`); +}); + +bot.hears(/.*🍕.*/, (ctx) => ctx.session.pizzaCount++); + +bot.start(); +``` + +```ts [Deno] +import { + Bot, + Context, + session, + SessionFlavor, +} from "https://deno.land/x/grammy/mod.ts"; + +// Определите форму нашей сессии. +interface SessionData { + pizzaCount: number; +} + +// Расширьте тип контекста, чтобы включить в него сессии. +type MyContext = Context & SessionFlavor; + +const bot = new Bot(""); + +// Установите middleware для сессии и определите начальное значение. +function initial(): SessionData { + return { pizzaCount: 0 }; +} +bot.use(session({ initial })); + +bot.command("hunger", async (ctx) => { + const count = ctx.session.pizzaCount; + await ctx.reply(`Ваш уровень голода ${count}!`); +}); + +bot.hears(/.*🍕.*/, (ctx) => ctx.session.pizzaCount++); + +bot.start(); +``` + +::: + +Обратите внимание, что нам также нужно +[настроить тип контекста](../guide/context#кастомизация-объекта-контекста), +чтобы сделать сессию доступной на нем. Расширитель контекста называется +`SessionFlavor`. + +### Первоначальные данные сессии + +Когда пользователь впервые обращается к вашему боту, у него нет данных о сессии. +Поэтому важно указать параметр `initial` для middleware сессии. Передайте +функцию, которая генерирует новый объект с начальными данными сессии для новых +чатов. + +```ts +// Создает новый объект, который будет использоваться в качестве начальных данных сессии. +function createInitialSessionData() { + return { + pizzaCount: 0, + // другие данные здесь + }; +} +bot.use(session({ initial: createInitialSessionData })); +``` + +Тоже самое, но короче: + +```ts +bot.use(session({ initial: () => ({ pizzaCount: 0 }) })); +``` + +::: warning Совместное использование объектов Убедитесь, что всегда создаете +_новый объект_. Не делайте этого **НЕ**: + +```ts +// ОПАСНОСТЬ, ПЛОХО, НЕПРАВИЛЬНО, СТОП +const initialData = { pizzaCount: 0 }; // НЕТ +bot.use(session({ initial: () => initialData })); // ЗЛО +``` + +Если это сделать, то несколько чатов могут совместно использовать один и тот же +объект сессии в памяти. Таким образом, изменение данных сессии в одном чате +может случайно повлиять на данные сессии в другом чате. +::: + +Вы также можете полностью опустить опцию `initial`, хотя вам советуют этого не +делать. Если вы не укажете его, чтение `ctx.session` будет вызывать ошибку у +новых пользователей. + +### Ключи сессии + +> В этом разделе описывается расширенная функция, о которой большинству людей не +> нужно беспокоиться. Возможно, вы захотите продолжить в разделе о +> [хранении ваших данных](#хранение-ваших-данных). + +Вы можете указать, какой ключ сессии использовать, передав функцию +`getSessionKey` в [настройки](/ref/core/sessionoptions#getsessionkey). Таким +образом, вы можете кардинально изменить принцип работы плагина сессий. По +умолчанию данные хранятся в каждом чате. Использование `getSessionKey` позволяет +хранить данные для каждого пользователя, или для комбинации пользователь-чат, +или как вам угодно. Вот три примера: + +::: code-group + +```ts [TypeScript] +// Сохраняет данные в каждом чате (по умолчанию). +function getSessionKey(ctx: Context): string | undefined { + // Пусть все пользователи в групповом чате используют одну и ту же сессию, + // но в личных чатах каждому пользователю предоставляется отдельная приватная сессия. + return ctx.chat?.id.toString(); +} + +// Хранит данные для каждого пользователя. +function getSessionKey(ctx: Context): string | undefined { + // Дайте каждому пользователю его личное хранилище сессий + // (будет распространяться по группам и в личном чате) + return ctx.from?.id.toString(); +} + +// Хранит данные по каждой комбинации пользователь-чат. +function getSessionKey(ctx: Context): string | undefined { + // Предоставьте каждому пользователю одно личное хранилище сессий для общения с ботом + // (независимая сессия для каждой группы и их приватного чата) + return ctx.from === undefined || ctx.chat === undefined + ? undefined + : `${ctx.from.id}/${ctx.chat.id}`; +} + +bot.use(session({ getSessionKey })); +``` + +```js [JavaScript] +// Сохраняет данные в каждом чате (по умолчанию). +function getSessionKey(ctx) { + // Пусть все пользователи в групповом чате используют одну и ту же сессию, + // но в личных чатах каждому пользователю предоставляется отдельная приватная сессия. + return ctx.chat?.id.toString(); +} + +// Хранит данные для каждого пользователя. +function getSessionKey(ctx) { + // Дайте каждому пользователю его личное хранилище сессий + // (будет распространяться по группам и в личном чате) + return ctx.from?.id.toString(); +} + +// Хранит данные по каждой комбинации пользователь-чат. +function getSessionKey(ctx) { + // Предоставьте каждому пользователю одно личное хранилище сессий для общения с ботом + // (независимая сессия для каждой группы и их приватного чата) + return ctx.from === undefined || ctx.chat === undefined + ? undefined + : `${ctx.from.id}/${ctx.chat.id}`; +} + +bot.use(session({ getSessionKey })); +``` + +::: + +Если `getSessionKey` возвращает `undefined`, то `ctx.session` будет `undefined`. +Например, стандартный преобразователь сеансовых ключей не будет работать для +обновлений `poll`/`poll_answer` или обновлений `inline_query`, потому что они не +принадлежат чату (`ctx.chat` является `undefined`). + +::: warning Ключи сеансов и вебхуки Если вы запускаете бота на вебхуках, вам +следует избегать использования опции `getSessionKey`. Telegram отправляет +вебхуки последовательно в каждый чат, поэтому стандартный преобразователь +сеансовых ключей --- единственная реализация, которая гарантированно не приведет +к потере данных. + +Если вы должны использовать эту опцию (что, конечно, все еще возможно), вы +должны знать, что вы делаете. Убедитесь, что вы понимаете последствия такой +конфигурации, прочитав статью [здесь](../guide/deployment-types) и особенно +[здесь](./runner#последовательная-обработка-при-необходимости). +::: + +### Миграции чата + +Если вы используете сессии для групп, вам следует знать, что при определенных +обстоятельствах Telegram переносит обычные группы в супергруппы (например, +[здесь](https://github.com/telegramdesktop/tdesktop/issues/5593)). + +Эта миграция происходит только один раз для каждой группы, но она может привести +к несоответствиям. Это происходит потому, что перенесенный чат технически +является совершенно другим чатом, имеющим другой идентификатор, и, +следовательно, его сессия будет идентифицироваться по-другому. + +В настоящее время не существует безопасного решения этой проблемы, поскольку +сообщения из двух чатов также идентифицируются по-разному. Это может привести к +скачкам данных. Однако существует несколько способов решения этой проблемы: + +- Игнорирование проблемы. При переносе группы, данные сеанса бота фактически + обнуляются. Простое, надежное, стандартное поведение, но потенциально + неожиданное один раз в чате. Например, если миграция произойдет, когда + пользователь находится в беседе, управляемой плагином + [conversations](./conversations), беседа будет сброшена. + +- Храните в сессии только временные данные (или данные с таймаутом), а для + важных вещей, которые необходимо перенести при миграции чата, используйте базу + данных. Затем можно использовать транзакции и пользовательскую логику для + обработки одновременного доступа к данным из старого и нового чата. Это + требует больших усилий и требует затрат на производительность, но это + единственный по-настоящему надежный способ решить эту проблему. + +- Теоретически возможно реализовать обходной путь, который будет соответствовать + обоим чатам **без гарантии надежности**. Telegram Bot API отправляет + обновление миграции для каждого из двух чатов, как только миграция была + запущена (см. свойства `migrate_to_chat_id` или `migrate_from_chat_id` в + [документации Telegram API](https://core.telegram.org/bots/api#message)). + Проблема в том, что нет никакой гарантии, что эти сообщения будут отправлены + до появления нового сообщения в супергруппе. Следовательно, бот может получить + сообщение из новой супергруппы до того, как узнает о переходе, и, таким + образом, не сможет сопоставить два чата, что приведет к вышеупомянутым + проблемам. + +- Другим обходным решением было бы ограничить бота только для супергрупп с + помощью [фильтров](../guide/filter-queries) (или ограничить только функции, + связанные с сессиями, для супергрупп). Однако это перекладывает + проблему/неудобство на пользователей. + +- Предоставление пользователям возможности принимать решение в явном виде. + ("Этот чат был перенесен, хотите ли вы перенести данные бота?"). Гораздо + надежнее и прозрачнее автоматических миграций за счет искусственно добавленной + задержки, но хуже пользовательский опыт. + +И наконец, разработчик сам решает, как поступить в этом случае. В зависимости от +функциональности бота можно выбрать тот или иной способ. Если данные +недолговечны (например, временные, с таймаутами), миграция не представляет +особой проблемы. Пользователь воспримет миграцию как заминку (если время +неудачно выбрано) и просто запустит функцию заново. + +Игнорировать проблему, конечно, проще всего, но все же важно знать о таком +поведении. В противном случае это может привести к путанице и стоить часов +времени на отладку. + +### Хранение ваших данных + +Во всех приведенных выше примерах данные сессии хранятся в оперативной памяти, +поэтому при остановке бота все данные будут потеряны. Это удобно, когда вы +разрабатываете бота или запускаете автоматические тесты (не нужно настраивать +базу данных), однако **это, скорее всего, нежелательно в production**. В +production билде вы захотите сохранить данные, например, в файле, базе данных +или другом хранилище. + +Вам следует использовать опцию `storage` в middleware сессии, чтобы подключить +его к вашему хранилищу данных. Возможно, для grammY уже написан адаптер +хранения, который вы можете использовать (см. +[ниже](#известные-адаптеры-хранения)), но если это не так, то обычно требуется +всего 5 строк кода, чтобы реализовать его самостоятельно. + +## Известные адаптеры хранения + +По умолчанию сессии будут храниться +[в вашей памяти](#оперативная-память-по-умолчанию) с помощью встроенного +адаптера хранения. Вы также можете использовать постоянные сессии, которые +grammY [предлагает бесплатно](#бесплатное-хранилище), или подключаться к +[внешним хранилищам](#внешние-решения-для-хранения-данных). + +Вот как можно установить один из адаптеров хранения данных снизу. + +```ts +const storageAdapter = ... // зависит от настроек + +bot.use(session({ + initial: ... + storage: storageAdapter, +})); +``` + +### Оперативная память (по умолчанию) + +По умолчанию все данные будут храниться в оперативной памяти. Это означает, что +все сессии будут потеряны, как только ваш бот остановится. + +Вы можете использовать класс `MemorySessionStorage` +([документация API](/ref/core/memorysessionstorage)) из пакета ядра grammY, если +хотите настроить дополнительные параметры хранения данных в оперативной памяти. + +```ts +bot.use(session({ + initial: ... + storage: new MemorySessionStorage() // также значение по умолчанию +})); +``` + +### Бесплатное хранилище + +> Бесплатное хранилище предназначено для использования в хобби проектах. +> Приложениям производственного масштаба следует размещать собственную базу +> данных. Список поддерживаемых интеграций внешних решений для хранения данных +> находится [внизу](#внешние-решения-для-хранения-данных). + +Преимущество использования grammY заключается в том, что вы получаете доступ к +бесплатному облачному хранилищу. Оно не требует настройки - вся аутентификация +осуществляется с помощью токена вашего бота. Загляните в +[репозиторий](https://github.com/grammyjs/storages/tree/main/packages/free)! + +Он очень прост в использовании: + +::: code-group + +```ts [TypeScript] +import { freeStorage } from "@grammyjs/storage-free"; + +bot.use(session({ + initial: ... + storage: freeStorage(bot.token), +})); +``` + +```js [JavaScript] +const { freeStorage } = require("@grammyjs/storage-free"); + +bot.use(session({ + initial: ... + storage: freeStorage(bot.token), +})); +``` + +```ts [Deno] +import { freeStorage } from "https://deno.land/x/grammy_storages/free/src/mod.ts"; + +bot.use(session({ + initial: ... + storage: freeStorage(bot.token), +})); +``` + +::: + +Готово! Теперь ваш бот будет использовать постоянное хранилище данных. + +Здесь приведен полный пример бота, который вы можете скопировать, чтобы +опробовать его. + +::: code-group + +```ts [TypeScript] +import { Bot, Context, session, SessionFlavor } from "grammy"; +import { freeStorage } from "@grammyjs/storage-free"; + +// Определите структуру сессии. +interface SessionData { + count: number; +} +type MyContext = Context & SessionFlavor; + +// Создайте бота и зарегистрируйте middleware сессии. +const bot = new Bot(""); + +bot.use( + session({ + initial: () => ({ count: 0 }), + storage: freeStorage(bot.token), + }), +); + +// Используйте постоянные данные сессии в обработчиках обновлений. +bot.on("message", async (ctx) => { + ctx.session.count++; + await ctx.reply(`Количество сообщений: ${ctx.session.count}`); +}); + +bot.catch((err) => console.error(err)); +bot.start(); +``` + +```js [JavaScript] +const { Bot, session } = require("grammy"); +const { freeStorage } = require("@grammyjs/storage-free"); + +// Создайте бота и зарегистрируйте middleware сессии. +const bot = new Bot(""); + +bot.use( + session({ + initial: () => ({ count: 0 }), + storage: freeStorage(bot.token), + }), +); + +// Используйте постоянные данные сессии в обработчиках обновлений. +bot.on("message", async (ctx) => { + ctx.session.count++; + await ctx.reply(`Количество сообщений: ${ctx.session.count}`); +}); + +bot.catch((err) => console.error(err)); +bot.start(); +``` + +```ts [Deno] +import { + Bot, + Context, + session, + SessionFlavor, +} from "https://deno.land/x/grammy/mod.ts"; +import { freeStorage } from "https://deno.land/x/grammy_storages/free/src/mod.ts"; + +// Определите структуру сессии. +interface SessionData { + count: number; +} +type MyContext = Context & SessionFlavor; + +// Создайте бота и зарегистрируйте middleware сессии. +const bot = new Bot(""); + +bot.use( + session({ + initial: () => ({ count: 0 }), + storage: freeStorage(bot.token), + }), +); + +// Используйте постоянные данные сессии в обработчиках обновлений. +bot.on("message", async (ctx) => { + ctx.session.count++; + await ctx.reply(`Количество сообщений: ${ctx.session.count}`); +}); + +bot.catch((err) => console.error(err)); +bot.start(); +``` + +::: + +### Внешние решения для хранения данных + +Мы поддерживаем коллекцию официальных адаптеров для хранения данных, которые +позволяют хранить данные о сеансах в различных местах. Каждый из них потребует +от вас регистрации у хостинг-провайдера или размещения собственного решения для +хранения данных. + +Посетите +[это место](https://github.com/grammyjs/storages/tree/main/packages#grammy-storages), +чтобы посмотреть список поддерживаемых в настоящее время адаптеров и получить +рекомендации по их использованию. + +::: tip Ваше хранилище не поддерживается? Не беда! Создать собственный адаптер +хранилища очень просто. Опция `storage` работает с любым объектом, который +соответствует этому [интерфейсу](/ref/core/storageadapter), так что вы можете +подключиться к своему хранилищу всего в нескольких строчках кода. + +> Если вы опубликовали свой собственный адаптер хранения, не стесняйтесь +> редактировать эту страницу и ссылаться на нее, чтобы другие люди могли +> использовать его. + +::: + +Все адаптеры для хранения данных устанавливаются одинаково. Во-первых, +необходимо обратить внимание на имя пакета выбранного вами адаптера. Например, +адаптер хранения для Supabase называется `supabase`. + +**На Node.js** вы можете установить адаптеры с помощью команды +`npm i @grammyjs/storage-`. Например, адаптер хранения для Supabase можно +установить через `npm i @grammyjs/storage-supabase`. + +**На Deno** все адаптеры хранения публикуются в одном модуле Deno. Вы можете +импортировать нужный вам адаптер из его подпапки по адресу +`https://deno.land/x/grammy_storages//src/mod.ts`. Например, адаптер +хранения для Supabase можно импортировать из +`https://deno.land/x/grammy_storages/supabase/src/mod.ts`. + +Ознакомьтесь с соответствующими репозиториями, посвященными каждой отдельной +настройке. В них содержится информация о том, как подключить их к вашему решению +для хранения данных. + +Вы также можете [прокрутить страницу вниз](#усовершенствования-для-хранилищ), +чтобы узнать, как плагин сессий может улучшить любой адаптер хранения. + +## Мульти сессии + +Плагин сессий способен хранить различные фрагменты данных сессии в разных +местах. В принципе, это работает так, как если бы вы установили несколько +независимых экземпляров плагина сессий, каждый из которых имеет свою +конфигурацию. + +Каждый из этих фрагментов данных будет иметь имя, под которым он может хранить +свои данные. Вы сможете получить доступ к `ctx.session.foo` и `ctx.session.bar`, +причем эти значения были загружены из разных хранилищ данных, и они же будут +записаны обратно в разные хранилища данных. Естественно, вы можете использовать +одно и то же хранилище с разной конфигурацией. + +Также можно использовать разные [ключи сессий](#ключи-сессии) для каждого +фрагмента. В результате вы можете хранить часть данных для каждого чата, а +часть - для каждого пользователя. + +> Если вы используете [grammY runner](./runner), убедитесь, что вы правильно +> настроили `sequentialize`, возвращая **все** сессионные ключи в качестве +> ограничений из функции. + +Вы можете использовать эту возможность, передав `type: "multi"` в конфигурацию +сессии. В свою очередь, вам нужно будет настроить каждый фрагмент со своим +собственным конфигом. + +```ts +bot.use( + session({ + type: "multi", + foo: { + // Это также значения по умолчанию + storage: new MemorySessionStorage(), + initial: () => undefined, + getSessionKey: (ctx) => ctx.chat?.id.toString(), + }, + bar: { + initial: () => ({ prop: 0 }), + storage: freeStorage(bot.token), + }, + baz: {}, + }), +); +``` + +Обратите внимание, что вы должны добавить запись конфигурации для каждого +фрагмента, который вы хотите использовать. Если вы хотите использовать +конфигурацию по умолчанию, вы можете указать пустой объект (как мы сделали для +`baz` в примере выше). + +Данные вашей сессии все равно будут состоять из объекта с несколькими +свойствами. Поэтому ваш расширитель контекста не изменится. В приведенном выше +примере можно использовать этот интерфейс при настройке объекта контекста: + +```ts +interface SessionData { + foo?: string; + bar: { prop: number }; + baz: { width?: number; height?: number }; +} +``` + +После этого вы можете продолжать использовать `SessionFlavor` для +своего контекстного объекта. + +## Ленивые сессии + +> В этом разделе описывается оптимизация производительности, о которой +> большинству людей не нужно беспокоиться. + +Ленивые сессии --- это альтернативная реализация сессий, которая может значительно +снизить трафик базы данных вашего бота, пропуская лишние операции чтения и +записи. + +Предположим, что ваш бот находится в групповом чате, где он не отвечает на +обычные текстовые сообщения, а только на команды. Без сессий это будет выглядеть +следующим образом: + +1. Вашему боту отправляется обновление с новым текстовым сообщением. +2. Никакой обработчик не вызывается, поэтому никаких действий не происходит. +3. middleware завершает работу немедленно. + +Как только вы устанавливаете стандартные (строгие) сессии, которые напрямую +предоставляют данные сессии в объект контекста, это происходит: + +1. Обновление с новым текстовым сообщением будет отправлено вашему боту. +2. Данные сессии загружаются из хранилища сессий (например, базы данных). +3. Обработчик не вызывается, поэтому никаких действий не происходит. +4. Идентичные данные сеанса записываются обратно в хранилище сеанса. +5. middleware завершает работу, выполнив чтение и запись в хранилище данных. + +В зависимости от характера вашего бота, это может привести к большому количеству +лишних чтений и записей. Ленивые сессии позволяют пропустить шаги 2. и 4., если +окажется, что ни одному вызванному обработчику не нужны данные сессии. В этом +случае данные не будут ни считываться из хранилища данных, ни записываться в +него. + +Это достигается путем перехвата доступа к `ctx.session`. Если обработчик не +вызван, то к `ctx.session` никогда не будет получен доступ. Ленивые сессии +используют это как индикатор для предотвращения связи с базой данных. + +На практике вместо того, чтобы иметь данные сессии, доступные в `ctx.session`, +вы теперь будете иметь _данные сессии в виде `Promise`_, доступные в +`ctx.session`. + +```ts +// Сессии по умолчанию (строгие сессии) +bot.command("settings", async (ctx) => { + // `session` - это данные сессии + const session = ctx.session; +}); + +// Ленивые сессии +bot.command("settings", async (ctx) => { + // `promise` - это Promise данных сессии, и + const promise = ctx.session; + // `session` - это данные сессии + const session = await ctx.session; +}); +``` + +Если вы никогда не обращаетесь к `ctx.session`, то никаких операций не будет, но +как только вы обратитесь к свойству `session` контекстного объекта, будет +запущена операция чтения. Если вы никогда не вызываете операцию чтения (или +напрямую присваиваете новое значение `ctx.session`), мы знаем, что нам также не +придется записывать данные обратно, поскольку они никак не могли быть изменены. +Следовательно, мы пропускаем и операцию записи. В результате мы получаем минимум +операций чтения и записи, но вы можете использовать сессию почти так же, как и +раньше, просто добавив в код несколько ключевых слов `async` и `await`. + +Так что же нужно для использования ленивых сессий вместо стандартных (строгих)? +В основном вам нужно сделать три вещи: + +1. Используйте для расширения контекста `LazySessionFlavor` вместо + `SessionFlavor`. Они работают одинаково, просто для ленивого варианта + `ctx.session` обернута в `Promise`. +2. Используйте `lazySession` вместо `session` для регистрации middleware сессии. +3. Всегда ставьте строку `await ctx.session` вместо `ctx.session` везде в вашем + middleware, как для чтения, так и для записи. Не волнуйтесь: вы можете + `await` promise с данными сессии столько раз, сколько захотите, но вы всегда + будете ссылаться на одно и то же значение, поэтому никогда не будет + дублирования чтения для обновления. + +Обратите внимание, что при использовании ленивых сессий вы можете присваивать +`ctx.session` как объекты, так и promise объектов. Если вы зададите +`ctx.session` как promise, то оно будет `await` перед записью данных обратно в +хранилище данных. Это позволит использовать следующий код: + +```ts +bot.command("reset", async (ctx) => { + // Гораздо короче, чем если бы сначала нужно было `await ctx.session`: + ctx.session = ctx.session.then((stats) => { + stats.counter = 0; + }); +}); +``` + +Можно долго доказывать, что явное использование `await` предпочтительнее, чем +назначение promise на `ctx.session`, но суть в том, что вы _можете_ сделать это, +если вам по какой-то причине больше нравится такой стиль. + +::: tip Плагины, которым нужны сессии Разработчики плагинов, использующих +`ctx.session`, должны всегда разрешать пользователям передавать +`SessionFlavor | LazySessionFlavor` и, следовательно, поддерживать оба режима. В +коде плагина просто постоянно await `ctx.session`: если передается объект, не +являющийся promise, он просто будет оценен сам по себе, так что вы эффективно +пишете код только для ленивых сессий и, таким образом, автоматически +поддерживаете строгие сессии. +::: + +## Усовершенствования для хранилищ + +Плагин сессий способен расширить возможности любого адаптера хранилища, добавив +к нему дополнительные функции: [таймауты](#таимауты) и [миграции](#миграции). + +Их можно установить с помощью функции `enhanceStorage`. + +```ts +// Используйте улучшенный адаптер для хранения данных. +bot.use( + session({ + storage: enhanceStorage({ + storage: freeStorage(bot.token), // настройте это + // другие настройки здесь + }), + }), +); +``` + +Вы также можете использовать оба варианта одновременно. + +### Таймауты + +Улучшение таймаутов позволяет добавить дату истечения срока действия к данным +сессии. Это означает, что вы можете указать период времени, и если в течение +этого времени сессия не будет изменена, данные для конкретного чата будут +удалены. + +Вы можете использовать тайм-ауты сессий с помощью опции `millisecondsToLive`. + +```ts +const enhanced = enhanceStorage({ + storage, + millisecondsToLive: 30 * 60 * 1000, // 30 минут +}); +``` + +Обратите внимание, что фактическое удаление данных произойдет только при +следующем чтении данных соответствующей сессии. + +### Миграции + +Миграции полезны, если вы развиваете бота дальше, а данные о сессиях уже +существуют. Вы можете использовать их, если хотите изменить данные сессии, не +ломая все предыдущие данные. + +Для этого данным присваиваются номера версий, а затем пишутся небольшие функции +миграции. Функции миграции определяют, как обновлять данные сессии от одной +версии к другой. + +Мы попытаемся проиллюстрировать это на примере. Допустим, вы храните информацию +о домашнем животном пользователя. До сих пор вы хранили только имена питомцев в +строковом массиве в `ctx.session.petNames`. + +```ts +interface SessionData { + petNames: string[]; +} +``` + +Теперь вы понимаете, что хотите также хранить возраст питомцев. + +Вы можете сделать следующее: + +```ts +interface SessionData { + petNames: string[]; + petBirthdays?: number[]; +} +``` + +Это не нарушит существующие данные сессии. Однако это не очень хорошо, потому +что имена и дни рождения теперь хранятся в разных местах. В идеале данные сессии +должны выглядеть следующим образом: + +```ts +interface Pet { + name: string; + birthday?: number; +} + +interface SessionData { + pets: Pet[]; +} +``` + +Функции миграции позволяют преобразовать старый массив строк в новый массив +объектов домашних животных. + +::: code-group + +```ts [TypeScript] +interface OldSessionData { + petNames: string[]; +} + +function addBirthdayToPets(old: OldSessionData): SessionData { + return { + pets: old.petNames.map((name) => ({ name })), + }; +} + +const enhanced = enhanceStorage({ + storage, + migrations: { + 1: addBirthdayToPets, + }, +}); +``` + +```js [JavaScript] +function addBirthdayToPets(old) { + return { + pets: old.petNames.map((name) => ({ name })), + }; +} + +const enhanced = enhanceStorage({ + storage, + migrations: { + 1: addBirthdayToPets, + }, +}); +``` + +::: + +При считывании данных сессии улучшение хранилища проверит, не находятся ли +данные сессии в версии `1`. Если версия ниже (или отсутствует, потому что вы не +использовали эту функцию раньше), то будет запущена функция миграции. Это +обновит данные до версии `1`. Таким образом, в вашем боте вы всегда можете +считать, что данные сессии имеют самую актуальную структуру, а улучшение +хранилища позаботится об остальном и при необходимости перенесет ваши данные. + +С течением времени и дальнейшими изменениями вашего бота вы сможете добавлять +все больше и больше функций миграции: + +```ts +const enhanced = enhanceStorage({ + storage, + migrations: { + 1: addBirthdayToPets, + 2: addIsFavoriteFlagToPets, + 3: addUserSettings, + 10: extendUserSettings, + 10.1: fixUserSettings, + 11: compressData, + }, +}); +``` + +В качестве версий можно выбрать любые числа JavaScript. Независимо от того, +насколько изменились данные сессии для чата, при считывании они будут +перемещаться по версиям до тех пор, пока не будет использована самая последняя +структура. + +### Типы для усовершенствования хранилищ + +При использовании расширений хранилища адаптер хранилища должен хранить больше +данных, чем просто данные сеанса. Например, он должен хранить время, когда +сессия была сохранена в последний раз, чтобы правильно [просрочить](#таимауты) +данные по истечении времени. В некоторых случаях TypeScript сможет определить +правильные типы для вашего адаптера хранения. Однако чаще всего необходимо явно +указать типы данных сессии в нескольких местах. + +Следующий пример фрагмента кода иллюстрирует использование улучшения таймаута с +правильными типами TypeScript. + +```ts +interface SessionData { + count: number; +} + +type MyContext = Context & SessionFlavor; + +const bot = new Bot(""); + +bot.use( + session({ + initial(): SessionData { + return { count: 0 }; + }, + storage: enhanceStorage({ + storage: new MemorySessionStorage>(), + millisecondsToLive: 60_000, + }), + }), +); + +bot.on( + "message", + (ctx) => ctx.reply(`Счетчик чата теперь: ${ctx.session.count++}`), +); + +bot.start(); +``` + +Обратите внимание, что каждый [адаптер хранения](#известные-адаптеры-хранения) +может принимать параметр типа. Например, для +[бесплатных сессий](#бесплатное-хранилище) можно использовать +`freeStorage>` вместо +`MemorySessionStorage>`. То же самое справедливо и для всех +остальных адаптеров хранения. + +## Краткая информация о плагине + +Этот плагин встроен в ядро grammY. Вам не нужно ничего устанавливать, чтобы +использовать его. Просто импортируйте все из самого grammY. + +Кроме того, документация и ссылка на API этого плагина объединены с основным +пакетом. diff --git a/site/docs/ru/plugins/stateless-question.md b/site/docs/ru/plugins/stateless-question.md new file mode 100644 index 000000000..6fd134f6f --- /dev/null +++ b/site/docs/ru/plugins/stateless-question.md @@ -0,0 +1,66 @@ +--- +prev: false +next: false +--- + +# Вопросы без состояния (`stateless-question`) + +> Создание вопросов без статичности для пользователей Telegram, работающих в режиме конфиденциальности + +Вы хотите сохранить конфиденциальность пользователя с помощью [включённого режима приватности Telegram (по умолчанию)](https://core.telegram.org/bots/features#privacy-mode), отправлять пользователям переведенные вопросы на их язык и не сохранять информацию о том, что пользователи делают в данный момент? + +Этот плагин призван решить эту проблему. + +Основная идея заключается в том, чтобы отправить свой вопрос с [специальным текстом](https://en.wikipedia.org/wiki/Zero-width_non-joiner) в конце. +Этот текст невидим для пользователя, но виден для бота. +Когда пользователь отвечает на сообщение, оно проверяется. +Если оно содержит этот специальный текст в конце, значит, это ответ на вопрос. +Таким образом, вы можете иметь много строк для одних и тех же вопросов, как и в случае с переводами. +Вам нужно только убедиться, что `uniqueIdentifier` уникален в пределах вашего бота. + +## Использование + +```ts +import { StatelessQuestion } from "@grammyjs/stateless-question"; + +const bot = new Bot(""); + +const unicornQuestion = new StatelessQuestion("unicorns", async (ctx) => { + console.log("Пользователь считает, что единороги делают:", ctx.message); +}); + +// Не забудьте использовать middleware. +bot.use(unicornQuestion.middleware()); + +bot.command("rainbows", async (ctx) => { + let text; + if (ctx.session.language === "de") { + text = "Was machen Einhörner?"; + } else { + text = "Что делают единороги?"; + } + + return unicornQuestion.replyWithMarkdown(ctx, text); +}); + +// Или отправьте свой вопрос вручную (обязательно используйте parse_mode и force_reply!). +bot.command("unicorn", async (ctx) => { + await ctx.replyWithMarkdown( + "Что делают единороги?" + unicornQuestion.messageSuffixMarkdown(), + { parse_mode: "Markdown", reply_markup: { force_reply: true } }, + ); +}); +bot.command("unicorn", async (ctx) => { + await ctx.replyWithHTML( + "Что делают единороги?" + unicornQuestion.messageSuffixHTML(), + { parse_mode: "HTML", reply_markup: { force_reply: true } }, + ); +}); +``` + +Дополнительную информацию см. в [README репозитория плагина](https://github.com/grammyjs/stateless-question). + +## Краткая информация о плагине + +- Название: `stateless-question` +- [Исходник](https://github.com/grammyjs/stateless-question) diff --git a/site/docs/ru/plugins/transformer-throttler.md b/site/docs/ru/plugins/transformer-throttler.md new file mode 100644 index 000000000..768fcd738 --- /dev/null +++ b/site/docs/ru/plugins/transformer-throttler.md @@ -0,0 +1,120 @@ +--- +prev: false +next: false +--- + +# Контроль флуда (`transformer-throttler`) + +> Вместо этого используйте плагин [auto-retry](./auto-retry). + +Этот плагин регистрирует исходящие API-запросы через [Bottleneck](https://github.com/SGrondin/bottleneck), чтобы ваш бот не сбивал [ограничения скорости](https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this), как описано в этом [расширенном разделе](../advanced/flood) документации. + +::: warning Существуют недокументированные ограничения API +Telegram реализует неопределенные и недокументированные ограничения скорости для некоторых вызовов API. +Эти недокументированные ограничения **не учитываются** троттлером. +Если вы все еще хотите использовать этот плагин, подумайте об использовании плагина [auto-retry](./auto-retry) вместе с ним. +::: + +## Использование + +Вот пример того, как использовать этот плагин с параметрами по умолчанию. +Обратите внимание, что параметры по умолчанию соответствуют фактическим ограничениям скорости, установленным Telegram, так что они должны быть в порядке. + +::: code-group + +```ts [TypeScript] +import { Bot } from "grammy"; +import { run } from "@grammyjs/runner"; +import { apiThrottler } from "@grammyjs/transformer-throttler"; + +const bot = new Bot(""); + +const throttler = apiThrottler(); +bot.api.config.use(throttler); + +bot.command("example", (ctx) => ctx.reply("Я затроттлил")); + +// Если вы используете троттлер, то, скорее всего, захотите использовать runner для одновременной обработки обновлений. +run(bot); +``` + +```js [JavaScript] +const { Bot } = require("grammy"); +const { run } = require("@grammyjs/runner"); +const { apiThrottler } = require("@grammyjs/transformer-throttler"); + +const bot = new Bot(""); + +const throttler = apiThrottler(); +bot.api.config.use(throttler); + +bot.command("example", (ctx) => ctx.reply("Я затроттлил")); + +// Если вы используете троттлер, то, скорее всего, захотите использовать runner для одновременной обработки обновлений. +run(bot); +``` + +```ts [Deno] +import { Bot } from "https://deno.land/x/grammy/mod.ts"; +import { run } from "https://deno.land/x/grammy_runner/mod.ts"; +import { apiThrottler } from "https://deno.land/x/grammy_transformer_throttler/mod.ts"; + +const bot = new Bot(""); + +const throttler = apiThrottler(); +bot.api.config.use(throttler); + +bot.command("example", (ctx) => ctx.reply("Я затроттлил")); + +// Если вы используете троттлер, то, скорее всего, захотите использовать runner для одновременной обработки обновлений. +run(bot); +``` + +::: + +## Настройка + +Троттлер принимает один необязательный аргумент следующего вида: + +```ts +type ThrottlerOptions = { + global?: Bottleneck.ConstructorOptions; // для троттлинга всех вызовов API + group?: Bottleneck.ConstructorOptions; // для троттлинга исходящих групповых сообщений + out?: Bottleneck.ConstructorOptions; // для троттлинга исходящих личных сообщений +}; +``` + +Полный список свойств объектов, доступных для `Bottleneck.ConstructorOptions`, можно найти в [Bottleneck](https://github.com/SGrondin/bottleneck#constructor). + +Если аргумент не передан, созданный троттлер будет использовать настройки конфигурации по умолчанию, которые должны подходить для большинства случаев использования. +Конфигурация по умолчанию выглядит следующим образом: + +```ts +// Исходящий глобальный троттлер +const globalConfig = { + reservoir: 30, // количество новых заданий, которые троттлер будет принимать при запуске + reservoirRefreshAmount: 30, // количество заданий, которые троттлер будет принимать после обновления + reservoirRefreshInterval: 1000, // интервал в миллисекундах, через который резервуар будет обновляться +}; + +// Outgoing Group Throttler +const groupConfig = { + maxConcurrent: 1, // только 1 задание за раз + minTime: 1000, // сколько миллисекунд ждать, чтобы быть готовым, после выполнения задания + reservoir: 20, // количество новых заданий, которые троттлер будет принимать при запуске + reservoirRefreshAmount: 20, // количество заданий, которые троттлер будет принимать после обновления + reservoirRefreshInterval: 60000, // интервал в миллисекундах, через который резервуар будет обновляться +}; + +// Outgoing Private Throttler +const outConfig = { + maxConcurrent: 1, // только 1 задание за раз + minTime: 1000, // сколько миллисекунд ждать, чтобы быть готовым, после выполнения задания +}; +``` + +## Краткая информация о плагине + +- Название: `transformer-throttler` +- [Исходник](https://github.com/grammyjs/transformer-throttler) +- [Ссылка](/ref/transformer-throttler/) diff --git a/site/docs/ru/resources/about.md b/site/docs/ru/resources/about.md new file mode 100644 index 000000000..1e8cf7760 --- /dev/null +++ b/site/docs/ru/resources/about.md @@ -0,0 +1,59 @@ +--- +next: + text: FAQ + link: ./faq +--- + +# О grammY + +## Что такое grammY? + +grammY --- это библиотека, которую вы можете использовать, когда хотите написать своего собственного [чат-бота](https://core.telegram.org/bots) для [Telegram](https://telegram.org). +Создавая ботов, вы заметите, что некоторые части этого процесса утомительны и всегда одинаковы. +grammY делает всю тяжелую работу за вас и упрощает создание бота. + +## Когда grammY был создан? + +Первая публикация кода grammY состоялась в конце марта 2021 года. +Через несколько недель она достигла своей первой стабильной версии. + +## Кто разрабатывает grammY? + +grammY разрабатывается командой добровольцев, которые в свободное время работают над библиотекой, документацией и остальной экосистемой. +Список всех наших участников вы можете найти в [README](https://github.com/grammyjs/grammY#contributors-) нашего репозитория. + +Не стесняйтесь писать в [групповом чате](https://t.me/grammyjs)! +Если вы говорите по-русски, вы также можете присоединиться к нашему русскоязычному чату [здесь](https://t.me/grammyjs_ru)! + +## Как grammY разрабатывается? + +grammY --- полностью бесплатное программное обеспечение с открытым исходным кодом. +Его код доступен на [GitHub](https://github.com/grammyjs/grammY). + +Мы приветствуем любой вклад. +Все изменения в коде проверяются несколькими парами глаз, оптимизируются и широко тестируются, часто с использованием кода различных проектов и людей. + +## Какой язык программирования grammY использует? + +grammY написан с нуля на [TypeScript](https://www.typescriptlang.org/) --- языке программирования, родителем которого является JavaScript. +Поэтому он работает на Node.js. + +Однако grammY может работать и на Deno, который позиционирует себя как преемник Node.js. +(Технически, grammY можно запустить даже в современных браузерах, хотя это редко бывает полезным). + +## Как grammY выглядит по сравнению с конкурентами? + +Если вы переходите с другого языка программирования или библиотеки, вы можете ознакомиться с нашим [списком сравнения библиотек](./comparison) для написания Telegram ботов. + +## Как я могу поддержать вас? + +Есть несколько отличных способов поддержать этот проект! + +- Добавляйте код через [pull request](https://github.com/grammyjs/grammY/pulls). +- [Находите ошибки](https://github.com/grammyjs/grammY/issues/new) и сообщайте об обнаруженной вами ошибке или о фиче, которую мы бы могли добавить, или о чем-либо ещё. +- Помогите с [Документацией](https://github.com/grammyjs/website). +- Помогайте людям в чатах сообщества на [английском](https://t.me/grammyjs) или [русском](https://t.me/grammyjs_ru) языках. +- Или просто расскажите нам, что вы думаете! + Оставив несколько слов о понравившейся фиче, вы поможете нам определить направление развития проекта. + +В настоящее время мы не принимаем никаких пожертвований или другой финансовой поддержки diff --git a/site/docs/ru/resources/comparison.md b/site/docs/ru/resources/comparison.md new file mode 100644 index 000000000..d97a890ed --- /dev/null +++ b/site/docs/ru/resources/comparison.md @@ -0,0 +1,167 @@ +--- +next: false +--- + +# Как grammY конкурирует с другими фреймворками + +Хотя grammY использует некоторые концепции, известные из других библиотек (и веб библиотек), он был написан с нуля для оптимальной читабельности и производительности. + +> Пожалуйста, примите во внимание, что это сравнение необъективно, хотя мы и пытаемся предоставить вам объективное описание преимуществ и недостатков использования grammY по сравнению с другими библиотеками. +> Мы стараемся поддерживать эту статью в актуальном состоянии. +> Если вы заметили, что что-то устарело то пожалуйста, отредактируйте эту страницу, используя ссылку внизу. + +## Сравнение с другими фреймворками на JavaScript + +::: tip Сначала выберите язык программирования +Учитывая, что вы читаете документацию по фреймворку в экосистеме JavaScript то вы, скорее всего, ищете что-то для работы на Node.js или Deno. +Однако если это не так, [прокрутите страницу вниз](#сравнение-с-фреимворками-на-других-языках-программирования), чтобы узнать, какие языки программирования подходят для разработки ботов. +Естественно, вы также найдете краткое сравнение с фреймворками других языков (в основном Python). +::: + +Есть два основных проекта, из которых grammY черпает вдохновение, а именно [Telegraf](https://github.com/telegraf/telegraf) и [NTBA](https://github.com/yagop/node-telegram-bot-api). +Пока что мы сосредоточимся на них, но в будущем мы (или вы?) можем добавить другие сравнения. + +### Telegraf + +grammY берет свое начало в Telegraf, поэтому здесь мы кратко расскажем о том, как эти фреймворки связаны между собой исторически. + +Когда создавался grammY, Telegraf был замечательной библиотекой, и без неё grammY не стал бы самим собой. +Однако раньше Telegraf был написан на JavaScript (в версии 3). +Редкие объявления типов добавлялись вручную и плохо поддерживались, поэтому они были неполными, неправильными и устаревшими. +Строгое объявление типов --- важнейший аспект любой серьезной библиотеки, поскольку они обеспечивают инструментальную поддержку и позволяют значительно быстрее итерировать код. +Многие люди предпочитают иметь безопасность типов при разработке сложных ботов, а для некоторых отказ от неё является критическим. + +В Telegraf v4 была предпринята попытка исправить это, переписав весь код на язык TypeScript. +К сожалению, многие из получившихся типов оказались настолько сложными (но правильными), что их было трудно понять. +Более того, миграция выявила в коде бесчисленные странности ([пример](https://github.com/telegraf/telegraf/issues/1076)), из-за которых было сложно найти правильные типы даже для существующего кода. + +В результате, несмотря на то, что версия 4.0 пыталась _улучшить_ корректность и инструментальную поддержку, в итоге она сделала Telegraf существенно _труднее в использовании_, чем его нетипизированный предшественник. +Понятно, что многие уже существующие пользователи Telegraf v3 не захотели переходить на новую версию. +Новым пользователям также стало сложнее начать работу. + +**grammY делает шаг назад и переосмысливает всю ситуацию со строгой типизацией, создавая фреймворк и ориентируясь на удобство использования.** +Это позволило пропустить множество разочаровывающих дискуссий о том, как справиться со странными внутренними типизациями. +Это позволило проекту получить чистый, последовательный, компилируемый код, который предоставляет пользователям отличные типы (что означает, поддержку IDE). +Безопасность типов, в свою очередь, позволяет использовать более продвинутые возможности, которые в корне меняют наше представление о разработке ботов, такие как [трансформация API](../advanced/transformers). + +Несмотря на то, что Telegraf v3 до сих пор используется многими активными ботами --- библиотека сильно устарела. +Кроме того, экосистема плагинов Telegraf перешла на Telegraf v4 (по крайней мере, те, которые не были перенесены на grammY). + +В данном сравнении grammY сравнивается только с Telegraf v4. + +Вот список причин, по которым вы должны использовать grammY вместо Telegraf. + +- grammY всегда поддерживает последнюю версию Bot API. + Telegraf часто отстает на несколько версий. +- У grammY есть [документация](../). + В Telegraf его нет --- она была заменена сгенерированным руководством по API, в котором нет пояснений, а те немногие руководства, которые существуют, являются неполными и их тяжело найти. +- grammY работает на TypeScript --- типы _просто работают_ и будут следовать за вашим кодом. + В Telegraf вам часто придется писать код определенным образом, иначе он не скомпилируется (даже если на самом деле он будет работать нормально). +- grammY интегрирует подсказки из [официальной документации Bot API](https://core.telegram.org/bots/api) в строки, которые помогают вам во время написания кода. + Telegraf не дает вам никаких пояснений к вашему коду. +- Многое другое, например, более высокая производительность, большая экосистема плагинов, документация, переведенная для миллиардов людей, лучшая интеграция с базами данных и веб-фреймворками, лучшая совместимость с runtime'ом, расширение для [VS Code](https://marketplace.visualstudio.com/items?itemName=grammyjs.grammyjs) и ряд других вещей, которые вы обнаружите по мере продвижения. + +Вот список причин, по которым вам стоит использовать Telegraf вместо grammY. + +- У вас уже есть большой бот, написанный на Telegraf, и вы больше не работаете над ним. + В этом случае переход на grammY может занять больше времени, чем вы сэкономите в долгосрочной перспективе, независимо от того, насколько плавным будет переход. +- Вы знаете Telegraf как свои пять пальцев и не заботитесь о том, чтобы менять свой набор навыков. + grammY вводит ряд новых концепций, которые могут быть незнакомы, если вы пользовались только Telegraf, и использование grammY означает, что вы познакомитесь с новыми вещами. +- Есть несколько деталей, в которых Telegraf и grammY используют разный синтаксис для достижения одной и той же цели, а вы просто предпочитаете один стиль другому. + Например, Telegraf использует `bot.on(message("text"))`, а grammY --- `bot.on("message:text")` для прослушивания текстовых сообщений. + +### NTBA + +Библиотека `node-telegram-bot-api` --- это второй большой проект, повлиявший на развитие grammY. +Его главное преимущество перед другими фреймворками заключается в том, что он до безобразия прост. +Его архитектуру можно описать в одном предложении, в то время как grammY для этого требуется [целый гайд](../guide/) на сайте документации. +Мы считаем, что все эти объяснения на сайте grammY помогают людям легко начать работу, но очень заманчиво иметь библиотеку, которая вообще не нуждается в объяснениях. + +С другой стороны, это хорошо только в краткосрочной перспективе. +Идея поместить все в гигантский файл и использовать примитивный `EventEmitter` для обработки потоков сложных объектов (ака веб-запросы) принесла много боли в мир ботов Telegram, и это, безусловно, помешало реализовать ряд хороших идей. + +Боты всегда начинают с малого, но ответственный фреймворк должен предоставлять им четкий путь для роста и масштабирования. +К сожалению, NTBA ужасно не справляется с этой задачей. +Любой проект, содержащий более 50 строк и использующий NTBA, в итоге превращается в ужасный беспорядок, состоящий из перекрестных ссылок, похожих на спагетти. +Вы не хотите этого. + +### Другие фреймворки + +В настоящее время не существует других библиотек TypeScript, которые стоило бы использовать для создания ботов. +Всё, кроме grammY, Telegraf и NTBA, в основном не поддерживается и, следовательно, ужасно устарело. + +Вы только что создали новую потрясающую библиотеку, а мы еще не знаем о ней? +Не стесняйтесь редактировать эту страницу и добавлять сравнения или расскажите нам о своем мнении в [групповом чате](https://t.me/grammyjs)! + +## Сравнение с фреймворками на других языках программирования + +Есть причины отдать предпочтение другому языку программирования, а не TypeScript. +Самое главное, чтобы вам нравилось работать со своими инструментами и языками. +Если вы твердо решили остановиться на другом языке, то можете не читать дальше. + +Если же вы все еще читаете, то, возможно, хотите узнать, почему grammY написан на TypeScript и почему вам тоже стоит подумать о том, чтобы выбрать этот язык для своего бота. + +В этом разделе мы расскажем о том, как TypeScript имеет ряд преимуществ перед другими языками, когда речь идет о разработке ботов Telegram. +Это сравнение будет ограничено Python, Go и Rust. +Не стесняйтесь добавлять другие разделы, если вы хотите сравнить TypeScript с другим языком. + +Некоторые из приведенных ниже пунктов частично основаны на личном мнении. +У людей разные вкусы, поэтому воспринимайте этот раздел с щепоткой соли. + +### Фреймворки написанные на Python + +Сравнивая TypeScript с Python, можно сделать однозначный вывод. +Выбирайте TypeScript, и вы получите удовольствие: + +1. **Лучший инструментарий редактора.** + Аннотации типов в grammY просто великолепны. + Хотя Python и представил типы в релизе 3.5, они не используются в экосистеме так часто, как это происходит с JavaScript/TypeScript. + Поэтому они не могут сравниться с тем, что вы получаете из коробки с grammY и сопутствующими библиотеками. + С типами поставляется автодополнение на каждом этапе разработки, а также полезные всплывающие подсказки с пояснениями и ссылками. + +2. **Легче масштабировать кодовую базу.** + У системы типов есть и второе преимущество --- она позволяет масштабировать код вашего бота. + Это гораздо сложнее сделать для проектов, написанных на языке с худшей безопасностью типов. + +3. **Легче масштабировать нагрузку.** + Если ваш бот действительно начнет набирать популярность, то масштабировать ботов, написанных на JS, будет значительно проще, чем на Python. + +4. **Более высокая отзывчивость вашего бота.** + На данный момент движок V8 и его партнеры делают JavaScript самым быстрым в мире языком сценариев. + Если вы хотите, чтобы ваш бот был максимально быстрым и при этом пользовался динамичным языком, то grammY --- ваш лучший выбор. + +Как всегда, языки программирования отлично справляются с определенными задачами, а для других их следует избегать. +Этот язык не является исключением. + +Например, при нынешнем состоянии экосистемы --- всё, что связано с машинным обучением, не должно выполняться на JavaScript. +Однако, когда речь идет о веб-серверах, TypeScript, как правило, является более хорошим выбором. + +### Фреймворки написанные на Go + +Если вы хорошо знаете TypeScript и [Go](https://go.dev/), то разумным критерием выбора языка для вашего бота будет баланс между скоростью разработки и скоростью выполнения. + +Если вы не до конца уверены в том, что создаёте, то выбирайте grammY. +TypeScript позволяет выполнять итерации над проектом с невероятной скоростью. +Он отлично подходит для быстрого создания прототипов, опробования новых вещей, знакомства с ботами и быстрого выполнения задач. +Как правило, обработка ~100 000 000 обновлений в день может быть легко выполнена с помощью TypeScript, но выход за эти рамки потребует дополнительной работы, например, использования еще одного плагина grammY. + +Выбирайте библиотеку, написанную на Go, если вы уже достаточно хорошо знаете, что будете создавать (не ожидаете, что вам понадобится большая помощь), и уже знаете, что ваш бот будет обрабатывать очень большое количество обновлений. +Будучи нативно компилируемым языком, Go превосходит TypeScript по скорости работы процессора на несколько пунктов. +Это менее важно, когда вы пишете бота, потому что большая часть времени уходит на ожидание сети, но со временем начнет иметь значение, насколько быстро ваш бот может разбирать JSON. +В таких случаях Go может оказаться лучшим выбором. + +### Фреймворки написанные на Rust + +Аналогичный тезис можно привести [как в случае с Go](#фреимворки-написанные-на-go), но в случае с [Rust](https://www.rust-lang.org/) он еще сильнее. +В некотором смысле, написание Rust займет у вас еще больше времени, но ваш бот будет еще быстрее. + +Также обратите внимание, что использование Rust --- это весело, но редко необходимо для ботов. +Если вы хотите использовать Rust, то сделайте это, но подумайте о том, что вы любите Rust, а не о том, что это подходящий инструмент для работы. + +## Как возразить такому сравнению? + +Если вам кажется, что на этой странице что-то не так, не спешите злиться и защищать свой любимый язык или библиотеку! +Пожалуйста, сообщите нам об этом в [групповом чате](https://t.me/grammyjs)! +Мы будем рады, если вы расскажете нам о своей точке зрения. +Естественно, вы также можете просто отредактировать эту страницу на GitHub или создать [Issue](https://github.com/grammyjs/website/issues/new), чтобы указать на ошибки или предложить другие вещи. +Эта страница всегда будет иметь возможность стать более объективной и более справедливой. diff --git a/site/docs/ru/resources/faq.md b/site/docs/ru/resources/faq.md new file mode 100644 index 000000000..033b46261 --- /dev/null +++ b/site/docs/ru/resources/faq.md @@ -0,0 +1,167 @@ +--- +prev: + text: О grammY + link: ./about +--- + +# ЧаВо? + +Здесь собраны часто задаваемые вопросы, которые не поместились больше нигде. +Вопросы, касающиеся [общих ошибок](#почему-я-получаю-эту-ошибку) и [Deno](#вопросы-о-deno), были сгруппированы в двух специальных разделах. + +Если этот FAQ не отвечает на ваш вопрос, вам также стоит заглянуть в [Bot FAQ](https://core.telegram.org/bots/faq), написанный командой Telegram. + +## Где найти документацию метода? + +В документации API. +Возможно, вы захотите лучше понять [это](../guide/). + +## Методу не хватает параметра! + +Нет, это не так. + +1. Убедитесь, что у вас установлена последняя версия grammY. +2. Проверьте [здесь](https://core.telegram.org/bots/api), является ли параметр необязательным. + Если да, то grammY будет собирать его в объекте опций под названием `other`. + Передайте `{имя_параметра: значение}` в это место, и все заработает. + Как обычно, TypeScript автоматически заполнит имена параметров. +3. Возможно, перепроверьте сигнатуру метода [actions](../guide/context#доступные-деиствия) в `ctx` [здесь](/ref/core/context#methods), или методов API (`ctx.api`, `bot.api`) [здесь](/ref/core/api#methods). + +## Как получить доступ к истории чата? + +Вы не можете. + +Telegram не хранит сообщения для вашего бота. + +Вместо этого вам нужно ждать появления новых сообщений / постов в канале и сохранять их в своей базе данных. +Затем вы можете загрузить историю чатов из своей базы данных. + +Именно это и делает плагин [conversations](../plugins/conversations) для соответствующей части истории сообщений. + +## Как я могу работать с медиагруппами? + +Нельзя... по крайней мере, не так, как вы думаете. + +Медиагруппы в действительности существует только в пользовательском интерфейсе клиента Telegram. +Для бота работа с медиагруппой это то же самое, что и работа с серией отдельных сообщений. +Самый практичный совет --- игнорировать существование медиагрупп и просто писать бота, ориентируясь на отдельные сообщения. +Тогда медиагруппы будут работать автоматически. +Например, вы можете попросить пользователя [нажать кнопку](../plugins/keyboard#встроенные-клавиатуры) или отправить `/done`, когда все файлы будут загружены в чат вашего бота. + +Но если клиент Telegram может это делать, то и мой бот должен уметь делать то же самое! + +И да, и нет. +Технически, существует `media_group_id`, который позволяет определить сообщения, принадлежащие одной медиагруппе. +Однако, + +- нет способа узнать количество сообщений в медиагруппе, +- нет возможности узнать, когда было получено последнее сообщение в медиагруппе, и +- между сообщениями медиагруппа могут быть отправлены другие сообщения, например текстовые, служебные и т.д. + +Так что да, теоретически вы можете знать, какие сообщения принадлежат друг другу, но только в отношении тех сообщений, которые вы получили на данный момент. +Вы не можете знать, будут ли добавлены новые сообщения в медиагруппу позже. +Если вы когда-нибудь получали медиагруппу в Telegram при _очень_ плохом интернет-соединении, вы можете увидеть, как клиент неоднократно перегруппировывает медиагруппу по мере поступления новых сообщений. + +## Почему я получаю эту ошибку? + +### 400 Bad Request: Cannot parse entities + +Вы отправляете сообщение с форматированием, то есть устанавливаете `parse_mode` при отправке сообщения. +Однако ваше форматирование нарушено, поэтому Telegram не знает, как его разобрать. +Вам следует перечитать [раздел о форматировании](https://core.telegram.org/bots/api#formatting-options) в документации Telegram. +Смещение байта, указанное в сообщении об ошибке, подскажет вам, где именно в вашей строке находится ошибка. + +::: tip Передача сущностей вместо форматирования +При желании вы можете предварительно разобрать сущности для Telegram и указать `entities` при отправке сообщения. +Тогда текст вашего сообщения может быть обычной строкой. +Таким образом, вам не придется беспокоиться о появлении странных символов. +Может показаться, что для этого нужно больше кода, но на самом деле это гораздо более надежное и безошибочное решение данной проблемы. +Самое главное, что это значительно упрощается благодаря нашему плагину [parse-mode](../plugins/parse-mode). +::: + +### 401 Unauthorized + +Ваш токен бота неправильный. +Возможно, вы думаете, что он правильный. +Это не так. +Поговорите с [@BotFather](https://t.me/BotFather), чтобы узнать, какой у вас токен. + +### 403 Forbidden: bot was blocked by the user + +Вероятно, вы пытались отправить сообщение пользователю и столкнулись с этой проблемой. + +Когда пользователь блокирует вашего бота, вы не можете отправлять ему сообщения или взаимодействовать с ним каким-либо другим способом (за исключением случаев, когда ваш бот был приглашен в групповой чат, участником которого является пользователь). +Telegram делает это для защиты своих пользователей. +Вы не можете ничего с этим поделать. + +Но вы можете: + +- Обработать ошибку и, например, удалить данные пользователя из своей базы. +- Проигнорировать ошибку. +- Слушать обновления `my_chat_member` через `bot.on("my_chat_member")`, чтобы получить уведомление, когда пользователь заблокирует вашего бота. + Подсказка: сравните поля `status` старого и нового участника чата. + +### 404 Not found + +Если это происходит при запуске бота, значит, у вас неправильный токен. +Поговорите с [@BotFather](https://t.me/BotFather), чтобы узнать, какой у вас токен. + +Если ваш бот работает нормально большую часть времени, но потом внезапно вы получаете сообщение 404, значит, вы делаете что-то очень странное. +Вы можете спросить нас в [групповом чате](https://t.me/grammyjs) (или в [русскоязычном групповом чате](https://t.me/grammyjs_ru)). + +### 409 Conflict: terminated by other getUpdates request + +Вы случайно запускаете бота дважды при [Long Polling](../advanced/reliability#long-polling). +Вы можете запустить только один экземпляр своего бота. + +Если вы думаете, что запустили бота только один раз, вы можете просто отозвать токен бота. +Это остановит все остальные экземпляры. +Для этого обратитесь к [@BotFather](https://t.me/BotFather). + +### 429: Too Many Requests: retry after X + +Поздравляем! +Вы столкнулись с ошибкой, которая относится к числу наиболее трудно устранимых. + +Есть два возможных сценария: + +**Первый:** У вашего бота не так много пользователей. +В этом случае вы просто спамите серверам Telegram, отправляя слишком много запросов. +Решение: не делайте этого! +Вам следует серьезно задуматься о том, как существенно сократить количество вызовов API. + +**Второй:** Ваш бот становится очень популярным и у него много пользователей (сотни тысяч). +Вы уже позаботились о том, чтобы использовать минимальное количество вызовов API для самых распространенных операций вашего бота, но все равно сталкиваетесь с этими ошибками (так называемые ограничениями флуда). + +Вы можете сделать несколько вещей: + +1. Прочитайте эту [статью](../advanced/flood) в документации, чтобы получить базовое понимание ситуации. +2. Используйте плагин [`auto-retry`](../plugins/auto-retry). +3. Обратитесь за помощью к нам в [групповой чат](https://t.me/grammyjs). + Там есть опытные люди. +4. Можно попросить Telegram увеличить лимиты, но это маловероятно, если вы не выполнили сначала шаги 1-3. + +### Cannot find type definition file for 'node-fetch' + +Это происходит из-за отсутствия деклараций типов. + +Рекомендуемый способ исправить это --- установить `skipLibCheck` на `true` в настройках компилятора TypeScript (в файле `tsconfig.json`). + +Если вы уверены, что вам нужно сохранить значение `false`, вы можете установить недостающие определения типов, выполнив команду `npm i -D @types/node-fetch@2`. + +## Вопросы о Deno + +### Почему вы поддерживаете Deno? + +Некоторые важные причины, по которым мы любим [Deno](https://deno.com/) больше, чем [Node.js](https://nodejs.org): + +- Проще и быстрее начать работу. +- Инструментарий значительно лучше. +- Он нативно выполняет TypeScript. +- Нет необходимости поддерживать `package.json` или `node_modules`. +- Пересмотренная стандартная библиотека. + +> Компания Deno была основана Райаном Далом --- тем самым человеком, который изобрел Node.js. +> Он рассказал о своих 10 сожалениях о Node.js в этом [видео](https://youtu.be/M3BM9TB-8yA). + +Сам grammY --- это в первую очередь Deno, и он бэкпортирован для поддержки Node.js так же хорошо.