English 中文 Español Deutsch 日本語
preview
MQL5-советник, интегрированный в Telegram (Часть 6): Добавление адаптивных встроенных кнопок

MQL5-советник, интегрированный в Telegram (Часть 6): Добавление адаптивных встроенных кнопок

MetaTrader 5Торговые системы | 7 апреля 2025, 13:38
598 4
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В этой статье мы сделаем наш Telegram-советник на основе MetaQuotes Language 5 (MQL5) более отзывчивым и интерактивным. В пятой части серии мы заложили основу для нашего бота, реализовав возможность отвечать на команды и сообщения из Telegram, а также создав пользовательские кнопки клавиатуры. В этом сегменте мы повышаем интерактивность нашего бота, интегрируя встроенные кнопки, которые запускают различные действия и динамически реагируют на действия пользователя.

В статье рассмотрены несколько ключевых компонентов. Во-первых, мы добавим встроенные кнопки в Telegram-ботов, подчеркнув их функционал и преимущества по сравнению с другими методами создания интерфейса бота. Затем мы перейдем к обсуждению того, как использовать эти встроенные кнопки в MQL5, чтобы они могли стать частью пользовательского интерфейса нашего советника. 

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

  1. Введение во встроенные кнопки в Telegram-ботах
  2. Интеграция встроенных кнопок в MQL5
  3. Обработка запросов обратного вызова для действий кнопок
  4. Тестирование реализации состояний встроенных кнопок
  5. Заключение

К концу этой статьи вы получите четкое представление о том, как интегрировать встроенные кнопки и управлять ими в вашем MQL5-советнике для Telegram, расширяя функционал вашего торгового бота и делая его более отзывчивым и интерактивным для пользователей.


Введение во встроенные кнопки в Telegram-ботах

Встроенные кнопки — это интерактивные элементы, которые появляются непосредственно в сообщениях бота Telegram, позволяя пользователям выполнять действия одним нажатием. Они используют разметку встроенной клавиатуры в формате JavaScript Object Notation (JSON), который определяет их внешний вид и поведение, предлагая более интегрированный и отзывчивый интерфейс по сравнению с традиционными методами. Встраивая эти кнопки непосредственно в сообщения, боты могут предоставить пользователям удобный интерфейс и мгновенное взаимодействие без необходимости в дополнительных текстовых командах или сообщениях. Встроенные кнопки показаны ниже:

Встроенные кнопки

Основное преимущество встроенных кнопок перед традиционными клавиатурами для ответа заключается в их способности оставаться внутри самого сообщения, что делает взаимодействие более плавным и привязанным к контексту. Встроенные кнопки, определяемые с помощью структуры JSON, обеспечивают сложное взаимодействие с пользователем и динамические ответы. Такой подход устраняет необходимость в отдельных меню или дополнительных сообщениях, тем самым уменьшая беспорядок и повышая вовлеченность пользователей за счет мгновенной обратной связи и действий. Начнем реализацию в MQL5 для MetaTrader 5.


Интеграция встроенных кнопок в MQL5

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

//+------------------------------------------------------------------+
//|        Class_Message                                             |
//+------------------------------------------------------------------+
class Class_Message : public CObject {
public:
    Class_Message(); // Constructor
    ~Class_Message(){}; // Destructor
    
    bool              done; //--- Indicates if a message has been processed.
    long              update_id; //--- Stores the update ID from Telegram.
    long              message_id; //--- Stores the message ID.
    long              from_id; //--- Stores the sender’s ID.
    string            from_first_name; //--- Stores the sender’s first name.
    string            from_last_name; //--- Stores the sender’s last name.
    string            from_username; //--- Stores the sender’s username.
    long              chat_id; //--- Stores the chat ID.
    string            chat_first_name; //--- Stores the chat’s first name.
    string            chat_last_name; //--- Stores the chat’s last name.
    string            chat_username; //--- Stores the chat’s username.
    string            chat_type; //--- Stores the chat type.
    datetime          message_date; //--- Stores the date of the message.
    string            message_text; //--- Stores the text of the message.
};

Здесь мы определяем класс Class_Message, который служит контейнером для всех соответствующих сведений о сообщениях, полученных от Telegram. Класс необходим для управления и обработки данных сообщений в нашем MQL5-советнике.

В этот класс мы включаем несколько публичных атрибутов, которые фиксируют определенные аспекты сообщения. Атрибут done указывает, было ли обработано сообщение. Атрибуты update_id и message_id хранят уникальные идентификаторы обновления и сообщения соответственно. Атрибуты from_id, from_first_name, from_last_name и from_username содержат информацию об отправителе сообщения. Аналогично, chat_id, chat_first_name, chat_last_name, chat_username и chat_type собирают сведения о чате, в который было отправлено сообщение. Атрибут message_date записывает дату и время сообщения, а message_text хранит фактическое содержимое сообщения. После определения членов класса мы можем приступить к инициализации членов нашего класса.

//+------------------------------------------------------------------+
//|      Constructor to initialize class members                     |
//+------------------------------------------------------------------+
Class_Message::Class_Message(void) {
    done = false; //--- Sets default value indicating message not yet processed.
    update_id = 0; //--- Initializes update ID to zero.
    message_id = 0; //--- Initializes message ID to zero.
    from_id = 0; //--- Initializes sender ID to zero.
    from_first_name = NULL; //--- Sets sender's first name to NULL (empty).
    from_last_name = NULL; //--- Sets sender's last name to NULL (empty).
    from_username = NULL; //--- Sets sender's username to NULL (empty).
    chat_id = 0; //--- Initializes chat ID to zero.
    chat_first_name = NULL; //--- Sets chat’s first name to NULL (empty).
    chat_last_name = NULL; //--- Sets chat’s last name to NULL (empty).
    chat_username = NULL; //--- Sets chat’s username to NULL (empty).
    chat_type = NULL; //--- Sets chat type to NULL (empty).
    message_date = 0; //--- Initializes message date to zero.
    message_text = NULL; //--- Sets message text to NULL (empty).
}

Здесь мы инициализируем конструктор Class_Message, чтобы задать значения по умолчанию для всех атрибутов класса. Атрибут done равен false, что указывает на то, что сообщение не было обработано. Мы инициализируем update_id, message_id, from_id и chat_id значением 0, значения from_first_name, from_last_name, from_username, chat_first_name, chat_last_name, chat_username и chat_type устанавливаются на NULL, чтобы указать, что эти поля пусты. Наконец, message_date устанавливается равным 0, а message_text инициализируется NULL, гарантируя, что каждый новый экземпляр Class_Message начинается со значений по умолчанию, прежде чем будет заполнен фактическими данными. Используя ту же логику, мы определяем класс чата, в котором будем хранить данные об обновлениях чата, как показано ниже:

//+------------------------------------------------------------------+
//|        Class_Chat                                                |
//+------------------------------------------------------------------+
class Class_Chat : public CObject {
public:
    Class_Chat(){}; //--- Declares an empty constructor.
    ~Class_Chat(){}; //--- Declares an empty destructor.
    long              member_id; //--- Stores the chat ID.
    int               member_state; //--- Stores the state of the chat.
    datetime          member_time; //--- Stores the time of chat activities.
    Class_Message     member_last; //--- Instance of Class_Message to store the last message.
    Class_Message     member_new_one; //--- Instance of Class_Message to store the new message.
};

После определения класса чатов нам необходимо определить дополнительный класс, который будет обрабатывать запросы обратного вызова. Это будет необходимо для обработки конкретных данных, связанных с запросами обратного вызова, которые отличаются от обычных сообщений. Callback-запросы предоставляют уникальные данные, такие как данные обратного вызова и взаимодействие, вызвавшее запрос, которые отсутствуют в стандартных сообщениях. Таким образом, этот класс позволит нам эффективно собирать и управлять этими специализированными данными. Более того, это позволит нам обрабатывать взаимодействие пользователя с встроенными кнопками особым образом. Такое разделение позволит нам точно обрабатывать нажатия кнопок и реагировать на них, отличая их от других типов сообщений и взаимодействий. Реализация будет следующей:

//+------------------------------------------------------------------+
//|        Class_CallbackQuery                                       |
//+------------------------------------------------------------------+
class Class_CallbackQuery : public CObject {
public:
    string            id; //--- Stores the callback query ID.
    long              from_id; //--- Stores the sender’s ID.
    string            from_first_name; //--- Stores the sender’s first name.
    string            from_last_name; //--- Stores the sender’s last name.
    string            from_username; //--- Stores the sender’s username.
    long              message_id; //--- Stores the message ID related to the callback.
    string            message_text; //--- Stores the message text.
    string            data; //--- Stores the callback data.
    long              chat_id; //--- Stores the chat ID to send responses.
};

Здесь мы определяем класс Class_CallbackQuery для управления данными, связанными с запросами обратного вызова из Telegram. Этот класс имеет решающее значение для обработки взаимодействий со встроенными кнопками. В классе мы объявляем различные переменные для хранения информации, специфичной для запросов обратного вызова. Переменная id содержит уникальный идентификатор запроса обратного вызова, что позволяет нам различать разные запросы. from_id хранит идентификатор отправителя, который помогает идентифицировать пользователя, инициировавшего обратный вызов. Мы используем from_first_name, from_last_name и from_username для отслеживания данных имени отправителя.

Переменная message_id фиксирует идентификатор сообщения, связанного с обратным вызовом, а message_text содержит текст этого сообщения. data содержит данные обратного вызова, отправленные с помощью встроенной кнопки, что имеет решающее значение для определения действия, которое следует выполнить на основе нажатой кнопки. Наконец, chat_id хранит идентификатор чата, куда следует отправлять ответы, гарантируя, что ответ попадет в правильный контекст чата. Остальная часть определения и инициализации класса Expert остается прежней, за исключением того, что теперь нам нужно включить дополнительную пользовательскую функцию для обработки запросов обратного вызова.

//+------------------------------------------------------------------+
//|        Class_Bot_EA                                              |
//+------------------------------------------------------------------+
class Class_Bot_EA {
private:
    string            member_token; //--- Stores the bot’s token.
    string            member_name; //--- Stores the bot’s name.
    long              member_update_id; //--- Stores the last update ID processed by the bot.
    CArrayString      member_users_filter; //--- Array to filter messages from specific users.
    bool              member_first_remove; //--- Indicates if the first message should be removed.
    
protected:
    CList             member_chats; //--- List to store chat objects.

public:
    Class_Bot_EA(); //--- Constructor.
    ~Class_Bot_EA(){}; //--- Destructor.
    int getChatUpdates(); //--- Function to get updates from Telegram.
    void ProcessMessages(); //--- Function to process incoming messages.
    void ProcessCallbackQuery(Class_CallbackQuery &cb_query); //--- Function to process callback queries.
};

//+------------------------------------------------------------------+
//|   Constructor for Class_Bot_EA                                   |
//+------------------------------------------------------------------+
Class_Bot_EA::Class_Bot_EA(void) {
    member_token = NULL; //--- Initialize bot token to NULL (empty).
    member_token = getTrimmedToken(InpToken); //--- Assign bot token by trimming input token.
    member_name = NULL; //--- Initialize bot name to NULL.
    member_update_id = 0; //--- Initialize last update ID to zero.
    member_first_remove = true; //--- Set first remove flag to true.
    member_chats.Clear(); //--- Clear the list of chat objects.
    member_users_filter.Clear(); //--- Clear the user filter array.
}

Определив все необходимые классы, мы можем перейти к получению сведений об обновлении чата.

