+7(921)-851-18-76 Иконка телефона Запросить звонок
Опубликовано:

Спасаем бизнес: Создаём VK-бота на PHP, Python, JS и C#. Часть 3 — Кнопки и клавиатуры: делаем бота удобным

В прошлых частях мы научили бота реагировать на сообщения и отвечать пользователю. Но текстовый интерфейс — это как разговор с глухим: пользователь должен угадать, что можно написать, и каждый раз набирать команды руками. Это неудобно, медленно и повышает порог входа.
Можно, конечно подключить к ответам нейросеть или создать огромный массив фраз, на которые отвечает пользователь, но мы немного упростим задачу.

VK API предоставляет два способа сделать общение с ботом интуитивным: обычные клавиатуры, которые прикрепляются к полю ввода и заменяют собой клавиатуру телефона, и inline-клавиатуры, которые крепятся к конкретному сообщению и остаются рядом с ним. Первые хороши для пошаговых сценариев (главное меню, формы), вторые — для контекстных действий (кнопки «Лайк», «Подробнее» под постом).
В этой части мы разберём оба типа: научимся создавать клавиатуры, настраивать их внешний вид, обрабатывать нажатия и выбирать подходящий сценарий для каждой задачи. Все примеры — снова на четырёх языках: PHP, Python, JavaScript и C#.

Предварительная настройка: готовим сообщество к работе с клавиатурами

Прежде чем погружаться в код, убедимся, что ваше сообщество настроено правильно. Клавиатуры — это расширенная функциональность, и для их работы нужно выполнить несколько простых шагов:

  1. Сообщения сообщества включены
    Перейдите в Управление сообществом → Сообщения → включите опцию «Сообщения сообщества». Без этого бот просто не сможет отправлять или получать сообщения, а значит, и клавиатуры не появятся.

    Включение сообщений сообщества

  2. Токен с нужными правами
    В первой части мы создавали ключ доступа. Убедитесь, что при его создании вы отметили право «Сообщения сообщества» (и, если планируете использовать бота в беседах, также «Доступ к сообщениям»). Если токен был создан без этих прав, клавиатуры могут не отправляться.

  3. Кнопка «Начать» (опционально)
    В настройках сообщества есть раздел «Сообщения» → «Настройка для бота». Там можно включить кнопку «Начать». Она позволяет пользователю инициировать диалог одним касанием, после чего бот может сразу показать приветственное меню. Клавиатура будет работать и без этой кнопки, но с ней пользовательский опыт становится плавнее.

    включение возможностей ботов

Создание обычной (встроенной) клавиатуры

Доработка функции отправки сообщения

Обычная клавиатура — это панель с кнопками, которая появляется вместо стандартной клавиатуры телефона внизу экрана. Она остаётся на месте (если не установлен флаг one_time) и позволяет пользователю взаимодействовать с ботом без набора текста. Чтобы отправить такую клавиатуру, нужно при вызове метода messages.send передать параметр keyboard — JSON-строку определённой структуры.

Доработаем нашу функцию отправки сообщения:


// Добавим в параметры необязательный аргумент - клавиатуру
function sendMessage($peerId, $message, $keyboard = null)
        
        
{
    $params = [
        'peer_id' => $peerId,
        'message' => $message,
        'access_token' => ACCESS_TOKEN,
        'v' => VERSION,
        'random_id' => rand(1, 1000000),
    ];
        
        
    // Если аргумент передан, добавляем новое поле
    if ($keyboard) {
        // Обязательно сериализуем в json
        $params['keyboard'] = json_encode($keyboard);
    }
        
        
    $url = ENDPOINT . 'messages.send?' . http_build_query($params);

    $response = file_get_contents($url);

    return json_decode($response);
}
      

