Адаптация Telegram-плагина под VK
| Название плагина |
|---|
Илья Зыщик
Описание
Плагин Vk позволяет форматироватьиспользовать текстодин и кнопкитот отправляемыеже скрипт бота в ВК.Telegram, Max и VK. Он не вводит новый класс: плагин «патчит» методы класса TelegramMessage из плагина Common.Integrations.Telegram.
Плагин поддерживает отправку текстовых сообщений, создание inline-клавиатур (кнопки text и open_link), обработку нажатий и текстового ввода, а также отправку медиагрупп (по одному вложению). PHP-драйвер VK не меняется: часть поведения Telegram/Max в VK осознанно деградирует (см. таблицу совместимости).
Документация VK API: https://dev.vk.com/method/messages.send
Подключение
Сначала подключается Telegram-плагин, затем при необходимости Max, затем — Vk. Порядок важен: Vk патчит уже загруженный класс. Повторное подключение безопасно (патч ставится один раз).
snippet(let TelegramMessage = require('Business.Common.Integrations.VK'Telegram')
varrequire('Common.Integrations.Max') // опционально
require('Common.Integrations.Vk')
let msg = new VkMessage(TelegramMessage()
Больше ничего в скрипте менять не нужно: проверять канал (if (isVk)) не требуется.
ИспользованиеОсновные параметры
Текст
msg.text = 'Привет'"Ваше сообщение" // Текст сообщения
msg.send()parse_mode = "HTML" // В VK разметка снимается до plain text
msg.keyboard = "Да[yes]==Нет[no]" // Создание клавиатуры
msg.entities = [] // В VK игнорируются (текст очищается так же, как при parse_mode)
Кнопки
text
text
text
message (plain)
parse_mode
parse_mode
format (HTML → html, Markdown/MarkdownV2 → markdown)
strip → plain
keyboard
inline-клавиатура
вложение inline_keyboard
VK keyboard JSON (inline: true)
entities
entities
преобразуются в HTML
strip
protect_content
поддерживается
игнорируется
игнорируется
Форматы клавиатуры
DSL:Формат строки клавиатуры полностью совпадает с Telegram-плагином.
Вертикальное разделение — используйте ==- новый ряд;
msg.keyboard = "Кнопка1[btn1]==Кнопка2[btn2]" // Кнопки будут расположены вертикально
Горизонтальное разделение — используйте ||;
msg.keyboard = "Кнопка1[btn1]||Кнопка2[btn2]" // Кнопки будут расположены горизонтально
msg.keyboard = "Кнопка1[btn1]||Кнопка2[btn2]==Кнопка3[btn3]||Кнопка4[btn4]"
Значение в квадратных скобках для обычной кнопки отправляется в одномVK рядукак action.type=text с payload в виде JSON-строки "btn_static_<значение>" (VK требует валидный JSON, иначе ошибка API 911). В getMessagePayload().callback_data кавычки и префикс снимаются — так же, как в Telegram/Max.
Специальные типы кнопок
msg.keyboard = "Отправить контакт[telegram_contact]"
Текст[команда]Запрос msg.keyboard = "Отправить локацию[telegram_location]"
Сайт[msg.keyboard = "Посетить сайт[https://example.com]"
msg.keyboard = "Открыть приложение{https://webapp-url.com}"
ПодписьЛимит обрезаетсяподписи докнопки 20VK символов,— кнопок40 несимволов больше(плагин 7.обрезает).
Примеры использования
Простое меню с обработкой ответов
Код callback_dataвсегдабудет {работать command:корректно "..."только }.в команде JS Callback. Один и тот же код работает для Telegram, Max и VK.
let TelegramMessage = require('Common.Integrations.Telegram')
require('Common.Integrations.Max')
require('Common.Integrations.Vk')
let msg = new TelegramMessage()
msg.text = "Выберите действие:"
msg.keyboard = "Информация[info]==Помощь[help]==Настройки[settings]"
msg.parse_mode = "HTML"
switch (true) {
case (isFirstImmediateCall):
msg.send()
return false
case (msg.getMessagePayload()?.callback_data == "info"):
msg.addReplyToText()
bot.sendMessage("Информация о боте...")
return false
case (msg.getMessagePayload()?.callback_data == "help"):
msg.addReplyToText()
bot.sendMessage("Справка по использованию...")
return false
case (msg.getMessagePayload()?.callback_data == "settings"):
msg.addReplyToText()
bot.runScriptForLead(123, leadId) // Запуск скрипта настроек
return false
}
Минимальное демо: кнопки + переход дальше
const TelegramMessage = require('Common.Integrations.Telegram')
require('Common.Integrations.Max')
require('Common.Integrations.Vk')
const msg = new TelegramMessage()
msg.text = 'Меню'VK+Telegram+Max demo\n\nВыберите кнопку:'
msg.keyboard = 'Да[yes]Кнопка A[a]==Нет[no]Кнопка B[b]'
msg.parse_mode = 'HTML'
if (isFirstImmediateCall) {
msg.send()
return false
}
msg.addReplyToText()
return true
В Telegram и Max после нажатия клавиатура исчезнет, а к тексту сообщения добавится строка ➤ Кнопка A. В VK кнопкаподпись частокнопки уже приходит текстомв подписичат как сообщение пользователя (эхо text-кнопки), поэтому addReplyToText() ничего не callback.
отправляет — только очищает _keyboard и возвращает getMessagePayload().
Справочник методов
Основные методы отправки
Получение информации
Обработка ответов пользователя:
snippet('Business.Integrations.VK')// varПроверка msgтипа = new VkMessage()
var MENU = 'Да[yes]==Нет[no]'
msg.keyboard = MENU
var p = msg.getMessagePayload()ввода
if (!isFirstImmediateCallmsg.getMessagePayload()?.input_type && p.callback_data && p.callback_data !== 'undefined'"write") {
bot.runScriptByCodeForLead(p.callback_data,// leadId)Пользователь returnнаписал trueтекст
}
// Проверка нажатия кнопки
if (msg.textgetMessagePayload()?.callback_data == 'Выберите:'"button_id") {
// Пользователь нажал кнопку
}
Структура вебхука VK отличается от Telegram: событие всегда message_new. Сообщение лежит в object.message (API ≥ 5.103) или прямо в object. Нажатие text-кнопки приходит как message_new с полем payload (JSON-строка); плагин декодирует его, ищет кнопку в _keyboard и выставляет input_type=press. Текст без payload — input_type=write; если подпись совпала с кнопкой текущей клавиатуры, callback_data берётся из неё.
Отладка
msg.debug("Отладочное сообщение")
В Telegram при bot_debug_level == 0 используется «тихий» getMe. В VK такого метода нет: при уровне 0 отладка не отправляется пользователю. При ненулевом уровне сообщение уходит в чат (как и в Telegram-плагине).
Лучшие практики
if (isFirstImmediateCall) {
msg.send()
return truefalse
}
Также
1if (msg.getMessagePayload()?.input_type == "write") { bot.sendMessage('Пожалуйста, используйте кнопки') return false },
2Картинки
После нажатия кнопки вызывайте addReplyToText() — в Telegram/Max это скроет клавиатуру и msg.addReplyToText()
Форматирование текста и entities
ПлагинVK не принимает толькоHTML/Markdown в messages.send так же, как Telegram/Max. Если заданы parse_mode или entities, плагин удаляет HTML-теги, типичные entities и imagefile.маркеры ФайлMarkdown должени бытьотправляет указанplain как публичный URL.text.
msg.text = 'Фото'"<b>Жирный текст</b>\n<i>Курсив</i>\n<code>Моноширинный текст</code>"
msg.filesDataparse_mode = ["HTML"
// В VK уйдёт: "Жирный текст\nКурсив\nМоноширинный текст"
Работа с entities
msg.entities в VK не отправляются. Текст очищается так же, как при parse_mode.
let TelegramMessage = require('Common.Integrations.Telegram')
require('Common.Integrations.Max')
require('Common.Integrations.Vk')
let msg = new TelegramMessage()
let messageData = lead.getJsonAttr("messageData")
msg.text = messageData?.text
msg.entities = messageData?.entities
// Не используйте одновременно parse_mode и entities
Отправка медиагрупп
Глобальная функция sendMediaGroup для лидов VK отправляет вложения по одному через bot.sendMessage с { type:type, url }. Типы элементов приводятся так: photo/image → image, document/file → file, video → video, audio → audio. Подпись очищается от разметки. Для Telegram/Max-лидов вызывается исходная (или Max) реализация.
Работа с файлами
Входящие файлы обрабатываются стандартными методами бота — так же, как в Telegram-плагине. Вы можете получить URL файла и сохранить его в карточку лида.
let TelegramMessage = require('Common.Integrations.Telegram')
require('Common.Integrations.Max')
require('Common.Integrations.Vk')
let msg = new TelegramMessage()
// Все входящие файлы
let attachments = bot.getAllAttachments()
if (Boolean(attachments?.[0]?.url)) {
let uploadData = bot.downloadFileFromUrl(attachments[0].url)
msg.debug('Пользователь отправил файл: 'image' + uploadData.url)
lead.setAttr('file', url:uploadData.url)
'https://example.com/pic.jpg' Запуск скрипта обработки
bot.runScriptByCodeForLead("recieveFile", lead.getData('id'))
bot.stop()
}
]
msg.send()
Пример ожидания фото:
let TelegramMessage = require('Common.Integrations.Telegram')
require('Common.Integrations.Max')
require('Common.Integrations.Vk')
let msg = new TelegramMessage()
msg.text = 'Документ'А теперь жду твоё фото 😉'
msg.filesDataparse_mode = ['HTML'
{ type: 'file', url: 'https://example.com/doc.pdf' }
]
msg.send()После send():
lead.getJsonAttr('vk_last_send_result')lead.getAttr('vk_last_message_id')Входящие вложения
msg.getImages()
msg.getFiles()
msg.getAllAttachments()Видео из VK сейчас принимать нельзя
Исходный код плагина
/**
* Upload в VK: только image и file (photo/doc).
* Текст+кнопки с вложением платформа для VK отправляет раздельно (не как TG/Max).
*/
if (typeof VkMessage === 'undefined') {
var vkNormalizeCommand = function (data) {
var s
var o
if (data == null) {
return ''
}
if (typeoflet data === 'object'bot.getAllAttachments()
switch (true) {
return String(data.command || data.callback_data || '')
}
s = String(data)
ifcase (s.charAt(0) === '{') {
try {
o = JSON.parse(s)
return String(o.command || o.callback_data || '')
} catch (e) {}
}
if (s.indexOf('btn_static_') === 0) {
return s.slice('btn_static_'.length)
}
return s
}
var vkMakeCallback = function (command) {
return { command: String(command) }
}
var vkSafeCall = function (fn, args) {
try {
if (typeof fn !== 'function') {
return []
}
return fn.apply(bot, args || []) || []
} catch (e) {
return []
}
}
var vkMapAttachmentType = function (raw) {
var t = String(raw || 'file').toLowerCase()
if (t === 'photo' || t === 'image' || t === 'sticker') {
return 'image'
}
if (t === 'document' || t === 'doc' || t === 'file') {
return 'file'
}
return null
}
VkMessage = function () {
this._text = null
this._keyboard = null
this._attachments = []
this._answer_symbol = '>'
this._max_button_label = 20
this._max_buttons = 7
this._baseServerUrl = 'https://app.metabot24.com/' /*меняем на сервер который будет использоваться для отправки сообщений*/
this.webhookPayload = bot.getWebhookPayload()
}
Object.defineProperty(VkMessage.prototype, 'text', {
get: function () {
return this._text
},
set: function (value) {
this._text = value
}
})
Object.defineProperty(VkMessage.prototype, 'keyboard', {
get: function () {
return this._keyboard
},
set: function (value) {
this._keyboard = value ? this.createVkKeyboard(value) isFirstImmediateCall):
null
}
})
Object.defineProperty(VkMessage.prototype, 'answer_symbol', {
set: function (value) {
this._answer_symbol = value
}
})
Object.defineProperty(VkMessage.prototype, 'filesData', {
get: function () {
return this._attachments
},
set: function (value) {
var i
var prepared
var item
var list
this._attachments = []
if (!value) {
return
}
if (Object.prototype.toString.call(value) !== '[object Array]') {
this.debug('filesData must be an array')
return
}
list = value
for (i = 0; i < list.length; i++) {
item = list[i]
prepared = this.prepareAttachment(item)
if (prepared) {
this._attachments.push(prepared)
}
}
}
})
Object.defineProperty(VkMessage.prototype, 'baseServerUrl', {
set: function (value) {
if (value) {
this._baseServerUrl = String(value)
if (this._baseServerUrl.charAt(this._baseServerUrl.length - 1) !== '/') {
this._baseServerUrl += '/'
}
}
}
})
VkMessage.prototype.createVkKeyboard = function (keyboard) {
var rows = String(keyboard).split('==')
var result = []
var count = 0
var i
var j
var cols
var row
var item
var btn
var code
for (i = 0; i < rows.length; i++) {
cols = rows[i].split('||')
row = []
for (j = 0; j < cols.length; j++) {
item = String(cols[j] || '').trim()
if (!item) {
continue
}
if (count >= this._max_buttons) {
break
}
btn = null
if (/https?:\/\//.test(item) && /\[[^\[\]]+\]/.test(item)) {
code = item.match(/\[[^\[\]]+\]/)[0].slice(1, -1)
btn = {
text: this._clip(item.replace(/\[[^\[\]]+\]/g, '')),
url: code,
callback_data: vkMakeCallback(code),
text_button_label: String(count + 1)
}
} else if (/\[[^\[\]]+\]/.test(item)) {
code = item.match(/\[[^\[\]]+\]/)[0].slice(1, -1)
btn = {
text: this._clip(item.replace(/\[[^\[\]]+\]/g, '')),
callback_data: vkMakeCallback(code),
text_button_label: String(count + 1)
}
} else {
btn = {
text: this._clip(item),
callback_data: vkMakeCallback(item),
text_button_label: String(count + 1)
}
}
row.push(btn)
count++
}
if (row.length) {
result.push(row)
}
}
return result
}
VkMessage.prototype._clip = function (label) {
var text = String(label || '').trim()
if (text.length > this._max_button_label) {
return text.slice(0, this._max_button_label)
}
return text
}
VkMessage.prototype.debug = function (data) {
bot.sendMessage(
'VK debug\n\n' + (typeof data === 'string' ? data : JSON.stringify(data))
)
}
VkMessage.prototype.processLocalFile = function (filePath) {
var attrs
var businessId
attrs = bot.getDebugAttributes ? bot.getDebugAttributes() : null
businessId = attrs && attrs.business_id != null ? attrs.business_id : ''
return this._baseServerUrl + 'uploads/business/' + businessId + '/' + filePath
}
/**
* Готовит вложение для bot.sendMessage(..., attachments).
* Поддерживаемые type: image, file (photo как image, document/doc как file).
*/
VkMessage.prototype.prepareAttachment = function (item) {
var type
var url
var out
if (!item || typeof item !== 'object') {
return null
}
type = vkMapAttachmentType(item.type || item.file_type)
if (!type) {
this.debug({
error: 'unsupported attachment type for VK upload',
type: item.type || item.file_type,
hint: 'use image or file only'
})
return null
}
url = item.url || item.file_url || item.file_id || null
if (!url && item.file_path) {
url = this.processLocalFile(item.file_path)
}
if (!url) {
this.debug({
error: 'attachment url missing',
item: item
})
return null
}
out = {
type: type,
url: String(url)
}
if (item.caption != null) {
out.caption = String(item.caption)
} else if (item.text != null) {
out.caption = String(item.text)
}
if (item.payload && typeof item.payload === 'object') {
out.payload = item.payload
}
if (item.file_id && item.file_id !== url) {
out.file_id = item.file_id
}
return out
}
VkMessage.prototype.getFiles = function (mimeInclude, mimeExclude) {
if (mimeExclude === undefined) {
return vkSafeCall(bot.getFiles, [mimeInclude])
}
return vkSafeCall(bot.getFiles, [mimeInclude, mimeExclude])
}
VkMessage.prototype.getImages = function (mimeInclude) {
if (mimeInclude === undefined) {
return vkSafeCall(bot.getImages, [])
}
return vkSafeCall(bot.getImages, [mimeInclude])
}
VkMessage.prototype.getVideos = function (mimeInclude) {
if (mimeInclude === undefined) {
return vkSafeCall(bot.getVideos, [])
}
return vkSafeCall(bot.getVideos, [mimeInclude])
}
VkMessage.prototype.getAllAttachments = function (filesMimeExclude) {
if (filesMimeExclude === undefined) {
return vkSafeCall(bot.getAllAttachments, [])
}
return vkSafeCall(bot.getAllAttachments, [filesMimeExclude])
}
VkMessage.prototype._resolveMessageId = function (resultPayload) {
var messageId = null
if (typeof bot.getLastMessageId === 'function') {
try {
messageId = bot.getLastMessageId()
} catch (e) {}
}
if (messageId != null) {
return messageId
}
if (!resultPayload) {
return null
}
if (resultPayload.response != null) {
return resultPayload.response
}
if (resultPayload.message_id != null) {
return resultPayload.message_id
}
if (resultPayload.result != null && typeof resultPayload.result !== 'object') {
return resultPayload.result
}
if (resultPayload.result && resultPayload.result.message_id != null) {
return resultPayload.result.message_id
}
return null
}
VkMessage.prototype.send = function () {
var text
var sent
var resultPayload
var messageId
var hasAttachments
var keyboard
hasAttachments =
Object.prototype.toString.call(this._attachments) === '[object Array]' &&
this._attachments.length > 0
if (!this._text && !hasAttachments) {
this.debug('text or filesData is required'msg.send()
return false
}case text = this._text(Boolean(data?.[0]?.type != null'image')):
&&bot.sendMessage('Отправь String(this._text)изображение. !Такой формат не подходит')
return false
case (Boolean(data?.[0]?.type == '' ? String(this._text) image')):
'let '
keyboard = this._keyboard && this._keyboard.length ? this._keyboard : null
if (hasAttachments) {
sentuploadData = bot.sendMessage(text,downloadFileFromUrl(data[0].url)
keyboard,msg.debug('Пользователь this._attachments)отправил } else if (keyboard) {
sent = bot.sendMessage(text, keyboard)
} else {
sent = bot.sendMessage(text)
}
resultPayload = bot.getLastResultPayload()
lead.setJsonAttr('vk_last_send_result', {
sent: sent,
has_keyboard: !!keyboard,
has_attachments: hasAttachments,
attachments_count: hasAttachments ? this._attachments.length : 0,
result: resultPayload
})
messageId = this._resolveMessageId(resultPayload)
if (messageId != null) {
lead.setAttr('vk_last_message_id', String(messageId))
}
if (sent === false || (resultPayload && resultPayload.error)) {
this.debug({
error: 'send failed',
sent: sent,
has_attachments: hasAttachments,
result: resultPayload
})
}
return this.getMessagePayload()
}
VkMessage.prototype.addReplyToText = function () {
var payload = this.getMessagePayload()
if (payload.input_type === 'press' && payload.text) {
this._text = this._text + '\n\n' + this._answer_symbol + 'файл: ' + payload.textuploadData.url)
}lead.setAttr('photo', this._keyboarduploadData.url)
=bot.sendMessage('Класс! nullФото this._attachments = []загрузил')
return this.send(true
default:
msg.addReplyToText()
}
VkMessage.prototype._attachIncoming = function (result) {
var attachments
var hasText
attachments = this.getAllAttachments()
if (Object.prototype.toString.call(attachments) !== '[object Array]') {
attachments = []
}
result.attachments = attachments
hasText = result.text != null && String(result.text) !== '' && result.text !== 'undefined'
if (attachments.length && !hasText && result.input_type !== 'press') {
result.input_type = 'attachment'
if (!result.text || result.text === 'undefined') {
result.text = ''
}
}
return resulttrue
}
Рекомендации =по functionработе с файлами
Совместимость rс varTelegram-плагином
send()
✅
✅
✅
VK: messages.send
edit()
✅
✅
⚠️
VK: новое сообщение
addReplyToText()
✅
✅
⚠️
VK: ничего не отправляет (removeInlineKeyboard()
removeReplyKeyboard()
_remove_keyboard_text
getMessagePayload()
debug()
bot_debug_level == null0 parse_mode = vkNormalizeCommand(command)HTML
parse_mode = 0;MarkdownV2
entities
✅
⚠️
❌
strip
Кнопки contact / location
✅
✅
❌
кнопка пропускается +open_link
Web App {url}
tg://user?id=
✅
❌
❌
кнопка пропускается
protect_content
✅
❌
❌
игнорируется
TelegramPhoto
✅
❌
❌
не патчится, используйте sendMediaGroup
sendMediaGroup
✅
✅
⚠️
по одному вложению
Эхо подписи кнопки
нет
нет
да
особенность VK text-кнопок