//+------------------------------------------------------------------+
//|   Function to get chat updates from Telegram                     |
//+------------------------------------------------------------------+
int Class_Bot_EA::getChatUpdates(void) {

    //... 

    return 0; //--- Return 0 to indicate successful processing of updates.
}

Здесь мы определяем функцию getChatUpdates как метод класса Class_Bot_EA. Функция предназначена для извлечения обновлений из API Telegram: обновлений, которые состоят либо из новых сообщений, либо из запросов обратного вызова, которые бот еще не обработал. Текущая реализация getChatUpdates возвращает целочисленное значение 0, что условно означает, что операция завершена успешно. Возвращая 0, мы сигнализируем, что мы извлекли обновления и обработали их, не столкнувшись с какими-либо проблемами. Следующим шагом для нас будет заполнение этой функции так, чтобы она выполняла то, для чего она предназначена: извлекала обновления из API.

    if (member_token == NULL) { //--- If bot token is empty
        Print("ERR: TOKEN EMPTY"); //--- Print error message indicating empty token.
        return (-1); //--- Return -1 to indicate error.
    }

Мы определяем, является ли переменная member_token пустой. Если member_token равен NULL, это означает, что мы не получили токен бота. Поэтому мы информируем пользователя, печатая ERR: TOKEN EMPTY, что необходимая часть информации не была предоставлена, и возвращаем -1, чтобы сообщить об ошибке, которая останавливает дальнейшую работу функции. Если мы пройдем этот шаг, мы сможем продолжить отправку запроса на получение обновлений чата.

    string out; //--- String to hold response data.
    string url = TELEGRAM_BASE_URL + "/bot" + member_token + "/getUpdates"; //--- Construct URL for Telegram API.
    string params = "offset=" + IntegerToString(member_update_id); //--- Set parameters including the offset based on the last update ID.
    
    int res = postRequest(out, url, params, WEB_TIMEOUT); //--- Send a POST request to Telegram with a timeout.

Во-первых, мы определяем строковую переменную out. Настроим переменную out для хранения данных ответа, которые мы получим от API Telegram. Затем мы создаем URL-адрес API, необходимый для получения обновлений. Для этого мы объединяем TELEGRAM_BASE_URL с несколькими другими компонентами: /bot и токеном для бота, который хранится в member_token, а также /getUpdates - конечной точкой, к которой мы обращаемся для получения обновлений от Telegram. API Telegram является частью другой основной платформы, которую использует наше приложение, а метод getUpdates — это способ, с помощью которого мы извлекаем новые данные с этой платформы. Затем мы переходим к вызову API и позволяем API возвращать нам новые данные из нашего приложения. Затем мы можем использовать полученные результаты для внесения дальнейших поправок.

    if (res == 0) { //--- If request succeeds (res = 0)
        CJSONValue obj_json(NULL, jv_UNDEF); //--- Create a JSON object to parse the response.
        bool done = obj_json.Deserialize(out); //--- Deserialize the response.
        if (!done) { //--- If deserialization fails
            Print("ERR: JSON PARSING"); //--- Print error message indicating JSON parsing error.
            return (-1); //--- Return -1 to indicate error.
        }
        
        bool ok = obj_json["ok"].ToBool(); //--- Check if the response has "ok" field set to true.
        if (!ok) { //--- If "ok" field is false
            Print("ERR: JSON NOT OK"); //--- Print error message indicating that JSON response is not okay.
            return (-1); //--- Return -1 to indicate error.
        }
    }

Начнем с определения успешности запроса, проверив значение переменной res. Если res равен 0, мы знаем, что запрос выполнен успешно, и можем продолжить обработку ответа. Создадим объект CJSONValue - obj_json - для анализа ответа. Объект инициализируется в состоянии NULL и с jv_UNDEF, что обозначает неопределенное состояние или готовность объекта к приему некоторых данных. После анализа с out мы получаем объект, содержащий проанализированные данные, или ошибку.

Если десериализация не удалась (на это указывает переменная done, равная false), мы выводим сообщение об ошибке ERR: JSON PARSING и возвращаем -1, чтобы сообщить о проблеме. Если мы успешно десериализуем данные, мы проверяем, содержит ли ответ поле с именем "ok". Мы преобразуем его в логическое значение, используя метод ToBool, и сохраняем результат в переменной "ok". Если "ok" равен false, то есть запрос не был выполнен успешно на стороне сервера, выводим "ERR: JSON NOT OK" и возвращаем -1. Таким образом, мы гарантируем правильную обработку как десериализации ответа, так и его содержимого. Затем мы продолжаем итерацию каждого ответа, используя следующую логику.

        int total = ArraySize(obj_json["result"].m_elements); //--- Get the total number of update elements.
        for (int i = 0; i < total; i++) { //--- Iterate through each update element.
            CJSONValue obj_item = obj_json["result"].m_elements[i]; //--- Access individual update element.
            
            if (obj_item["message"].m_type != jv_UNDEF) { //--- Check if the update has a message.
                Class_Message obj_msg; //--- Create an instance of Class_Message to store the message details.
                obj_msg.update_id = obj_item["update_id"].ToInt(); //--- Extract and store update ID.
                obj_msg.message_id = obj_item["message"]["message_id"].ToInt(); //--- Extract and store message ID.
                obj_msg.message_date = (datetime)obj_item["message"]["date"].ToInt(); //--- Extract and store message date.
                obj_msg.message_text = obj_item["message"]["text"].ToStr(); //--- Extract and store message text.
                obj_msg.message_text = decodeStringCharacters(obj_msg.message_text); //--- Decode any special characters in the message text.
            }
        }

Чтобы начать проверку общего количества элементов обновления в ответе, мы используем функцию ArraySize для подсчета элементов в массиве m_elements объекта result в obj_json. Количество сохраняется в переменной total. Далее мы настраиваем цикл, который повторно обрабатывает каждый элемент обновления из массива m_elements. Необходимо обработать total элементов. Таким образом, переменная управления циклом находится в диапазоне от 0 до total минус 1. Во время каждой итерации текущее значение переменной управления циклом i указывает, к какому элементу массива m_elements мы обращаемся. Присваиваем i-й элемент переменной obj_item. Теперь мы проверяем, содержит ли текущее обновление (obj_item) допустимое сообщение (message).

Далее мы создаем объект Class_Message с именем obj_msg, который будет содержать сведения о рассматриваемом сообщении. Первое поле, которое мы заполняем в obj_msg — это поле update_id. Для этого мы извлекаем update_id из obj_item, преобразуем его в целое число и помещаем в obj_msg.update_id. Следующее поле, к которому мы обращаемся в obj_msg — это его поле message_id. Для этого значения мы снова извлекаем информацию из поля message в obj_item. Мы преобразуем значение поля message_id в obj_item в целое число и помещаем его в obj_msg.message_id. После этого мы заполняем поле datetime в obj_msg значением date - item. После этого мы заполняем поле message_text в obj_msg. Мы извлекаем значение text из message, преобразуем его в строку и помещаем в obj_msg.message_text. Наконец, мы используем функцию decodeStringCharacters, чтобы гарантировать, что любые специальные символы в message_text будут отображаться правильно. Аналогичный подход используется для получения других деталей ответа. 

                obj_msg.from_id = obj_item["message"]["from"]["id"].ToInt(); //--- Extract and store the sender's ID.
                obj_msg.from_first_name = obj_item["message"]["from"]["first_name"].ToStr(); //--- Extract and store the sender's first name.
                obj_msg.from_first_name = decodeStringCharacters(obj_msg.from_first_name); //--- Decode any special characters in the sender's first name.
                obj_msg.from_last_name = obj_item["message"]["from"]["last_name"].ToStr(); //--- Extract and store the sender's last name.
                obj_msg.from_last_name = decodeStringCharacters(obj_msg.from_last_name); //--- Decode any special characters in the sender's last name.
                obj_msg.from_username = obj_item["message"]["from"]["username"].ToStr(); //--- Extract and store the sender's username.
                obj_msg.from_username = decodeStringCharacters(obj_msg.from_username); //--- Decode any special characters in the sender's username.
                
                obj_msg.chat_id = obj_item["message"]["chat"]["id"].ToInt(); //--- Extract and store the chat ID.
                obj_msg.chat_first_name = obj_item["message"]["chat"]["first_name"].ToStr(); //--- Extract and store the chat's first name.
                obj_msg.chat_first_name = decodeStringCharacters(obj_msg.chat_first_name); //--- Decode any special characters in the chat's first name.
                obj_msg.chat_last_name = obj_item["message"]["chat"]["last_name"].ToStr(); //--- Extract and store the chat's last name.
                obj_msg.chat_last_name = decodeStringCharacters(obj_msg.chat_last_name); //--- Decode any special characters in the chat's last name.
                obj_msg.chat_username = obj_item["message"]["chat"]["username"].ToStr(); //--- Extract and store the chat's username.
                obj_msg.chat_username = decodeStringCharacters(obj_msg.chat_username); //--- Decode any special characters in the chat's username.
                obj_msg.chat_type = obj_item["message"]["chat"]["type"].ToStr(); //--- Extract and store the chat type.

Получив данные чата, мы приступаем к обработке сообщения на основе связанного с ним идентификатора чата.

                //--- Process the message based on chat ID.
                member_update_id = obj_msg.update_id + 1; //--- Update the last processed update ID.

После извлечения и сохранения необходимых данных сообщения мы обновляем идентификатор последнего обработанного обновления. Это достигается путем присвоения значения obj_msg.update_id плюс 1 переменной member_update_id. Это гарантирует, что при следующей обработке обновлений мы сможем пропустить это обновление и продолжить со следующего. Наконец, нам необходимо применить сортировку сообщений пользователей.

                //--- Check if we need to filter messages based on user or if no filter is applied.
                if (member_users_filter.Total() == 0 ||
                    (member_users_filter.Total() > 0 &&
                    member_users_filter.SearchLinear(obj_msg.from_username) >= 0)) {
                    
                    int index = -1; //--- Initialize index to -1 (indicating no chat found).
                    for (int j = 0; j < member_chats.Total(); j++) { //--- Iterate through all chat objects.
                        Class_Chat *chat = member_chats.GetNodeAtIndex(j); //--- Get chat object by index.
                        if (chat.member_id == obj_msg.chat_id) { //--- If chat ID matches
                            index = j; //--- Store the index.
                            break; //--- Break the loop since we found the chat.
                        }
                    }

                    if (index == -1) { //--- If no matching chat was found
                        member_chats.Add(new Class_Chat); //--- Create a new chat object and add it to the list.
                        Class_Chat *chat = member_chats.GetLastNode(); //--- Get the last (newly added) chat object.
                        chat.member_id = obj_msg.chat_id; //--- Assign the chat ID.
                        chat.member_time = TimeLocal(); //--- Record the current time for the chat.
                        chat.member_state = 0; //--- Initialize the chat state to 0.
                        chat.member_new_one.message_text = obj_msg.message_text; //--- Store the new message in the chat.
                        chat.member_new_one.done = false; //--- Mark the new message as not processed.
                    } else { //--- If matching chat was found
                        Class_Chat *chat = member_chats.GetNodeAtIndex(index); //--- Get the chat object by index.
                        chat.member_time = TimeLocal(); //--- Update the time for the chat.
                        chat.member_new_one.message_text = obj_msg.message_text; //--- Store the new message.
                        chat.member_new_one.done = false; //--- Mark the new message as not processed.
                    }
                }