# Добавим в параметры необязательный аргумент - клавиатуру
def send_message(peer_id, message, keyboard = None):
        
        
    params = {
        'peer_id': peer_id,
        'message': message,
        'access_token': ACCESS_TOKEN,
        'v': VERSION,
        'random_id': random.randint(1, 1000000)
    }
        
        
    # Если аргумент передан, добавляем новое поле
    if keyboard:
        # Обязательно сериализуем в json
        params['keyboard'] = json.dumps(keyboard, ensure_ascii=False)
        
        
    url = ENDPOINT + 'messages.send'
    
    response = requests.get(url, params=params)

    return response.json()
      

// Добавим в параметры необязательный аргумент - клавиатуру
async function sendMessage(peerId, message, keyboard) {
        
        
  const params = {
    peer_id: peerId,
    message: message,
    access_token: ACCESS_TOKEN,
    v: VERSION,
    random_id: Math.floor(Math.random() * 1000000) + 1,
  };
        
        
  // Если аргумент передан, добавляем новое поле
  if (keyboard) {
    // Обязательно сериализуем в json
    params.keyboard = JSON.stringify(keyboard);
  }
        
        
  const url = ENDPOINT + "messages.send?" + new URLSearchParams(params).toString();

  const response = await fetch(url);

  return response.json();
}
      

// Классы для типизации разметки клавиатуры
public class KeyboardMarkup
{
    [JsonPropertyName("one_time")]
    public bool OneTime { get; set; }
    [JsonPropertyName("buttons")]
    public KeyboardMarkupButton[][] Buttons { get; set; }
}
public class KeyboardMarkupButton
{
    [JsonPropertyName("action")]
    public KeyboardMarkupButtonAction Action { get; set; }
    [JsonPropertyName("color")]
    public string Color { get; set; }
}
public class KeyboardMarkupButtonAction
{
    [JsonPropertyName("type")]
    public string Type { get; set; }
    [JsonPropertyName("label")]
    public string Label { get; set; }
    [JsonPropertyName("payload")]
    public string? Payload { get; set; }
}

// Добавим в параметры необязательный аргумент - клавиатуру
public static async Task<VkResponse?> SendMessage(long peerId, string message, KeyboardMarkup? keyboard = null)
        
        
{
    Dictionary<string, string> parameters = new Dictionary<string, string>
    {
        ["peer_id"] = peerId.ToString(),
        ["message"] = message,
        ["access_token"] = ACCESS_TOKEN,
        ["v"] = VERSION,
        ["random_id"] = new Random().Next(1, 1000000).ToString() // обязательно для отправки
    };
        
        
    // Если аргумент передан, добавляем новое поле
    if (keyboard is not null)
    {
        // Обязательно сериализуем в json
        parameters.Add("keyboard", JsonSerializer.Serialize(keyboard));
    }
        
        
    string url = ENDPOINT + "messages.send?" + string.Join("&", parameters.Select(p => $"{HttpUtility.UrlEncode(p.Key)}={HttpUtility.UrlEncode(p.Value)}"));

    HttpClient httpClient = new HttpClient();
    HttpResponseMessage response = await httpClient.GetAsync(url);
    return await response.Content.ReadFromJsonAsync<VkResponse>();
}
      

Формат разметки клавиатуры

Итак, мы подготовили функцию отправки сообщения, которая умеет принимать клавиатуру. Теперь нужно понять, в каком формате передавать в неё разметку. VK API ожидает JSON-объект строго определённой структуры. Рассмотрим её:

        
{
    "one_time": "true/false",
    "inline": "true/false",
    "buttons": [
        [
            {
                "action": {
                    "type": "ТИП_КНОПКИ",
                    "label": "ТЕКСТ_КНОПКИ",
                    "payload": "ЛЮБЫЕ_ДАННЫЕ",
                },
                "color": "ВАРИАНТ_ЦВЕТА",
            }
        ],
        [
            {
                "action": {
                    "type": "ТИП_КНОПКИ",
                    "label": "ТЕКСТ_КНОПКИ",
                },
                "color": "ВАРИАНТ_ЦВЕТА",
            }
        ],
    ],
}
        
    

Описание полей