Чтобы отсортировать сообщения по пользователю или разрешить прохождение всех сообщений без сортировки, мы сначала проверяем, содержит ли member_users_filter какие-либо элементы. Если фильтр пуст (Total == 0), пропускаем все сообщения. Если фильтр содержит элементы (Total > 0), мы проверяем, присутствует ли в фильтре имя пользователя отправителя (obj_msg.from_username). Мы используем последовательный метод поиска SearchLinear, при котором имя пользователя отправителя проверяется по фильтру на предмет его присутствия. Если имя пользователя найдено (метод возвращает индекс 0 или более), мы продолжаем обработку сообщения в обычном режиме. После этого этапа сортировки мы просматриваем чат сообщения. Мы ищем имя отправителя в фильтре, чтобы пропустить только определенные имена пользователей (те, которые относятся к отправителю выше в фильтре).

Если chat.member_id совпадает с идентификатором чата сообщения (obj_msg.chat_id), мы сначала записываем текущий индекс в переменную index во время цикла, а затем выходим из этого цикла, поскольку мы нашли нужный чат. Если мы не находим совпадений для чата, а index к тому же остается равным -1, мы создаем новый объект чата и помещаем его в member_chats с помощью метода Add. Затем GetLastNode помогает нам собрать недавно созданный объект чата, который мы сохраняем в указателе chat. Мы присваиваем идентификатор чата из obj_msg.chat_id на chat.member_id и используем функцию TimeLocal для привязки текущего времени к chat.member_time. В самом начале мы устанавливаем member_state чата на 0 и сохраняем новое сообщение в chat.member_new_one.message_text.

Мы также указываем, что сообщение не обработано, устанавив chat.member_new_one.done на false. Если мы находим соответствующий чат (index не равен -1), мы извлекаем соответствующий объект чата с помощью GetNodeAtIndex и обновляем его member_time текущим временем. Затем мы вставляем новое сообщение в chat.member_new_one.message_text и снова пометим его необработанным, установив chat.member_new_one.done на false. Это гарантирует, что чат будет обновлен самым последним сообщением, и система будет знать, что сообщение еще не обработано. Далее нам необходимо обработать запросы обратного вызова из Telegram-чатов.

            //--- Handle callback queries from Telegram.
            if (obj_item["callback_query"].m_type != jv_UNDEF) { //--- Check if there is a callback query in the update.
                Class_CallbackQuery obj_cb_query; //--- Create an instance of Class_CallbackQuery.

                //...

            }

Начнем с проверки того, содержит ли текущее обновление (obj_item) запрос обратного вызова, проверяя, не равен ли тип поля callback_query (m_type) значению jv_UNDEF. Это гарантирует наличие запроса обратного вызова в обновлении. Если это условие выполняется, мы переходим к созданию экземпляра объекта Class_CallbackQuery с именем obj_cb_query. Этот объект будет использоваться для хранения и управления данными запроса обратного вызова. Затем мы можем использовать объект для получения и сохранения данных запроса обратного вызова.

                obj_cb_query.id = obj_item["callback_query"]["id"].ToStr(); //--- Extract and store the callback query ID.
                obj_cb_query.from_id = obj_item["callback_query"]["from"]["id"].ToInt(); //--- Extract and store the sender's ID.
                obj_cb_query.from_first_name = obj_item["callback_query"]["from"]["first_name"].ToStr(); //--- Extract and store the sender's first name.
                obj_cb_query.from_first_name = decodeStringCharacters(obj_cb_query.from_first_name); //--- Decode any special characters in the sender's first name.
                obj_cb_query.from_last_name = obj_item["callback_query"]["from"]["last_name"].ToStr(); //--- Extract and store the sender's last name.
                obj_cb_query.from_last_name = decodeStringCharacters(obj_cb_query.from_last_name); //--- Decode any special characters in the sender's last name.
                obj_cb_query.from_username = obj_item["callback_query"]["from"]["username"].ToStr(); //--- Extract and store the sender's username.
                obj_cb_query.from_username = decodeStringCharacters(obj_cb_query.from_username); //--- Decode any special characters in the sender's username.
                obj_cb_query.message_id = obj_item["callback_query"]["message"]["message_id"].ToInt(); //--- Extract and store the message ID related to the callback.
                obj_cb_query.message_text = obj_item["callback_query"]["message"]["text"].ToStr(); //--- Extract and store the message text related to the callback.
                obj_cb_query.message_text = decodeStringCharacters(obj_cb_query.message_text); //--- Decode any special characters in the message text.
                obj_cb_query.data = obj_item["callback_query"]["data"].ToStr(); //--- Extract and store the callback data.
                obj_cb_query.data = decodeStringCharacters(obj_cb_query.data); //--- Decode any special characters in the callback data.
                
                obj_cb_query.chat_id = obj_item["callback_query"]["message"]["chat"]["id"].ToInt(); //--- Extract and store the chat ID.

Начнем с деталей самого запроса обратного вызова. Идентификатор запроса обратного вызова берется из поля callback_query. Мы используем метод ToStr для преобразования его в строковый формат и сохраняем его в obj_cb_query.id. Следующая часть извлекаемой информации — это идентификатор отправителя, который берется из поля from (от). Мы снова используем метод ToInt и сохраняем преобразованное число в obj_cb_query.from_id. После этого мы берем имя отправителя, указанное в поле from, и преобразуем его в строковый формат. Имя отправителя хранится в obj_cb_query.from_first_name. Последнее, что мы делаем с именем, — используем функцию decodeStringCharacters для декодирования любых специальных символов, которые могут в нем быть.

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

Далее мы сосредоточимся на сообщении, связанном с запросом обратного вызова. Мы берем идентификатор сообщения из поля message, преобразуем его в целое число и сохраняем в obj_cb_query.message_id. При этом текст сообщения также извлекается и преобразуется в строку, которая сохраняется в obj_cb_query.message_text. Все специальные символы в тексте декодируются. Затем мы обращаем внимание на данные обратного вызова. Мы извлекаем их, преобразуем в строку и сохраняем в obj_cb_query.data. Как и любые другие данные, данные обратного вызова закодированы специальными символами.

Наконец, мы получаем из запроса обратного вызова идентификатор чата, в который было отправлено сообщение, преобразуем его в целое число и помещаем в obj_cb_query.chat_id. Это дает нам полный набор информации о запросе обратного вызова, включая то, какой пользователь был в чате, какое было сообщение и каковы были данные обратного вызова. Затем мы приступаем к обработке данных и обновлению итерации.

                ProcessCallbackQuery(obj_cb_query); //--- Call function to process the callback query.
                
                member_update_id = obj_item["update_id"].ToInt() + 1; //--- Update the last processed update ID for callback queries.

Здесь мы вызываем функцию ProcessCallbackQuery, передавая объект obj_cb_query в качестве аргумента. Эта функция отвечает за обработку запроса обратного вызова и обработку извлеченных данных, которые мы собрали ранее, таких как информация о пользователе, идентификатор чата, текст сообщения и данные обратного вызова. Вызывая эту функцию, мы гарантируем, что запрос обратного вызова будет обработан надлежащим образом на основе его конкретного содержимого.

После обработки запроса обратного вызова мы обновляем последний обработанный идентификатор обновления, извлекая update_id из поля obj_item, преобразуя его в целое число, а затем добавляя к нему 1. Это значение сохраняется в member_update_id, который отслеживает последнее обработанное обновление. Этот шаг гарантирует, что мы не будем повторно обрабатывать тот же запрос обратного вызова в будущих итерациях, эффективно отслеживая ход обновления. Наконец, после обработки первого сообщения нам необходимо пометить сообщение как уже обработанное, чтобы избежать повторной обработки.

        member_first_remove = false; //--- After processing the first message, mark that the first message has been handled.

После обработки первого сообщения мы присваиваем переменной member_first_remove значение false. Это значит, что мы позаботились о первом сообщении, и если для него требовалось что-то особенное, мы это уже сделали. Для чего нужен этот шаг? Его цель — обозначить, что первое сообщение обработано и больше обрабатываться не будет. Поступая так, мы гарантируем, что любая будущая логика, зависящая от необработанности первого сообщения, не будет запущена, поскольку в этом нет необходимости.

Полный фрагмент исходного кода, отвечающий за получение и хранение сообщений чата и запросов обратного вызова, выглядит следующим образом:

//+------------------------------------------------------------------+
//|   Function to get chat updates from Telegram                     |
//+------------------------------------------------------------------+
int Class_Bot_EA::getChatUpdates(void) {
    if (member_token == NULL) { //--- If bot token is empty
        Print("ERR: TOKEN EMPTY"); //--- Print error message indicating empty token.
        return (-1); //--- Return -1 to indicate error.
    }
    
    string out; //--- String to hold response data.
    string url = TELEGRAM_BASE_URL + "/bot" + member_token + "/getUpdates"; //--- Construct URL for Telegram API.
    string params = "offset=" + IntegerToString(member_update_id); //--- Set parameters including the offset based on the last update ID.
    
    int res = postRequest(out, url, params, WEB_TIMEOUT); //--- Send a POST request to Telegram with a timeout.
    
    if (res == 0) { //--- If request succeeds (res = 0)
        CJSONValue obj_json(NULL, jv_UNDEF); //--- Create a JSON object to parse the response.
        bool done = obj_json.Deserialize(out); //--- Deserialize the response.
        if (!done) { //--- If deserialization fails
            Print("ERR: JSON PARSING"); //--- Print error message indicating JSON parsing error.
            return (-1); //--- Return -1 to indicate error.
        }
        
        bool ok = obj_json["ok"].ToBool(); //--- Check if the response has "ok" field set to true.
        if (!ok) { //--- If "ok" field is false
            Print("ERR: JSON NOT OK"); //--- Print error message indicating that JSON response is not okay.
            return (-1); //--- Return -1 to indicate error.
        }
        
        int total = ArraySize(obj_json["result"].m_elements); //--- Get the total number of update elements.
        for (int i = 0; i < total; i++) { //--- Iterate through each update element.
            CJSONValue obj_item = obj_json["result"].m_elements[i]; //--- Access individual update element.
            
            if (obj_item["message"].m_type != jv_UNDEF) { //--- Check if the update has a message.
                Class_Message obj_msg; //--- Create an instance of Class_Message to store the message details.
                obj_msg.update_id = obj_item["update_id"].ToInt(); //--- Extract and store update ID.
                obj_msg.message_id = obj_item["message"]["message_id"].ToInt(); //--- Extract and store message ID.
                obj_msg.message_date = (datetime)obj_item["message"]["date"].ToInt(); //--- Extract and store message date.
                obj_msg.message_text = obj_item["message"]["text"].ToStr(); //--- Extract and store message text.
                obj_msg.message_text = decodeStringCharacters(obj_msg.message_text); //--- Decode any special characters in the message text.
                
                obj_msg.from_id = obj_item["message"]["from"]["id"].ToInt(); //--- Extract and store the sender's ID.
                obj_msg.from_first_name = obj_item["message"]["from"]["first_name"].ToStr(); //--- Extract and store the sender's first name.
                obj_msg.from_first_name = decodeStringCharacters(obj_msg.from_first_name); //--- Decode any special characters in the sender's first name.
                obj_msg.from_last_name = obj_item["message"]["from"]["last_name"].ToStr(); //--- Extract and store the sender's last name.
                obj_msg.from_last_name = decodeStringCharacters(obj_msg.from_last_name); //--- Decode any special characters in the sender's last name.
                obj_msg.from_username = obj_item["message"]["from"]["username"].ToStr(); //--- Extract and store the sender's username.
                obj_msg.from_username = decodeStringCharacters(obj_msg.from_username); //--- Decode any special characters in the sender's username.
                
                obj_msg.chat_id = obj_item["message"]["chat"]["id"].ToInt(); //--- Extract and store the chat ID.
                obj_msg.chat_first_name = obj_item["message"]["chat"]["first_name"].ToStr(); //--- Extract and store the chat's first name.
                obj_msg.chat_first_name = decodeStringCharacters(obj_msg.chat_first_name); //--- Decode any special characters in the chat's first name.
                obj_msg.chat_last_name = obj_item["message"]["chat"]["last_name"].ToStr(); //--- Extract and store the chat's last name.
                obj_msg.chat_last_name = decodeStringCharacters(obj_msg.chat_last_name); //--- Decode any special characters in the chat's last name.
                obj_msg.chat_username = obj_item["message"]["chat"]["username"].ToStr(); //--- Extract and store the chat's username.
                obj_msg.chat_username = decodeStringCharacters(obj_msg.chat_username); //--- Decode any special characters in the chat's username.
                obj_msg.chat_type = obj_item["message"]["chat"]["type"].ToStr(); //--- Extract and store the chat type.
                
                //--- Process the message based on chat ID.
                member_update_id = obj_msg.update_id + 1; //--- Update the last processed update ID.
                
                if (member_first_remove) { //--- If it's the first message after starting the bot
                    continue; //--- Skip processing it.
                }

                //--- Check if we need to filter messages based on user or if no filter is applied.
                if (member_users_filter.Total() == 0 ||
                    (member_users_filter.Total() > 0 &&
                    member_users_filter.SearchLinear(obj_msg.from_username) >= 0)) {
                    
                    int index = -1; //--- Initialize index to -1 (indicating no chat found).
                    for (int j = 0; j < member_chats.Total(); j++) { //--- Iterate through all chat objects.
                        Class_Chat *chat = member_chats.GetNodeAtIndex(j); //--- Get chat object by index.
                        if (chat.member_id == obj_msg.chat_id) { //--- If chat ID matches
                            index = j; //--- Store the index.
                            break; //--- Break the loop since we found the chat.
                        }
                    }

                    if (index == -1) { //--- If no matching chat was found
                        member_chats.Add(new Class_Chat); //--- Create a new chat object and add it to the list.
                        Class_Chat *chat = member_chats.GetLastNode(); //--- Get the last (newly added) chat object.
                        chat.member_id = obj_msg.chat_id; //--- Assign the chat ID.
                        chat.member_time = TimeLocal(); //--- Record the current time for the chat.
                        chat.member_state = 0; //--- Initialize the chat state to 0.
                        chat.member_new_one.message_text = obj_msg.message_text; //--- Store the new message in the chat.
                        chat.member_new_one.done = false; //--- Mark the new message as not processed.
                    } else { //--- If matching chat was found
                        Class_Chat *chat = member_chats.GetNodeAtIndex(index); //--- Get the chat object by index.
                        chat.member_time = TimeLocal(); //--- Update the time for the chat.
                        chat.member_new_one.message_text = obj_msg.message_text; //--- Store the new message.
                        chat.member_new_one.done = false; //--- Mark the new message as not processed.
                    }
                }
            }
            

            //--- Handle callback queries from Telegram.
            if (obj_item["callback_query"].m_type != jv_UNDEF) { //--- Check if there is a callback query in the update.
                Class_CallbackQuery obj_cb_query; //--- Create an instance of Class_CallbackQuery.
                obj_cb_query.id = obj_item["callback_query"]["id"].ToStr(); //--- Extract and store the callback query ID.
                obj_cb_query.from_id = obj_item["callback_query"]["from"]["id"].ToInt(); //--- Extract and store the sender's ID.
                obj_cb_query.from_first_name = obj_item["callback_query"]["from"]["first_name"].ToStr(); //--- Extract and store the sender's first name.
                obj_cb_query.from_first_name = decodeStringCharacters(obj_cb_query.from_first_name); //--- Decode any special characters in the sender's first name.
                obj_cb_query.from_last_name = obj_item["callback_query"]["from"]["last_name"].ToStr(); //--- Extract and store the sender's last name.
                obj_cb_query.from_last_name = decodeStringCharacters(obj_cb_query.from_last_name); //--- Decode any special characters in the sender's last name.
                obj_cb_query.from_username = obj_item["callback_query"]["from"]["username"].ToStr(); //--- Extract and store the sender's username.
                obj_cb_query.from_username = decodeStringCharacters(obj_cb_query.from_username); //--- Decode any special characters in the sender's username.
                obj_cb_query.message_id = obj_item["callback_query"]["message"]["message_id"].ToInt(); //--- Extract and store the message ID related to the callback.
                obj_cb_query.message_text = obj_item["callback_query"]["message"]["text"].ToStr(); //--- Extract and store the message text related to the callback.
                obj_cb_query.message_text = decodeStringCharacters(obj_cb_query.message_text); //--- Decode any special characters in the message text.
                obj_cb_query.data = obj_item["callback_query"]["data"].ToStr(); //--- Extract and store the callback data.
                obj_cb_query.data = decodeStringCharacters(obj_cb_query.data); //--- Decode any special characters in the callback data.
                
                obj_cb_query.chat_id = obj_item["callback_query"]["message"]["chat"]["id"].ToInt(); //--- Extract and store the chat ID.
                
                ProcessCallbackQuery(obj_cb_query); //--- Call function to process the callback query.
                
                member_update_id = obj_item["update_id"].ToInt() + 1; //--- Update the last processed update ID for callback queries.
            }
        }
        
        member_first_remove = false; //--- After processing the first message, mark that the first message has been handled.
    }
    
    return 0; //--- Return 0 to indicate successful processing of updates.
}

После получения обновлений чата, нам нужно приступить к обработке ответов.  


Обработка запросов обратного вызова для действий кнопок

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

#define BTN_MENU "BTN_MENU" //--- Identifier for menu button

//+------------------------------------------------------------------+
//| Process new messages                                             |
//+------------------------------------------------------------------+
void Class_Bot_EA::ProcessMessages(void){
   //--- Loop through all chats
   for(int i=0; i<member_chats.Total(); i++){
      Class_Chat *chat = member_chats.GetNodeAtIndex(i); //--- Get the current chat
      if(!chat.member_new_one.done){ //--- Check if the message has not been processed yet
         chat.member_new_one.done = true; //--- Mark the message as processed
         string text = chat.member_new_one.message_text; //--- Get the message text
                  
         //--- Example of sending a message with inline buttons
         if (text == "Start" || text == "/start" || text == "Help" || text == "/help"){
            string message = "Welcome! You can control me via inline buttons!"; //--- Welcome message
            //--- Define inline button to provide menu
            string buttons = "[[{\"text\": \"Provide Menu\", \"callback_data\": \""+BTN_MENU+"\"}]]";
            sendMessageToTelegram(chat.member_id, message, customInlineKeyboardMarkup(buttons)); //--- Send the inline keyboard markup
         }
      }
   }
}

В этом случае мы настраиваем функцию под названием ProcessMessages для обработки пользовательских сообщений, поступающих в нашу систему. Самое первое, что делает эта функция, — это перебирает набор всех чатов, которые мы сохранили в member_chats. Для каждого из этих чатов мы получаем объект чата, соответствующий текущему чату, вызывая GetNodeAtIndex(i). Теперь, когда у нас есть хэндл текущего чата, мы проверяем, было ли уже обработано сообщение в member_new_one. Если нет, мы отмечаем его как обработанный.

Далее мы извлекаем фактическое содержимое сообщения с помощью chat.member_new_one.message_text. Мы оцениваем содержимое, чтобы определить, отправлял ли пользователь какие-либо команды, такие как Start, /start, Help или /help. Когда мы получаем команду такого рода, мы возвращаем сообщение, приветствующее пользователя и сообщающее ему "You can control me via inline buttons!" (вы можете управлять мной с помощью встроенных кнопок). Затем мы определяем встроенную кнопку обратного вызова, которую мы хотим использовать в качестве пункта меню для пользователя. Мы используем поле callback_data кнопки, чтобы указать, что она связана с BTN_MENU. Мы форматируем кнопку как объект JSON и сохраняем ее в переменной buttons.

В заключение вызывается функция sendMessageToTelegram для отправки пользователю приветственного сообщения и нашей встроенной клавиатуры. Эта функция принимает три параметра: chat.member_id, message и разметку кнопки, которая генерируется функцией customInlineKeyboardMarkup. Сообщение вместе с нашими встроенными кнопками отправляется пользователю. Теперь они могут взаимодействовать со встроенными кнопками так же, как обычный пользователь взаимодействует с ботом Telegram. Поскольку мы новички в работе со встроенной клавиатурой, давайте сосредоточимся на ее логике.

            string buttons = "[[{\"text\": \"Provide Menu\", \"callback_data\": \""+BTN_MENU+"\"}]]";

Подробная разбивка:

Внешние скобки: Вся строка заключена в двойные кавычки (" "), что типично для определения строковых литералов во многих языках программирования. Внутри этой строки мы видим символы "[[ ... ]]". Эти скобки используются для определения структуры встроенной клавиатуры:

    1. Первая пара скобок [ ... ] обозначает массив строк на клавиатуре.
    2. Вторая пара скобок [ ... ] представляет строку в этом массиве. В данном случае имеется только одна строка.

Определение кнопки:

Внутри второго набора скобок у нас есть объект {"text": "Provide Menu", "callback_data": " + BTN_MENU + "}. Этот объект определяет одну кнопку:

  1. "text": Этот ключ определяет метку кнопки ("Provide Menu"). Это текст, который появится на кнопке, когда пользователь ее увидит.
  2. "callback_data": Этот ключ определяет данные, которые будут отправлены обратно боту при нажатии кнопки. В данном случае значение равно "BTN_MENU", что является константой, которую мы определили в другом месте кода. Это позволяет боту распознавать, какая кнопка была нажата, и реагировать нужным образом.

Объединение элементов:

Константа BTN_MENU вставляется в строку JSON с помощью конкатенации строк. Это позволяет динамически включать данные обратного вызова кнопки. Например, если BTN_MENU — это BTN_MENU, то результирующий JSON будет выглядеть следующим образом: [{"text": "Provide Menu", "callback_data": "BTN_MENU"}].

Окончательный формат:

Окончательный формат кнопок string при использовании в коде: "[ [{ "text": "Provide Menu", "callback_data": "BTN_MENU" }] ]". Этот формат указывает, что на клавиатуре имеется один ряд, содержащий одну кнопку.

Когда API Telegram получает эту структуру JSON, он интерпретирует ее как встроенную клавиатуру с одной кнопкой. Когда пользователь нажимает эту кнопку, бот получает данные обратного вызова BTN_MENU в запросе обратного вызова, которые он затем может использовать для определения соответствующего ответа. В структуре мы использовали пользовательскую функцию для создания встроенной кнопки. Ее логика такова:

//+------------------------------------------------------------------+
//| Create a custom inline keyboard markup for Telegram              |
//+------------------------------------------------------------------+
string customInlineKeyboardMarkup(const string buttons){
   //--- Construct the JSON string for the inline keyboard markup
   string result = "{\"inline_keyboard\": " + UrlEncode(buttons) + "}"; //--- Encode buttons as JSON
   return(result);
}

Функция customInlineKeyboardMarkup создает пользовательскую встроенную разметку клавиатуры для сообщений Telegram. Для этого мы начнем со строкового параметра buttons, который содержит структуру JSON, определяющую встроенные кнопки. Наша задача — создать объект JSON, который Telegram сможет использовать для отображения встроенной клавиатуры. Начнем с формирования структуры JSON с ключом inline_keyboard. Далее мы используем функцию UrlEncode для обработки любых специальных символов, которые могут присутствовать в строке buttons. Этот этап кодирования имеет решающее значение, поскольку без него мы можем столкнуться с проблемами со специальными символами в определениях кнопок. После добавления закодированной строки кнопок мы закрываем объект JSON. Результирующая строка представляет собой допустимое JSON-представление встроенной разметки клавиатуры. Мы возвращаем эту строку, чтобы ее можно было отправить в API Telegram, который затем интерактивно отобразит встроенную клавиатуру в сообщении. После запуска программы мы получим следующий результат.