Поля разметки
Поле Тип Обязательное Описание
one_time boolean да Если true — клавиатура исчезает после первого нажатия (скрывается). Если false — остаётся на месте.
inline boolean да false — обычная клавиатура (вместо системной клавиатуры). true — inline-клавиатура (крепится к сообщению).
buttons массив да Массив рядов. Каждый ряд — массив кнопок. Максимум 10 рядов, в ряду до 4 кнопок (для обычной клавиатуры) или до 5 (для inline).
Поля кнопки
Поле Тип Описание
action объект Описывает действие кнопки.
color строка Цвет кнопки (только для обычной клавиатуры, у inline цвет не поддерживается).
Объект action
Поле Тип Описание
type строка Тип действия. Для текстовых кнопок обычной клавиатуры — "text". Для inline есть и другие (callback, open_link и т.д.).
label строка Текст на кнопке (до 40 символов).
payload (необязательное) строка Произвольные данные в формате JSON-строки (до 255 символов). Возвращается при нажатии и помогает идентифицировать кнопку.
Цвета кнопок (для обычной клавиатуры)
Цвет Назначение
primary Синий, главное действие.
secondary Белый, второстепенное действие.
negative Красный, опасное действие (удалить, отмена).
positive Зелёный, подтверждение (оплатить, согласиться).

Поле color не используется для inline-клавиатур — их внешний вид определяется системой.

Отправляем кнопки пользователю

Теперь, зная структуру, мы можем сформировать массив данных кнопок, которые нам нужны и отправить их пользователю.
Давайте в нашем обработчике сообщений создадим массив из трех кнопок. Две будут вверху, третья внизу на всю ширину:


switch ($data['type']) {
    case 'confirmation':
        echo CONFIRMATION_TOKEN;
        exit();

    // Добавляем обработку нового сообщения
    case 'message_new':
        
        // Формируем клавиатуру
        $keyboardData = [
            "one_time" => false,
            "buttons" => [
                [
                    [
                        "action" => [
                            "type" => "text",
                            "label" => "Кнопка 1 от PHP",
                            "payload" => ["button" => 1]
                        ],
                        "color" => "primary"
                    ],
                    [
                        "action" => [
                            "type" => "text",
                            "label" => "Кнопка 2 от PHP",
                            "payload" => ["button" => 2]
                        ],
                        "color" => "secondary"
                    ]
                ],
                [
                    [
                        "action" => [
                            "type" => "text",
                            "label" => "Кнопка 3 от PHP"
                        ],
                        "color" => "negative"
                    ]
                ]
            ]
        ];
        // Отправляем клавиатуру в функцию
        sendMessage($data['object']['message']['peer_id'], 'Сообщение с PHP сервера', $keyboardData);
        
        
        http_response_code(200);
        exit('ok');
    default:
        http_response_code(200);
        exit('ok');
}
      

if data['type'] == 'confirmation':
        return CONFIRMATION_TOKEN, 200
        
# Добавляем обработку нового сообщения
if data['type'] == 'message_new':
    
    # Формируем клавиатуру
    keyboard_data = {
        "one_time" : False,
        "buttons" : [
            [
                {
                    "action" : {
                        "type" : "text",
                        "label" : "Кнопка 1 Python",
                        "payload" : { "button": 1 },
                    },
                    "color" : "primary"
                },
                {
                    "action" : {
                        "type" : "text",
                        "label" : "Кнопка 2 Python",
                        "payload" : { "button": 2 },
                    },
                    "color" : "secondary"
                }
            ],
            [
                {
                    "action" : {
                        "type" : "text",
                        "label" : "Кнопка 3 Python"
                    },
                    "color" : "negative"
                }
            ]
        ]
    }
    # Отправляем клавиатуру в функцию
    send_message(data['object']['message']['peer_id'], 'Сообщение с Python сервера', keyboard_data)
    
    return 'ok', 200

return 'ok', 200
      