Сообщение инициализации

Всё работает. Мы создали встроенную кнопку. Однако мы пока не можем реагировать на клики по ней. Таким образом, нам необходимо перехватить полученный запрос обратного вызова и отреагировать на щелчок соответственно. Для этого нам потребуется создать функцию, которая получает данные запроса.

//+------------------------------------------------------------------+
//|   Function to process callback queries                           |
//+------------------------------------------------------------------+
void Class_Bot_EA::ProcessCallbackQuery(Class_CallbackQuery &cb_query) {
   Print("Callback Query ID: ", cb_query.id); //--- Log the callback query ID
   Print("Chat Token: ", member_token); //--- Log the member token
   Print("From First Name: ", cb_query.from_first_name); //--- Log the sender's first name
   Print("From Last Name: ", cb_query.from_last_name); //--- Log the sender's last name
   Print("From Username: ", cb_query.from_username); //--- Log the sender's username
   Print("Message ID: ", cb_query.message_id); //--- Log the message ID
   Print("Message Text: ", cb_query.message_text); //--- Log the message text
   Print("Callback Data: ", cb_query.data); //--- Log the callback data
}

Функция ProcessCallbackQuery управляет деталями запроса обратного вызова, который поступает из Telegram. Она работает с экземпляром Class_CallbackQuery, который содержит всю информацию, связанную с обратным вызовом. Во-первых, она регистрирует идентификатор запроса обратного вызова, который является уникальным идентификатором запроса и необходим для его отслеживания и управления. Затем функция вводит member_token. Роль этого токена — указать, какой бот или участник обрабатывает обратный вызов, и таким образом гарантировать, что запрос обрабатывает правильный бот, причем только один.

Затем мы записываем имя и фамилию отправителя, используя cb_query.from_first_name и cb_query.from_last_name соответственно. Они позволяют нам узнать личность пользователя, нажавшего встроенную кнопку, и обеспечить индивидуальный подход, если нам когда-либо понадобится обратиться к этому пользователю в будущем. Мы также записываем имя пользователя отправителя с помощью cb_query.from_username. Это дает нам еще один способ напрямую обратиться к пользователю в будущем, если возникнет такая необходимость. После регистрации личности отправителя мы регистрируем идентификатор сообщения, связанного с обратным вызовом, используя cb_query.message_id. Зная этот идентификатор, мы можем узнать, какое конкретное сообщение было передано нажатием кнопки.

Кроме того, мы регистрируем текст сообщения с помощью cb_query.message_text. Это обеспечивает контекст сообщения при нажатии кнопки. Мы также регистрируем данные обратного вызова с помощью cb_query.data. Эти данные отправляются обратно при нажатии кнопки и используются для определения того, какое действие следует предпринять в зависимости от действия пользователя. Регистрируя эти данные, мы получаем полное представление о запросе обратного вызова. Это удобно для отладки и обеспечивает лучшее понимание взаимодействия пользователя с ботом. После запуска программы мы получим следующие результаты в торговом терминале.

Сообщения MetaTrader 5

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

#define BTN_NAME "BTN_NAME" //--- Identifier for name button
#define BTN_INFO "BTN_INFO" //--- Identifier for info button
#define BTN_QUOTES "BTN_QUOTES" //--- Identifier for quotes button
#define BTN_MORE "BTN_MORE" //--- Identifier for more options button
#define BTN_SCREENSHOT "BTN_SCREENSHOT" //--- Identifier for screenshot button
#define EMOJI_CANCEL "\x274C" //--- Cross mark emoji

#define EMOJI_UP "\x2B06" //--- Upwards arrow emoji
#define BTN_BUY "BTN_BUY" //--- Identifier for buy button
#define BTN_CLOSE "BTN_CLOSE" //--- Identifier for close button
#define BTN_NEXT "BTN_NEXT" //--- Identifier for next button

#define EMOJI_PISTOL "\xF52B" //--- Pistol emoji
#define BTN_CONTACT "BTN_CONTACT" //--- Identifier for contact button
#define BTN_JOIN "BTN_JOIN" //--- Identifier for join button

После определения функции мы можем перейти к получению ответов.

   //--- Respond based on the callback data
   string response_text;
   if (cb_query.data == BTN_MENU) {
      response_text = "You clicked "+BTN_MENU+"!"; //--- Prepare response text for BTN_MENU
      Print("RESPONSE = ", response_text); //--- Log the response
      //--- Send the response message to the correct group/channel chat ID
      sendMessageToTelegram(cb_query.chat_id, response_text, NULL);
      string message = "Information"; //--- Message to display options
      //--- Define inline buttons with callback data
      string buttons = "[[{\"text\": \"Get Expert's Name\", \"callback_data\": \""+BTN_NAME+"\"}],"
                        "[{\"text\": \"Get Account Information\", \"callback_data\": \""+BTN_INFO+"\"}],"
                        "[{\"text\": \"Get Current Market Quotes\", \"callback_data\": \""+BTN_QUOTES+"\"}],"
                        "[{\"text\": \"More\", \"callback_data\": \""+BTN_MORE+"\"}, {\"text\": \"Screenshots\", \"callback_data\": \""+BTN_SCREENSHOT+"\"}, {\"text\": \""+EMOJI_CANCEL+"\", \"callback_data\": \""+EMOJI_CANCEL+"\"}]]";
      sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); //--- Send the inline keyboard markup
   }

Здесь мы управляем ответом на запрос обратного вызова на основе данных обратного вызова. Начнем с инициализации строковой переменной response_text для хранения сообщения, которое мы хотим отправить пользователю. Затем проверяем, соответствует ли callback_data из запроса обратного вызова (cb_query.data) константе BTN_MENU. Если соответствует, устанавливаем response_text для "You clicked "+BTN_MENU+"!", что подтверждает нажатие кнопки и включает идентификатор нажатой кнопки. Мы регистрируем этот ответ с помощью функции Print, чтобы отслеживать, что именно отправляется.

Далее мы используем функцию sendMessageToTelegram для отправки сообщения response_text в чат, идентифицированный cb_query.chat_id. Поскольку на данном этапе мы отправляем простое текстовое сообщение без встроенной клавиатуры, третий параметр - NULL, что разметка дополнительной клавиатуры не включена.

После отправки первоначального сообщения мы готовим новое сообщение с текстом Information, которое предоставит пользователю различные возможности. Затем мы определяем встроенные кнопки, используя структуру, подобную JSON, в строке buttons. Эта структура включает кнопки с такими надписями, как Get Expert's Name (получить имя советника), Get Account Information (получить информацию о счете), Get Current Market Quotes (получить текущие рыночные котировки), More (дополнительно), Screenshots (скриншоты) и Cancel (отмена). Каждой кнопке назначаются определенные значения callback_data, такие как BTN_NAME, BTN_INFO, BTN_QUOTES, BTN_MORE, BTN_SCREENSHOT и EMOJI_CANCEL, которые помогают определить, какая кнопка была нажата.

Наконец, мы отправляем новое сообщение вместе со встроенной клавиатурой, используя функцию sendMessageToTelegram. Встроенная клавиатура форматируется в JSON с помощью функции customInlineKeyboardMarkup, что гарантирует, что Telegram сможет корректно отображать кнопки. Такой подход позволяет нам взаимодействовать с пользователями интерактивно, предоставляя им различные возможности непосредственно в интерфейсе Telegram. После компиляции получаем следующие результаты.

Встроенный ответ в виде меню

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

   else if (cb_query.data == BTN_NAME) {
      response_text = "You clicked "+BTN_NAME+"!"; //--- Prepare response text for BTN_NAME
      Print("RESPONSE = ", response_text); //--- Log the response
      string message = "The file name of the EA that I control is:\n"; //--- Message with EA file name
      message += "\xF50B"+__FILE__+" Enjoy.\n"; //--- Append the file name and a friendly message
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }

Этот раздел управляет конкретным запросом обратного вызова, в котором callback_data равен BTN_NAME. Начнем с настройки текста ответа в переменной response_text. Если callback_data соответствует BTN_NAME, устанавливаем response_text на "You clicked " + BTN_NAME + "!" Это подтверждает нажатие кнопки и включает идентификатор нажатой кнопки. Затем мы выводим этот ответ с помощью функции Print, чтобы следить за тем, что отправляется пользователю.

Затем мы создаем новое сообщение, которое передает сведения о файле советника, которым управляет бот. Это послание, сгенерированное ботом, начинается со слов "The file name of the EA that I control is:\n" (имя файла советника, которым я управляю, — \n), а затем добавляется имя текущего исходного файла, представленное как __FILE__, к сообщению, завершаемому дружелюбным "Enjoy" (наслаждайтесь). Странным штрихом является то, что послание начинается с символа \xF50B, который представляет собой значок, типографский штрих или просто способ удивить читателя.

В заключение мы вызываем функцию sendMessageToTelegram для отправки сообщения в чат, соответствующий cb_query.chat_id. NULL передается в качестве третьего параметра, что означает, что никакая встроенная клавиатура не будет сопровождать это сообщение. При нажатии на кнопку мы получаем следующий ответ.

Имя GIF

Всё работает. Используем аналогичный подход, чтобы добиться отзывчивости других кнопок, то есть информации о счете и котировок рыночных цен.

   else if (cb_query.data == BTN_INFO) {
      response_text = "You clicked "+BTN_INFO+"!"; //--- Prepare response text for BTN_INFO
      Print("RESPONSE = ", response_text); //--- Log the response
      ushort MONEYBAG = 0xF4B0; //--- Define money bag emoji
      string MONEYBAGcode = ShortToString(MONEYBAG); //--- Convert emoji code to string
      string currency = AccountInfoString(ACCOUNT_CURRENCY); //--- Get the account currency
      //--- Construct the account information message
      string message = "\x2733\Account No: "+(string)AccountInfoInteger(ACCOUNT_LOGIN)+"\n";
      message += "\x23F0\Account Server: "+AccountInfoString(ACCOUNT_SERVER)+"\n";
      message += MONEYBAGcode+"Balance: "+(string)AccountInfoDouble(ACCOUNT_BALANCE)+" "+currency+"\n";
      message += "\x2705\Profit: "+(string)AccountInfoDouble(ACCOUNT_PROFIT)+" "+currency+"\n";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }
   else if (cb_query.data == BTN_QUOTES) {
      response_text = "You clicked "+BTN_QUOTES+"!"; //--- Prepare response text for BTN_QUOTES
      Print("RESPONSE = ", response_text); //--- Log the response
      double Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Get the current ask price
      double Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get the current bid price
      //--- Construct the market quotes message
      string message = "\xF170 Ask: "+(string)Ask+"\n";
      message += "\xF171 Bid: "+(string)Bid+"\n";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }

После компиляции получаем следующие результаты:

Информация и котировки GIF

Всё работает. Теперь приступим к обработке встроенной кнопки More (дополнительно). Мы стараемся не загромождать интерфейс или поле чата сообщениями. Мы эффективно используем встроенные кнопки повторно.

   else if (cb_query.data == BTN_MORE) {
      response_text = "You clicked "+BTN_MORE+"!"; //--- Prepare response text for BTN_MORE
      Print("RESPONSE = ", response_text); //--- Log the response
      
      string message = "Choose More Options Below:\n"; //--- Message to prompt for additional options
      message += "Trading Operations"; //--- Title for trading operations
      //--- Define inline buttons for additional options
      string buttons = "[[{\"text\": \""+EMOJI_UP+"\", \"callback_data\": \""+EMOJI_UP+"\"}],"
                        "[{\"text\": \"Buy\", \"callback_data\": \""+BTN_BUY+"\"}, {\"text\": \"Close\", \"callback_data\": \""+BTN_CLOSE+"\"}, {\"text\": \"Next\", \"callback_data\": \""+BTN_NEXT+"\"}]]";
      sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); //--- Send the inline keyboard markup
   }

Здесь мы обрабатываем запрос обратного вызова, где данные обратного вызова — BTN_MORE. Начнем с подготовки ответного сообщения, хранящегося в переменной response_text. Если данные обратного вызова соответствуют BTN_MORE, мы устанавливаем response_text на "You clicked "+BTN_MORE+"!", что подтверждает нажатие кнопки и включает идентификатор нажатой кнопки. Этот ответ регистрируется с помощью функции Print, чтобы отслеживать, что именно отправляется.

Далее мы создаем новое сообщение, предлагающее пользователю выбрать один из дополнительных вариантов. Переменная message начинается с сообщения "Choose More Options Below:\n" (выберите больше параметров ниже: \n), за которой следует Trading Operations (торговые операции), что служит заголовком для набора параметров, связанных с торговлей. Затем мы определяем встроенные кнопки для этих дополнительных опций, используя структуру, подобную JSON, в строке buttons. В эту структуру входят:

  • Кнопка эмодзи EMOJI_UP (вверх) и соответствующий ей callback_data в виде EMOJI_UP.
  • Ряд кнопок для различных торговых операций: Buy (купить), Close (закрыть) и Next (далее), каждая со своими соответствующими значениями callback_data - BTN_BUY, BTN_CLOSE и BTN_NEXT.

Наконец, мы используем функцию sendMessageToTelegram для отправки этого сообщения вместе со встроенной клавиатурой в чат, идентифицированный как cb_query.chat_id. Встроенная разметка клавиатуры форматируется в JSON с помощью функции customInlineKeyboardMarkup. Если мы нажмем на эту кнопку, мы должны получить еще одну расширенную кнопку.

Кнопка More GIF

Всё прошло так, как и ожидалось. Сейчас мы работаем только над новыми кнопками. Во-первых, это кнопка эмодзи "вверх". При нажатии на нее мы хотим вернуться в предыдущее (главное) меню.

   else if (cb_query.data == EMOJI_UP) {
      response_text = "You clicked "+EMOJI_UP+"!"; //--- Prepare response text for EMOJI_UP
      Print("RESPONSE = ", response_text); //--- Log the response
      string message = "Choose a menu item:\n"; //--- Message to prompt for menu selection
      message += "Information"; //--- Title for information options
      //--- Define inline buttons for menu options
      string buttons = "[[{\"text\": \"Get Expert's Name\", \"callback_data\": \""+BTN_NAME+"\"}],"
                        "[{\"text\": \"Get Account Information\", \"callback_data\": \""+BTN_INFO+"\"}],"
                        "[{\"text\": \"Get Current Market Quotes\", \"callback_data\": \""+BTN_QUOTES+"\"}],"
                        "[{\"text\": \"More\", \"callback_data\": \""+BTN_MORE+"\"}, {\"text\": \"Screenshots\", \"callback_data\": \""+BTN_SCREENSHOT+"\"}, {\"text\": \""+EMOJI_CANCEL+"\", \"callback_data\": \""+EMOJI_CANCEL+"\"}]]";
      sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); //--- Send the inline keyboard markup
   }

Здесь мы просто используем значения по умолчанию и отправляем встроенную клавиатуру главного меню. Мы используем ту же логику при реакции на другие кнопки для операций открытия и закрытия позиции, как показано ниже.

   else if (cb_query.data == BTN_BUY) {
      response_text = "You clicked "+BTN_BUY+"!"; //--- Prepare response text for BTN_BUY
      Print("RESPONSE = ", response_text); //--- Log the response
      
      CTrade obj_trade; //--- Create a trade object
      double Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Get the current ask price
      double Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get the current bid price
      //--- Open a buy position
      obj_trade.Buy(0.01, NULL, 0, Bid - 300 * _Point, Bid + 300 * _Point);
      double entry = 0, sl = 0, tp = 0, vol = 0;
      ulong ticket = obj_trade.ResultOrder(); //--- Get the ticket number of the new order
      if (ticket > 0) {
         if (PositionSelectByTicket(ticket)) { //--- Select the position by ticket
            entry = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get the entry price
            sl = PositionGetDouble(POSITION_SL); //--- Get the stop loss price
            tp = PositionGetDouble(POSITION_TP); //--- Get the take profit price
            vol = PositionGetDouble(POSITION_VOLUME); //--- Get the volume
         }
      }
      //--- Construct the message with position details
      string message = "\xF340\Opened BUY Position:\n";
      message += "Ticket: "+(string)ticket+"\n";
      message += "Open Price: "+(string)entry+"\n";
      message += "Lots: "+(string)vol+"\n";
      message += "SL: "+(string)sl+"\n";
      message += "TP: "+(string)tp+"\n";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }
   else if (cb_query.data == BTN_CLOSE) {
      response_text = "You clicked "+BTN_CLOSE+"!"; //--- Prepare response text for BTN_CLOSE
      Print("RESPONSE = ", response_text); //--- Log the response
      CTrade obj_trade; //--- Create a trade object
      int totalOpenBefore = PositionsTotal(); //--- Get the total number of open positions before closing
      obj_trade.PositionClose(_Symbol); //--- Close the position for the symbol
      int totalOpenAfter = PositionsTotal(); //--- Get the total number of open positions after closing
      //--- Construct the message with position closure details
      string message = "\xF62F\Closed Position:\n";
      message += "Total Positions (Before): "+(string)totalOpenBefore+"\n";
      message += "Total Positions (After): "+(string)totalOpenAfter+"\n";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }

Вот какие результаты мы получили после запуска программы.

Больше операций GIF

Отлично! Аналогично добавляем остальные сегменты управления, как показано ниже.

   else if (cb_query.data == BTN_NEXT) {
      response_text = "You clicked "+BTN_NEXT+"!"; //--- Prepare response text for BTN_NEXT
      Print("RESPONSE = ", response_text); //--- Log the response
      
      string message = "Choose Still More Options Below:\n"; //--- Message to prompt for further options
      message += "More Options"; //--- Title for more options
      //--- Define inline buttons for additional options
      string buttons = "[[{\"text\": \""+EMOJI_UP+"\", \"callback_data\": \""+EMOJI_UP+"\"}, {\"text\": \"Contact\", \"callback_data\": \""+BTN_CONTACT+"\"}, {\"text\": \"Join\", \"callback_data\": \""+BTN_JOIN+"\"},{\"text\": \""+EMOJI_PISTOL+"\", \"callback_data\": \""+EMOJI_PISTOL+"\"}]]";
      sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); //--- Send the inline keyboard markup
   }
   else if (cb_query.data == BTN_CONTACT) {
      response_text = "You clicked "+BTN_CONTACT+"!"; //--- Prepare response text for BTN_CONTACT
      Print("RESPONSE = ", response_text); //--- Log the response
      string message = "Contact the developer via link below:\n"; //--- Message with contact link
      message += "https://t.me/Forex_Algo_Trader";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }
   else if (cb_query.data == BTN_JOIN) {
      response_text = "You clicked "+BTN_JOIN+"!"; //--- Prepare response text for BTN_JOIN
      Print("RESPONSE = ", response_text); //--- Log the response
      string message = "You want to be part of our MQL5 Community?\n"; //--- Message inviting to join the community
      message += "Welcome! <a href=\"https://t.me/forexalgo_trading\">Click me</a> to join.\n";
      message += "<s>Civil Engineering</s> Forex AlgoTrading\n"; //--- Strikethrough text
      message += "<pre>This is a sample of our MQL5 code</pre>\n"; //--- Preformatted text
      message += "<u><i>Remember to follow community guidelines!\xF64F\</i></u>\n"; //--- Italic and underline text
      message += "<b>Happy Trading!</b>\n"; //--- Bold text
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }
   else if (cb_query.data == EMOJI_PISTOL) {
      response_text = "You clicked "+EMOJI_PISTOL+"!"; //--- Prepare response text for EMOJI_PISTOL
      Print("RESPONSE = ", response_text); //--- Log the response
      string message = "Choose More Options Below:\n"; //--- Message to prompt for more options
      message += "Trading Operations"; //--- Title for trading operations
      //--- Define inline buttons for additional trading options
      string buttons = "[[{\"text\": \""+EMOJI_UP+"\", \"callback_data\": \""+EMOJI_UP+"\"}],"
                        "[{\"text\": \"Buy\", \"callback_data\": \""+BTN_BUY+"\"}, {\"text\": \"Close\", \"callback_data\": \""+BTN_CLOSE+"\"}, {\"text\": \"Next\", \"callback_data\": \""+BTN_NEXT+"\"}]]";
      sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); //--- Send the inline keyboard markup
   }

Здесь мы обработали кнопки, генерируемые в поле Next, а также их отзывчивость. Теперь нужно позаботиться о кнопке снимка экрана, которая находится в главном меню встроенных кнопок.

   else if (cb_query.data == BTN_SCREENSHOT) {
      response_text = "You clicked "+BTN_SCREENSHOT+"!"; //--- Prepare response text for BTN_SCREENSHOT
      Print("RESPONSE = ", response_text); //--- Log the response
      
      string message = "Okay. Command 'get Current Chart Screenshot' received.\n"; //--- Message acknowledging screenshot command
      message += "Screenshot sending process initiated \xF60E"; //--- Emoji indicating process initiation
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
      string caption = "Screenshot of Symbol: "+_Symbol+ //--- Caption for screenshot
                       " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+ //--- Timeframe
                       ") @ Time: "+TimeToString(TimeCurrent()); //--- Current time
      //--- Send the screenshot to Telegram
      sendScreenshotToTelegram(cb_query.chat_id, _Symbol, _Period, caption);
   }

Наконец, нам нужно позаботиться о кнопке Cancel, удалив текущие встроенные кнопки, чтобы можно было начать заново.

   else if (cb_query.data == EMOJI_CANCEL) {
      response_text = "You clicked "+EMOJI_CANCEL+"!"; //--- Prepare response text for EMOJI_CANCEL
      Print("RESPONSE = ", response_text); //--- Log the response
      
      string message = "Choose /start or /help to begin."; //--- Message for user guidance
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
      //--- Reset the inline button state by removing the keyboard
      removeInlineButtons(member_token, cb_query.chat_id, cb_query.message_id);
   }   

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

//+------------------------------------------------------------------+
//| Remove inline buttons by editing message reply markup            |
//+------------------------------------------------------------------+
void removeInlineButtons(string memberToken, long chatID, long messageID){
   //--- Reset the inline button state by removing the keyboard
   string url = TELEGRAM_BASE_URL + "/bot" + memberToken + "/editMessageReplyMarkup"; //--- API URL to edit message
   string params = "chat_id=" + IntegerToString(chatID) + //--- Chat ID parameter
                 "&message_id=" + IntegerToString(messageID) + //--- Message ID parameter
                 "&reply_markup=" + UrlEncode("{\"inline_keyboard\":[]}"); //--- Empty inline keyboard
   string response;
   int res = postRequest(response, url, params, WEB_TIMEOUT); //--- Send request to Telegram API
}