switch (data.type) {
  case "confirmation":
    return res.send(CONFIRMATION_TOKEN);
  // Добавляем обработку нового сообщения
  case "message_new":
  
    // Формируем клавиатуру
    const keyboardData = {
      one_time: false,
      buttons: [
        [
          {
            action: {
              type: "text",
              label: "Кнопка 1 от JS",
              payload: { button: 1 },
            },
            color: "primary",
          },
          {
            action: {
              type: "text",
              label: "Кнопка 2 JS",
              payload: { button: 2 },
            },
            color: "secondary",
          },
        ],
        [
          {
            action: {
              type: "text",
              label: "Кнопка 3 JS",
            },
            color: "negative",
          },
        ],
      ],
    };
    // Отправляем клавиатуру в функцию
    sendMessage(data.object.message["peer_id"], "Сообщение с JS сервера", keyboardData);
    
    return res.status(200).send("ok");
  default:
    return res.status(200).send("ok");
}
      

// Класс для типизации payload
public class KeyboardMarkupButtonPayload
{
    [JsonPropertyName("button")]
    public int Button { get; set; }
}

switch (data.Type)
{
    case "confirmation":
        return Results.Ok(CONFIRMATION_TOKEN);

    // Добавляем обработку нового сообщения
    case "message_new":
        // Формируем клавиатуру
        KeyboardMarkup markup = new KeyboardMarkup()
        {
            OneTime = false,
            Buttons = new KeyboardMarkupButton[2][]
            {
                new KeyboardMarkupButton[2]
                {
                    new KeyboardMarkupButton()
                    {
                        Action = new KeyboardMarkupButtonAction()
                        {
                            Type = "text",
                            Label = "Кнопка 1 C#",
                            Payload = new KeyboardMarkupButtonPayload()
                            {
                                Button = 1
                            }
                        },
                        Color = "primary"
                    },
                    new KeyboardMarkupButton()
                    {
                        Action = new KeyboardMarkupButtonAction()
                        {
                            Type = "text",
                            Label = "Кнопка 2 C#",
                            Payload = new KeyboardMarkupButtonPayload()
                            {
                                Button = 2
                            }
                        },
                        Color = "secondary"
                    }
                },
                new KeyboardMarkupButton[1]
                {
                    new KeyboardMarkupButton()
                    {
                        Action = new KeyboardMarkupButtonAction()
                        {
                            Type = "text",
                            Label = "Кнопка 3 C#"
                        },
                        Color = "negative"
                    }
                }
            }
        };
        // Отправляем клавиатуру в функцию
        await SendMessage(data.Object.Message.PeerId, "Сообщение с C# сервера", markup);

        return Results.Ok("ok");
    default:
        return Results.Ok("ok");
}
      

Теперь, когда нам придет любое новое сообщение, мы будем отправлять клавиатуру. Мы разметили ее таким образом, что так как в первом подмассиве у нас две кнопки, то они поделять место в первом ряду, а третья кнопка, так как находится во втором подмассиве и она там одна, растянется на всю ширину второго ряда.

Вот результат:

Результат отправки кнопок от php сервераРезультат отправки кнопок от python сервераРезультат отправки кнопок от javascript сервераРезультат отправки кнопок от C# сервера

Давайте теперь посмотрим, что будет если их нажать

Результаты будут по кнопкам C# сервера, но они идентичны остальным:


{
    "group_id": 236815260,
    "type": "message_new",
    "event_id": "a9dfe1339e157429895e8cb8b1e4b0a7a6dc45e3",
    "v": "5.199",
    "object": {
        "client_info": {
            "button_actions": [
                "text",
                "vkpay",
                "open_app",
                "location",
                "open_link",
                "open_photo",
                "callback",
                "intent_subscribe",
                "intent_unsubscribe"
            ],
            "keyboard": true,
            "inline_keyboard": true,
            "carousel": true,
            "lang_id": 0
        },
        "message": {
            "date": 1774033735,
            "from_id": 123052131,
            "id": 46,
            "version": 10000117,
            "out": 0,
            "fwd_messages": [],
            "important": false,
            "is_hidden": false,
            "attachments": [],
            "conversation_message_id": 46,
            "payload": "{\"button\":1}",
            "text": "Кнопка 1 C#",
            "peer_id": 123052131,
            "random_id": 0
        }
    }
}
      