Здесь мы определяем функцию removeInlineButtons. Ее цель — избавиться от встроенных кнопок в ранее отправленном сообщении путем изменения разметки ответа на сообщение. Функция имеет три параметра: memberToken (токен аутентификации бота), chatID (идентификатор чата, в который было отправлено сообщение) и messageID (идентификатор сообщения, содержащего встроенные кнопки). Сначала мы создаем URL-адрес конечной точки API для метода Telegram editMessageReplyMarkup. Мы делаем это, объединяя TELEGRAM_BASE_URL с /bot и memberToken. Это формирует URL-адрес, который мы будем использовать для связи с серверами Telegram.

Затем мы указываем строку params, которая содержит необходимые параметры для вызова API. Мы включаем параметр chat_id. Чтобы получить его значение, мы преобразуем переменную chatID из целого числа в строку. То же самое делаем для параметра message_id. Наконец, мы сообщаем API о необходимости удалить встроенные кнопки, отправив пустое поле reply_markup. Значением этого поля является пустая строка JSON, которую мы получаем с помощью UrlEncoding значения переменной emptyInlineKeyboard.

После настройки параметров мы объявляем переменную response для хранения всего, что отправляет сервер, и вызываем postRequest для отправки API-запроса в Telegram. Функция postRequest отправляет запрос, используя предоставленный URL и параметры, а также тайм-аут (WEB_TIMEOUT) на случай, если что-то пойдет не так. Если запрос выполнен успешно, мы получаем желаемый результат — сообщение без встроенных кнопок, что фактически сбрасывает их состояние. Если данные обратного вызова не распознаны, мы возвращаем сообщение, в котором говорится, что нажатая кнопка неизвестна.

   else {
      response_text = "Unknown button!"; //--- Prepare response text for unknown buttons
      Print("RESPONSE = ", response_text); //--- Log the response
   }

При нажатии кнопки Cancel мы получаем следующее.

Кнопка Cancel GIF

Всё работает. Полный исходный код, отвечающий за обработку запросов обратного вызова, выглядит следующим образом.

#define BTN_MENU "BTN_MENU" //--- Identifier for menu button

//+------------------------------------------------------------------+
//| Process new messages                                             |
//+------------------------------------------------------------------+
void Class_Bot_EA::ProcessMessages(void){
   //--- Loop through all chats
   for(int i=0; i<member_chats.Total(); i++){
      Class_Chat *chat = member_chats.GetNodeAtIndex(i); //--- Get the current chat
      if(!chat.member_new_one.done){ //--- Check if the message has not been processed yet
         chat.member_new_one.done = true; //--- Mark the message as processed
         string text = chat.member_new_one.message_text; //--- Get the message text
                  
         //--- Example of sending a message with inline buttons
         if (text == "Start" || text == "/start" || text == "Help" || text == "/help"){
            string message = "Welcome! You can control me via inline buttons!"; //--- Welcome message
            //--- Define inline button to provide menu
            string buttons = "[[{\"text\": \"Provide Menu\", \"callback_data\": \""+BTN_MENU+"\"}]]";
            sendMessageToTelegram(chat.member_id, message, customInlineKeyboardMarkup(buttons)); //--- Send the inline keyboard markup
         }
      }
   }
}


#define BTN_NAME "BTN_NAME" //--- Identifier for name button
#define BTN_INFO "BTN_INFO" //--- Identifier for info button
#define BTN_QUOTES "BTN_QUOTES" //--- Identifier for quotes button
#define BTN_MORE "BTN_MORE" //--- Identifier for more options button
#define BTN_SCREENSHOT "BTN_SCREENSHOT" //--- Identifier for screenshot button
#define EMOJI_CANCEL "\x274C" //--- Cross mark emoji

#define EMOJI_UP "\x2B06" //--- Upwards arrow emoji
#define BTN_BUY "BTN_BUY" //--- Identifier for buy button
#define BTN_CLOSE "BTN_CLOSE" //--- Identifier for close button
#define BTN_NEXT "BTN_NEXT" //--- Identifier for next button

#define EMOJI_PISTOL "\xF52B" //--- Pistol emoji
#define BTN_CONTACT "BTN_CONTACT" //--- Identifier for contact button
#define BTN_JOIN "BTN_JOIN" //--- Identifier for join button

//+------------------------------------------------------------------+
//|   Function to process callback queries                           |
//+------------------------------------------------------------------+
void Class_Bot_EA::ProcessCallbackQuery(Class_CallbackQuery &cb_query) {
   Print("Callback Query ID: ", cb_query.id); //--- Log the callback query ID
   Print("Chat Token: ", member_token); //--- Log the member token
   Print("From First Name: ", cb_query.from_first_name); //--- Log the sender's first name
   Print("From Last Name: ", cb_query.from_last_name); //--- Log the sender's last name
   Print("From Username: ", cb_query.from_username); //--- Log the sender's username
   Print("Message ID: ", cb_query.message_id); //--- Log the message ID
   Print("Message Text: ", cb_query.message_text); //--- Log the message text
   Print("Callback Data: ", cb_query.data); //--- Log the callback data

   //--- Respond based on the callback data
   string response_text;
   if (cb_query.data == BTN_MENU) {
      response_text = "You clicked "+BTN_MENU+"!"; //--- Prepare response text for BTN_MENU
      Print("RESPONSE = ", response_text); //--- Log the response
      //--- Send the response message to the correct group/channel chat ID
      sendMessageToTelegram(cb_query.chat_id, response_text, NULL);
      string message = "Information"; //--- Message to display options
      //--- Define inline buttons with callback data
      string buttons = "[[{\"text\": \"Get Expert's Name\", \"callback_data\": \""+BTN_NAME+"\"}],"
                        "[{\"text\": \"Get Account Information\", \"callback_data\": \""+BTN_INFO+"\"}],"
                        "[{\"text\": \"Get Current Market Quotes\", \"callback_data\": \""+BTN_QUOTES+"\"}],"
                        "[{\"text\": \"More\", \"callback_data\": \""+BTN_MORE+"\"}, {\"text\": \"Screenshots\", \"callback_data\": \""+BTN_SCREENSHOT+"\"}, {\"text\": \""+EMOJI_CANCEL+"\", \"callback_data\": \""+EMOJI_CANCEL+"\"}]]";
      sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); //--- Send the inline keyboard markup
   }
   else if (cb_query.data == BTN_NAME) {
      response_text = "You clicked "+BTN_NAME+"!"; //--- Prepare response text for BTN_NAME
      Print("RESPONSE = ", response_text); //--- Log the response
      string message = "The file name of the EA that I control is:\n"; //--- Message with EA file name
      message += "\xF50B"+__FILE__+" Enjoy.\n"; //--- Append the file name and a friendly message
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }
   else if (cb_query.data == BTN_INFO) {
      response_text = "You clicked "+BTN_INFO+"!"; //--- Prepare response text for BTN_INFO
      Print("RESPONSE = ", response_text); //--- Log the response
      ushort MONEYBAG = 0xF4B0; //--- Define money bag emoji
      string MONEYBAGcode = ShortToString(MONEYBAG); //--- Convert emoji code to string
      string currency = AccountInfoString(ACCOUNT_CURRENCY); //--- Get the account currency
      //--- Construct the account information message
      string message = "\x2733\Account No: "+(string)AccountInfoInteger(ACCOUNT_LOGIN)+"\n";
      message += "\x23F0\Account Server: "+AccountInfoString(ACCOUNT_SERVER)+"\n";
      message += MONEYBAGcode+"Balance: "+(string)AccountInfoDouble(ACCOUNT_BALANCE)+" "+currency+"\n";
      message += "\x2705\Profit: "+(string)AccountInfoDouble(ACCOUNT_PROFIT)+" "+currency+"\n";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }
   else if (cb_query.data == BTN_QUOTES) {
      response_text = "You clicked "+BTN_QUOTES+"!"; //--- Prepare response text for BTN_QUOTES
      Print("RESPONSE = ", response_text); //--- Log the response
      double Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Get the current ask price
      double Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get the current bid price
      //--- Construct the market quotes message
      string message = "\xF170 Ask: "+(string)Ask+"\n";
      message += "\xF171 Bid: "+(string)Bid+"\n";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }
   else if (cb_query.data == BTN_MORE) {
      response_text = "You clicked "+BTN_MORE+"!"; //--- Prepare response text for BTN_MORE
      Print("RESPONSE = ", response_text); //--- Log the response
      
      string message = "Choose More Options Below:\n"; //--- Message to prompt for additional options
      message += "Trading Operations"; //--- Title for trading operations
      //--- Define inline buttons for additional options
      string buttons = "[[{\"text\": \""+EMOJI_UP+"\", \"callback_data\": \""+EMOJI_UP+"\"}],"
                        "[{\"text\": \"Buy\", \"callback_data\": \""+BTN_BUY+"\"}, {\"text\": \"Close\", \"callback_data\": \""+BTN_CLOSE+"\"}, {\"text\": \"Next\", \"callback_data\": \""+BTN_NEXT+"\"}]]";
      sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); //--- Send the inline keyboard markup
   }
   else if (cb_query.data == EMOJI_CANCEL) {
      response_text = "You clicked "+EMOJI_CANCEL+"!"; //--- Prepare response text for EMOJI_CANCEL
      Print("RESPONSE = ", response_text); //--- Log the response
      
      string message = "Choose /start or /help to begin."; //--- Message for user guidance
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
      //--- Reset the inline button state by removing the keyboard
      removeInlineButtons(member_token, cb_query.chat_id, cb_query.message_id);
   }   
   else if (cb_query.data == EMOJI_UP) {
      response_text = "You clicked "+EMOJI_UP+"!"; //--- Prepare response text for EMOJI_UP
      Print("RESPONSE = ", response_text); //--- Log the response
      string message = "Choose a menu item:\n"; //--- Message to prompt for menu selection
      message += "Information"; //--- Title for information options
      //--- Define inline buttons for menu options
      string buttons = "[[{\"text\": \"Get Expert's Name\", \"callback_data\": \""+BTN_NAME+"\"}],"
                        "[{\"text\": \"Get Account Information\", \"callback_data\": \""+BTN_INFO+"\"}],"
                        "[{\"text\": \"Get Current Market Quotes\", \"callback_data\": \""+BTN_QUOTES+"\"}],"
                        "[{\"text\": \"More\", \"callback_data\": \""+BTN_MORE+"\"}, {\"text\": \"Screenshots\", \"callback_data\": \""+BTN_SCREENSHOT+"\"}, {\"text\": \""+EMOJI_CANCEL+"\", \"callback_data\": \""+EMOJI_CANCEL+"\"}]]";
      sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); //--- Send the inline keyboard markup
   }
   else if (cb_query.data == BTN_BUY) {
      response_text = "You clicked "+BTN_BUY+"!"; //--- Prepare response text for BTN_BUY
      Print("RESPONSE = ", response_text); //--- Log the response
      
      CTrade obj_trade; //--- Create a trade object
      double Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Get the current ask price
      double Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get the current bid price
      //--- Open a buy position
      obj_trade.Buy(0.01, NULL, 0, Bid - 300 * _Point, Bid + 300 * _Point);
      double entry = 0, sl = 0, tp = 0, vol = 0;
      ulong ticket = obj_trade.ResultOrder(); //--- Get the ticket number of the new order
      if (ticket > 0) {
         if (PositionSelectByTicket(ticket)) { //--- Select the position by ticket
            entry = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get the entry price
            sl = PositionGetDouble(POSITION_SL); //--- Get the stop loss price
            tp = PositionGetDouble(POSITION_TP); //--- Get the take profit price
            vol = PositionGetDouble(POSITION_VOLUME); //--- Get the volume
         }
      }
      //--- Construct the message with position details
      string message = "\xF340\Opened BUY Position:\n";
      message += "Ticket: "+(string)ticket+"\n";
      message += "Open Price: "+(string)entry+"\n";
      message += "Lots: "+(string)vol+"\n";
      message += "SL: "+(string)sl+"\n";
      message += "TP: "+(string)tp+"\n";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }
   else if (cb_query.data == BTN_CLOSE) {
      response_text = "You clicked "+BTN_CLOSE+"!"; //--- Prepare response text for BTN_CLOSE
      Print("RESPONSE = ", response_text); //--- Log the response
      CTrade obj_trade; //--- Create a trade object
      int totalOpenBefore = PositionsTotal(); //--- Get the total number of open positions before closing
      obj_trade.PositionClose(_Symbol); //--- Close the position for the symbol
      int totalOpenAfter = PositionsTotal(); //--- Get the total number of open positions after closing
      //--- Construct the message with position closure details
      string message = "\xF62F\Closed Position:\n";
      message += "Total Positions (Before): "+(string)totalOpenBefore+"\n";
      message += "Total Positions (After): "+(string)totalOpenAfter+"\n";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }
   else if (cb_query.data == BTN_NEXT) {
      response_text = "You clicked "+BTN_NEXT+"!"; //--- Prepare response text for BTN_NEXT
      Print("RESPONSE = ", response_text); //--- Log the response
      
      string message = "Choose Still More Options Below:\n"; //--- Message to prompt for further options
      message += "More Options"; //--- Title for more options
      //--- Define inline buttons for additional options
      string buttons = "[[{\"text\": \""+EMOJI_UP+"\", \"callback_data\": \""+EMOJI_UP+"\"}, {\"text\": \"Contact\", \"callback_data\": \""+BTN_CONTACT+"\"}, {\"text\": \"Join\", \"callback_data\": \""+BTN_JOIN+"\"},{\"text\": \""+EMOJI_PISTOL+"\", \"callback_data\": \""+EMOJI_PISTOL+"\"}]]";
      sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); //--- Send the inline keyboard markup
   }
   else if (cb_query.data == BTN_CONTACT) {
      response_text = "You clicked "+BTN_CONTACT+"!"; //--- Prepare response text for BTN_CONTACT
      Print("RESPONSE = ", response_text); //--- Log the response
      string message = "Contact the developer via link below:\n"; //--- Message with contact link
      message += "https://t.me/Forex_Algo_Trader";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }
   else if (cb_query.data == BTN_JOIN) {
      response_text = "You clicked "+BTN_JOIN+"!"; //--- Prepare response text for BTN_JOIN
      Print("RESPONSE = ", response_text); //--- Log the response
      string message = "You want to be part of our MQL5 Community?\n"; //--- Message inviting to join the community
      message += "Welcome! <a href=\"https://t.me/forexalgo_trading\">Click me</a> to join.\n";
      message += "<s>Civil Engineering</s> Forex AlgoTrading\n"; //--- Strikethrough text
      message += "<pre>This is a sample of our MQL5 code</pre>\n"; //--- Preformatted text
      message += "<u><i>Remember to follow community guidelines!\xF64F\</i></u>\n"; //--- Italic and underline text
      message += "<b>Happy Trading!</b>\n"; //--- Bold text
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
   }
   else if (cb_query.data == EMOJI_PISTOL) {
      response_text = "You clicked "+EMOJI_PISTOL+"!"; //--- Prepare response text for EMOJI_PISTOL
      Print("RESPONSE = ", response_text); //--- Log the response
      string message = "Choose More Options Below:\n"; //--- Message to prompt for more options
      message += "Trading Operations"; //--- Title for trading operations
      //--- Define inline buttons for additional trading options
      string buttons = "[[{\"text\": \""+EMOJI_UP+"\", \"callback_data\": \""+EMOJI_UP+"\"}],"
                        "[{\"text\": \"Buy\", \"callback_data\": \""+BTN_BUY+"\"}, {\"text\": \"Close\", \"callback_data\": \""+BTN_CLOSE+"\"}, {\"text\": \"Next\", \"callback_data\": \""+BTN_NEXT+"\"}]]";
      sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); //--- Send the inline keyboard markup
   }
   else if (cb_query.data == BTN_SCREENSHOT) {
      response_text = "You clicked "+BTN_SCREENSHOT+"!"; //--- Prepare response text for BTN_SCREENSHOT
      Print("RESPONSE = ", response_text); //--- Log the response
      
      string message = "Okay. Command 'get Current Chart Screenshot' received.\n"; //--- Message acknowledging screenshot command
      message += "Screenshot sending process initiated \xF60E"; //--- Emoji indicating process initiation
      sendMessageToTelegram(cb_query.chat_id, message, NULL); //--- Send the message
      string caption = "Screenshot of Symbol: "+_Symbol+ //--- Caption for screenshot
                       " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+ //--- Timeframe
                       ") @ Time: "+TimeToString(TimeCurrent()); //--- Current time
      //--- Send the screenshot to Telegram
      sendScreenshotToTelegram(cb_query.chat_id, _Symbol, _Period, caption);
   }
   else {
      response_text = "Unknown button!"; //--- Prepare response text for unknown buttons
      Print("RESPONSE = ", response_text); //--- Log the response
   }
   
   //--- Optionally, reset the inline button state by removing the keyboard
   // removeInlineButtons(member_token, cb_query.chat_id, cb_query.message_id);
}


//+------------------------------------------------------------------+
//| Create a custom inline keyboard markup for Telegram              |
//+------------------------------------------------------------------+
string customInlineKeyboardMarkup(const string buttons){
   //--- Construct the JSON string for the inline keyboard markup
   string result = "{\"inline_keyboard\": " + UrlEncode(buttons) + "}"; //--- Encode buttons as JSON
   return(result);
}

//+------------------------------------------------------------------+
//| Remove inline buttons by editing message reply markup            |
//+------------------------------------------------------------------+
void removeInlineButtons(string memberToken, long chatID, long messageID){
   //--- Reset the inline button state by removing the keyboard
   string url = TELEGRAM_BASE_URL + "/bot" + memberToken + "/editMessageReplyMarkup"; //--- API URL to edit message
   string params = "chat_id=" + IntegerToString(chatID) + //--- Chat ID parameter
                 "&message_id=" + IntegerToString(messageID) + //--- Message ID parameter
                 "&reply_markup=" + UrlEncode("{\"inline_keyboard\":[]}"); //--- Empty inline keyboard
   string response;
   int res = postRequest(response, url, params, WEB_TIMEOUT); //--- Send request to Telegram API
}

Итак, мы управляем callback-запросами, отвечая на нажатия определенных кнопок и направляя пользователей к соответствующим опциям встроенной клавиатуры. Это улучшает взаимодействие, предоставляя контекстно-зависимые сообщения и варианты. Далее мы протестируем интеграцию, чтобы убедиться, что функции работают так, как должны, и что взаимодействия обрабатываются правильно.


Тестирование реализации состояний встроенных кнопок

В этом разделе мы проверим, насколько хорошо встроенные кнопки взаимодействуют с Telegram-ботом и MQL5-советником. Этот процесс будет включать в себя имитацию действий пользователя, таких как нажатие кнопок, а также правильную обработку callback-запросов. Мы оценим правильность отображения, удаления или обновления встроенных кнопок на основе взаимодействия с пользователем. Для большей ясности я создал видеоролик, демонстрирующий работу интеграции и пошаговое поведение бота при нажатии на встроенные кнопки. Это гарантирует, что настройки будут работать так, как и ожидалось, в режиме реального времени.

Мы протестировали взаимодействие кнопок и запросов обратного вызова, чтобы убедиться, что бот правильно обрабатывает вводимые пользователем данные и что состояние встроенных кнопок обновляется или сбрасывается по мере необходимости. Это обеспечивает нелинейный стиль взаимодействия, который повышает вовлеченность и обеспечивает более эффективный опыт управления ботом через Telegram.


Заключение

Мы внедрили и протестировали callback-запросы и встроенные кнопки в Telegram-бота. Теперь бот может отвечать на действия пользователя персонализированными сообщениями и предлагать интерактивные возможности с помощью встроенной клавиатуры. Пользовательский опыт был улучшен за счет добавления простых в использовании кнопок в режиме реального времени для таких действий, как доступ к меню, получение информации о советнике или выполнение команд, связанных с торговлей.

Система протестирована и работает так, как и задумано, правильно обрабатывая каждый callback-запрос и предоставляя пользователям необходимую обратную связь. При этом бот сохраняет необходимое качество общения, что помогает поддерживать интерес пользователя и удобство использования. Мы видим, что встроенные кнопки более эффективны, поскольку они не загромождают поле чата. Надеюсь, статья оказалась достаточно подробной и понятной.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/15823

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
Clemence Benjamin
Clemence Benjamin | 12 сент. 2024 в 23:04

Это фантастика! Вы постоянно предоставляете ценные идеи, и я это очень ценю. Спасибо, уважаемый сэр Аллан.

Allan Munene Mutiiria
Allan Munene Mutiiria | 13 сент. 2024 в 02:44
Clemence Benjamin #:

Это фантастика! Вы постоянно предоставляете ценные идеи, и я это очень ценю. Спасибо, уважаемый сэр Аллан.

@Clemence Benjamin это так мило с вашей стороны. Спасибо за добрый отзыв и признание. Мы очень рады вам.
Javier Santiago Gaston De Iriarte Cabrera
Javier Santiago Gaston De Iriarte Cabrera | 13 сент. 2024 в 22:39
Отличная работа! Спасибо!
Allan Munene Mutiiria
Allan Munene Mutiiria | 15 сент. 2024 в 22:38
Javier Santiago Gaston De Iriarte Cabrera #:
Отличная работа! Спасибо!

@Javier Santiago Gaston De Iriarte Cabrera спасибо за добрый отзыв и признание. Мы вам очень рады.

Возможности Мастера MQL5, которые вам нужно знать (Часть 38): Полосы Боллинджера Возможности Мастера MQL5, которые вам нужно знать (Часть 38): Полосы Боллинджера
Полосы Боллинджера — очень распространенный индикатор конвертов, используемый многими трейдерами для ручного размещения и закрытия сделок. Мы изучим этот индикатор, рассмотрев как можно больше различных сигналов, которые он генерирует, и посмотрим, как их можно использовать в советнике, собранном с помощью Мастера.
Нейросети в трейдинге: Иерархия навыков для адаптивного поведения агентов (HiSSD) Нейросети в трейдинге: Иерархия навыков для адаптивного поведения агентов (HiSSD)
Предлагаем познакомиться с фреймворком HiSSD, который объединяет иерархическое обучение и мультиагентные подходы для создания адаптивных систем. В этой работе мы подробно рассмотрим, как этот инновационный подход помогает выявлять скрытые закономерности на финансовых рынках и оптимизировать стратегии торговли в условиях децентрализации.
Машинное обучение в однонаправленной трендовой торговле на примере золота Машинное обучение в однонаправленной трендовой торговле на примере золота
В данной статье рассматривается подход к торговле только в выбранном направлении (на покупку или на продажу). Для этого используется техника причинно-следственного вывода и машинное обучение.
Переходим на MQL5 Algo Forge (Часть 1): Создание основного репозитория Переходим на MQL5 Algo Forge (Часть 1): Создание основного репозитория
В процессе работы над проектами в MetaEditor разработчики сталкиваются с необходимостью управления версиями кода. Несмотря на планы по переходу на GIT и запуск MQL5 Algo Forge, интеграция еще не завершена. В статье рассматриваются возможные способы повышения удобства работы с текущими инструментами.