{
    "group_id": 236815260,
    "type": "message_new",
    "event_id": "ca2b9e428953f7d37c285f6eff81a00be2c693ff",
    "v": "5.199",
    "object": {
        "client_info": {
            "button_actions": [
                "text",
                "vkpay",
                "open_app",
                "location",
                "open_link",
                "open_photo",
                "callback",
                "intent_subscribe",
                "intent_unsubscribe"
            ],
            "keyboard": true,
            "inline_keyboard": true,
            "carousel": true,
            "lang_id": 0
        },
        "message": {
            "date": 1774033736,
            "from_id": 123052131,
            "id": 47,
            "version": 10000118,
            "out": 0,
            "fwd_messages": [],
            "important": false,
            "is_hidden": false,
            "attachments": [],
            "conversation_message_id": 47,
            "payload": "{\"button\":2}",
            "text": "Кнопка 2 C#",
            "peer_id": 123052131,
            "random_id": 0
        }
    }
}
      

{
    "group_id": 236815260,
    "type": "message_new",
    "event_id": "3a019ce59dd88b07eb0608179cdae5da4a3f5537",
    "v": "5.199",
    "object": {
        "client_info": {
            "button_actions": [
                "text",
                "vkpay",
                "open_app",
                "location",
                "open_link",
                "open_photo",
                "callback",
                "intent_subscribe",
                "intent_unsubscribe"
            ],
            "keyboard": true,
            "inline_keyboard": true,
            "carousel": true,
            "lang_id": 0
        },
        "message": {
            "date": 1774033691,
            "from_id": 123052131,
            "id": 45,
            "version": 10000116,
            "out": 0,
            "fwd_messages": [],
            "important": false,
            "is_hidden": false,
            "attachments": [],
            "conversation_message_id": 45,
            "text": "Кнопка 3 C#",
            "peer_id": 123052131,
            "random_id": 0
        }
    }
}
      

Обратите внимание на то, что прожатые кнопки инициируют событие "message_new", а внутри них лежат данные в том же формате, что мы описывали ранее, с типом, группой, откуда пришло сообщение, но также, там лежат:

  • В поле object -> message -> text текст нашей кнопки.
    Он лежит там, так как нажатая кнопка переносит свой текст как сообщение и отпрвляет его

  • В поле object -> message -> payload лежит то что мы отправили в payload нашей кнопки в клавиатуре

Заключение

Мы разобрали первый тип клавиатур, научились создавать кнопки с разными цветами и обрабатывать нажатия. Теперь ваш бот стал не просто отвечающим, а интерактивным: пользователь может выбирать действия одним касанием, не вводя команды вручную.

Что мы сделали:

  1. Доработали функцию отправки сообщений, добавив поддержку клавиатур.

  2. Изучили структуру JSON для обычной клавиатуры.

  3. Настроили цвета и обработку payload.

  4. Проверили, как выглядят нажатия кнопок в событиях.

Клавиатуры — важный шаг к удобному боту, но впереди ещё больше. В следующей части мы научимся работать с inline-клавиатурой.

Получить коммерческое предложение

Оставьте заявку, и мы свяжемся с вами, чтобы обсудить ваши потребности и предоставить коммерческое предложение по разработке сайта. Наша команда поможет вам создать эффективный и привлекательный веб-сайт, адаптированный к вашему бизнесу.

Я не робот

Нажмите чтобы продолжить

*Согласие обязательно
Я ознакомлен с политикой конфиденциальности и согласен на обработку персональных данных
Оставьтезаявку
Кнопка закрытия формы
Стрелка вверх
Все получилось Заявкауспешно
отправлена
Спасибо, что доверяете нам! Наш специалист уже получил вашу заявку
и скоро свяжется с вами.
Кнопка закрытия формы