English Русский Español Deutsch 日本語
preview
创建 MQL5-Telegram 集成 EA 交易(第 6 部分):添加响应式内联按钮

创建 MQL5-Telegram 集成 EA 交易(第 6 部分):添加响应式内联按钮

MetaTrader 5交易系统 | 12 六月 2025, 07:40
163 4
Allan Munene Mutiiria
Allan Munene Mutiiria

概述

本文深入探讨如何通过 Telegram 使我们的 MetaQuotes Language 5 (MQL5) EA 交易对用户更具响应性和互动性。在本系列的第五部分中,我们通过实现对 Telegram 命令和消息的响应能力以及创建自定义键盘按钮,为我们的机器人奠定了基础。在本节中,我们将通过集成触发各种操作并动态响应用户输入的内联按钮来提高机器人的交互性。

本文旨在探讨几个关键组成部分。首先,我们将介绍 Telegram 机器人中的内联按钮,包括它们是什么、它们的用处以及它们相对于创建机器人界面的其他方法所提供的好处。然后,我们将转而讨论如何在 MQL5 中使用这些内联按钮,以便它们可以成为我们 EA 交易用户界面的一部分。 

从那里,我们将演示如何处理用户按下按钮时从 Telegram 发送的回调查询。这将涉及处理用户的行为,并确定机器人在与用户的对话中下一个适当的步骤是什么。最后,我们将测试机器人的内置功能,以确保一切都能完美运行。以下是我们将在文章中讨论的主题:

  1. Telegram 机器人内联按钮简介
  2. 将内联按钮集成到 MQL5
  3. 处理按钮操作的回调查询
  4. 测试内联按钮状态的实现
  5. 结论

在本文结束时,您将清楚地了解如何在 MQL5 Telegram EA 交易中集成和管理内联按钮,增强交易机器人的功能,并使其对用户的响应更快、互动性更强。那我们就开始吧。


Telegram 机器人内联按钮简介

内联按钮是直接出现在 Telegram 机器人消息中的交互式元素,允许用户只需点击一下即可执行操作。这些按钮利用 JavaScript 对象表示法(JSON) 格式的内联键盘标记来定义其外观和行为,与传统方法相比,提供了更加集成和响应迅速的界面。通过将这些按钮直接嵌入消息中,机器人可以为用户提供简化的体验和即时交互,而不需要额外的文本命令或消息。为了深入了解我们到底在说什么,我们提供了一个内联按钮的可视化图示,如下所示:

内联按钮图示

内联按钮相对于传统回复键盘的主要优势在于它们能够保持在消息本身中,使交互更加无缝和上下文相关。内联按钮,使用 JSON 结构定义,允许复杂的用户交互和动态响应。这种方法消除了对单独菜单或附加消息的需要,从而通过提供即时反馈和操作来减少混乱并提高用户参与度。有了这些见解,我们现在可以开始在 MQL5 中为 MetaTrader 5 实现它们,如下一节所示。


将内联按钮集成到 MQL5

将内联按钮合并到我们的 MQL5 EA 交易中需要一个可以处理消息交互和按钮各种状态的框架。这是通过创建和管理几个类来实现的,每个类负责处理调用和消息的不同部分。我们将详细解释集成,包括使用的类和函数,更重要的是,为什么使用它们以及它们如何通过内联按钮为机器人的功能做出贡献。我们要做的第一件事是设计一个类,它可以封装从 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 EA 交易中的消息数据至关重要。

在这个类中,我们包含了几个捕获消息特定方面的公有属性。“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.
};

在定义了聊天类之后,我们需要定义一个额外的类来处理回调查询。它对于处理与回调查询相关的特定数据至关重要,回调查询不同于常规消息。回调查询提供独特的数据,例如回调数据和触发查询的交互,这些数据在标准消息中不存在。因此,该类将使我们能够有效地捕获和管理这些专门的数据。此外,它将允许我们以不同的方式处理用户与内联按钮的交互。这种分离将确保我们能够准确地处理和响应按钮按下,将其与其他类型的消息和交互区分开来。具体实现方式如下:

//+------------------------------------------------------------------+
//|        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” 存储发件人的 ID,这有助于识别触发回调的用户。我们使用 “from_first_name”、“from_last_name” 和 “from_username” 来跟踪发件人的姓名详细信息。

“message_id” 变量捕获与回调相关的消息的 ID,而 “message_text” 包含该消息的文本。“data” 保存通过内联按钮发送的回调数据,这对于根据按下的按钮确定要采取的操作至关重要。最后,“chat_id” 存储应发送响应的聊天 ID,确保回复到达正确的聊天上下文。除了我们现在需要包含一个用于处理回调查询的额外自定义函数之外,类的其余定义和初始化保持不变。

//+------------------------------------------------------------------+
//|        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” 的方法。此功能旨在从 Telegram API 中提取更新:由新消息或机器人尚未处理的回调查询组成的更新。“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” 变量来保存从 Telegram API 返回的响应数据。然后我们构建获取更新所需的 API URL。为此,我们将“TELEGRAM_BASE_URL”与其他几个组件结合起来: “/bot” 和机器人的令牌,保存在 “member_token” 中;以及 “/getUpdates”,这是我们从 Telegram 获取更新的端点。Telegram API 是我们应用程序使用的另一个主要平台的一部分,而 “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 函数来计算 “obj_json” 中 “result” 对象的 “m_elements” 数组中的元素数量。我们将计数存储在变量 “total” 中。接下来,我们建立一个循环,重复处理 “m_elements” 数组中的每个更新元素。总共有 “total” 个元素需要处理;因此,循环控制变量的范围是从 0 到 “total” 减 1。在每次迭代期间,循环控制变量的当前值 “i” 指示我们正在访问 “m_elements ”数组的哪个元素。我们将第 “i” 个元素分配给变量 “obj_item”。我们现在检查当前更新(“obj_item”)是否包含有效的 “消息”。

接下来,我们实例化一个名为 “obj_msg ”的 “Class_Message” 对象,它将保存相关消息的详细信息。我们在 “obj_msg” 中填充的第一个字段是其 “update_id” 字段。为此,我们从 “obj_item” 中提取 “update_id”,将其转换为整数,然后将其放入 “obj_msg.update_id” 中。我们在 “obj_msg” 中访问的下一个字段是它的 “message_id” 字段。对于这个值,我们再次从 “obj_item” 的 “message” 字段中提取信息。我们将 “obj_item” 的 “message_id” 字段中的值转换为整数,并将其放在 “obj_msg.message_id” 中。之后,我们用 “item” 的 “date” 值填充 “obj_msg” 的 “datetime” 字段。接下来,我们填充 “obj_msg” 中的 “message_text” 字段。我们从 “message” 中提取 “text” 值,将其转换为字符串,然后将其放在 “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.

在获取聊天详细信息后,我们将根据其关联的聊天 ID 继续处理消息。

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

提取并存储必要的消息详细信息后,我们更新最后处理的更新 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” 与消息的聊天 ID(“obj_msg.chat_id”)相同时,我们首先在循环期间将当前索引记录在变量 “index” 中,然后由于我们找到了正确的聊天,因此跳出该循环。当我们发现聊天没有匹配项并且 “index” 保持为 -1 时,我们会创建一个新的聊天对象,并使用 “Add” 方法将其放入“member_chats”中。然后,“GetLastNode” 帮助我们收获新创建的聊天对象,我们将其保存在指针 “chat” 中。我们将 “obj_msg.chat_id” 中的聊天 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.

我们从回调查询本身的细节开始。回调查询 ID 取自 “callback_query” 字段。我们使用 “ToStr” 方法将其转换为字符串格式,并将其存储在 “obj_cb_query.id” 中。我们提取的下一个信息是发件人的 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” 字段中获取消息 ID,将其转换为整数,并将其存储在 “obj_cb_query.message_id” 中。同时,消息文本也被提取出来并转换为字符串,存储在 “obj_cb_query.message_text” 中。文本中的任何特殊字符都会被解码。然后我们将注意力转向回调数据。我们将其提取出来,转换为字符串,然后存储在 “obj_cb_query.data” 中。与任何其他数据一样,回调数据是特殊字符编码的。

最后,我们从回调查询中获取发送消息的聊天的 ID,将其转换为整数,并将其放入 “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” 对象作为参数传递。此函数负责处理回调查询,并处理我们之前收集的提取细节,如用户信息、聊天 ID、消息文本和回调数据。通过调用此函数,我们确保回调查询根据其特定内容得到适当处理。

处理回调查询后,我们通过从 “obj_item” 字段中获取 “update_id”,将其转换为整数,然后为其加 1 来更新最后处理的更新 ID。该值存储在 “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”。当我们收到这样的命令时,我们会返回一条欢迎用户的消息,并告诉他们,“你可以通过内联按钮控制我!”然后,我们定义了一个内联回调按钮,我们希望将其作为用户的菜单选项。我们利用按钮的 “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"}].

最终格式:

按钮字符串的最终格式在代码中使用时为:“[ [{ "text":“Provide Menu”,“callback_data”:“BTN_MENU” }] ]”。此格式指定键盘上有一行,该行包含一个按钮。

当 Telegram API 接收到此 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 结构。我们的工作是构建一个 Telegram 可以用来呈现内联键盘的 JSON 对象。我们首先使用键 “inline_keyboard” 构建 JSON 结构。接下来,我们使用 “UrlEncode” 函数来处理 “buttons” 字符串中可能存在的任何特殊字符。这个编码步骤至关重要,因为如果没有它,我们可能会遇到按钮定义中的特殊字符问题。在附加编码的按钮字符串后,我们关闭JSON对象。结果字符串是内联键盘标记的有效 JSON 表示形式。我们返回此字符串,以便可以将其发送到 Telegram API,然后以交互方式在消息中呈现内联键盘。运行程序后,我们得到以下输出。

初始化消息

我们可以看到这是成功的。我们确实创建了内联按钮。然而,我们还无法对它的点击做出反应。因此,我们需要捕获收到的回调查询,并分别对点击做出响应。为了实现这一点,我们需要创建一个获取查询数据的函数。

//+------------------------------------------------------------------+
//|   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” 实例。首先,它记录回调查询的 ID,这是查询的唯一标识符,对于跟踪和管理查询至关重要。接下来,该函数记录 “member_token”。此令牌的作用是指示哪个机器人或成员正在处理回调,从而确保正确的机器人和只有一个机器人正在处理查询。

然后,我们分别使用 “cb_query.from_first_name” 和 “cb_query.from_last_name” 记录发件人的名字和姓氏。这些使我们能够了解按下内联按钮的用户的身份,并在将来需要与用户联系时提供个性化服务。说到身份,我们还使用 “cb_query.from_username” 记录发件人的用户名。这为我们将来在需要时直接与用户联系提供了另一种方式。记录发送者的身份后,我们使用 “cb_query.message_id” 记录与回调关联的消息的 ID。知道这个 ID 可以让我们知道按下按钮时所对应的具体消息。

此外,我们使用 “cb_query.message_text” 记录消息文本。这提供了单击按钮时的消息的上下文。我们还使用 “cb_query.data” 记录回调数据。这些数据是通过按钮点击发送回来的,用于根据用户的交互确定要采取的行动。通过记录这些详细信息,我们可以全面了解回调查询。这便于调试,并更好地理解用户与机器人的交互。一旦我们运行了程序,这些就是我们在交易终端中得到的输出。

MT5 消息

既然我们知道获取信息,我们就可以检查点击的按钮并相应地生成响应。在我们的例子中,让我们使用菜单按钮操作的回调数据。首先,我们将定义按钮常量。为了便于理解,我们添加了详细的注释。

#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” 来保存我们想要发送回用户的消息。然后我们检查回调查询(“cb_query.data”)中的 “callback_data” 是否与常量 “BTN_MENU” 匹配。如果是,我们将 “response_text” 设置为 “您点击了“+BTN_MENU+”!”,这确认按钮按下并包含所点击按钮的标识符。我们使用 “Print” 函数记录此响应以跟踪正在发送的内容。

接下来,我们使用 “sendMessageToTelegram” 函数将 “response_text” 消息发送到由 “cb_query.chat_id” 标识的聊天。由于我们在此阶段发送的是没有内联键盘的简单文本消息,因此第三个参数为 NULL ,表示不包含任何额外的键盘标记。

发送初始消息后,我们准备一条包含 “Information” 文本的新消息,它将为用户提供各种选项。然后,我们使用 “buttons” 字符串中类似 JSON 的结构定义内联按钮。该结构包括带有“获取 EA 名称”、“获取账户信息”、“获取当前市场报价”、“更多”、“屏幕截图”和“取消”等标签的按钮。每个按钮都分配有特定的 “callback_data” 值,例如 “BTN_NAME”、“BTN_INFO”、“BTN_QUOTES”、“BTN_MORE”、“BTN_SCREENSHOT” 和 “EMOJI_CANCEL”,这有助于识别按下了哪个按钮。

最后,我们使用 “sendMessageToTelegram” 函数将此新消息与内联键盘一起发送。内联键盘通过 “customInlineKeyboardMarkup” 函数格式化为 JSON,确保 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” 设置为 “您点击了“+ BTN_NAME +“!”这确认了按钮的按下,并包括所点击按钮的标识符。然后,我们使用 “Print” 函数输出此响应,以关注发送给用户的内容。

然后,我们创建一条新消息,传达有关机器人控制的 EA 文件的详细信息。这个由机器人生成的邮件开头是 “我控制的 EA 的文件名是:\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

成功了,我们现在继续处理“更多”内联按钮。在这种程度上,你可以看到我们不会用消息弄乱界面或聊天字段。它很干净,我们有效地重复使用了内联按钮。

   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”设置为 “您点击了“+BTN_MORE+”!”,这确认按钮按下并包含所点击按钮的标识符。使用 “Print” 函数记录此响应以跟踪正在发送的内容。

接下来,我们构建一条新消息,提示用户从其他选项中进行选择。“message” 变量以 “选择以下更多选项:\n”开头,后跟 “交易操作”,作为与交易相关的选项集的标题。然后,我们使用 “buttons” 字符串中类似 JSON 的结构为这些附加选项定义内联按钮。该结构包括:

  • 一个带有表情符号 “EMOJI_UP” 且其对应的 “callback_data” 为 “EMOJI_UP” 的按钮。
  • 一排按钮用于各种交易操作:“买入”、“平仓” 和 “下一个”,每个都有各自的 “callback_data”值“BTN_BUY”、“BTN_CLOSE” 和 “BTN_NEXT”。

最后,我们使用 “sendMessageToTelegram” 函数将此消息连同内联键盘一起发送到由 “cb_query.chat_id” 标识的聊天。内联键盘标记通过 “customInlineKeyboardMarkup” 函数格式化为 JSON。如果我们点击这个按钮,我们应该收到另一个扩展按钮,如下图所示:

更多按钮 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
   }

这将处理 “下一步” 字段中生成的按钮及其响应功能。然后,我们需要处理主内联按钮菜单上的屏幕截图按钮。

   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 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”(发送消息的聊天的 ID)和 “messageID”(包含内联按钮的消息的 ID)。首先,我们为 Telegram 的 “editMessageReplyMarkup” 方法构建 API 端点 URL。我们通过将 “TELEGRAM_BASE_URL” 与 “/bot” 和 “memberToken” 结合起来来实现这一点。这构成了我们用来与 Telegram 服务器通信的 URL。

然后我们指定 “params” 字符串,其中包含 API 调用所需的参数。我们包括 “chat_id” 参数。为了获取它的值,我们将变量 “chatID” 从整数转换为字符串。我们对 “message_id” 参数执行相同的操作。最后,我们通过发送一个空的 “reply_markup” 字段告诉 API 删除内联按钮。该字段的值是一个空的 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
   }

当我们点击取消按钮时,我们得到以下输出。

取消按钮 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
}

简单总结一下,我们通过回应某些按钮按下的行为并将用户引导至适当的内联键盘选项来管理回调查询。这通过提供符合上下文的信息和选项来增强交互。接下来,我们将测试集成,以确保这些功能正常工作,并且正确处理交互。


测试内联按钮状态的实现

在本节中,我们将验证内联按钮与 Telegram 机器人和 MQL5 EA 交易的交互效果。此过程将涉及模拟用户操作,例如按下按钮,并确保机器人正确处理回调查询。我们将根据用户交互评估内联按钮的正确显示、删除或更新。为了提供进一步的清晰度,我们制作了一段视频,演示了集成是如何工作的,展示了机器人在响应内联按钮按下时的逐步行为。这可确保设置实时按预期工作。下面是示意图。

我们测试了按钮交互和回调查询,以确保机器人能够准确地处理用户输入,并根据需要更新或重置内联按钮状态。这提供了一种非线性交互风格,增强了参与度,并在通过 Telegram 控制机器人时提供了更有效的体验。


结论

总而言之,我们已经在 Telegram 机器人中实施并测试了回调查询和内联按钮的通道。现在,机器人可以通过定制的消息来响应用户的输入,并通过内联键盘提供交互选项。通过添加实时、易于使用的按钮,用户体验得到了增强,这些按钮用于访问菜单、获取 EA 交易的信息或执行与交易相关的命令等操作。

我们已经测试了该系统,并且可以确认它按预期运行,正确处理每个回调查询并为用户提供他们需要的相关反馈。在做这些事情时,机器人仍然保持了对话的质量,这有助于保持用户的兴趣和可用性。我们可以看到,内联按钮更有效,因为它们不会像预期的那样弄乱聊天字段。我们希望您觉得这篇文章详细易懂。

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/15823

最近评论 | 前往讨论 (4)
Clemence Benjamin
Clemence Benjamin | 12 9月 2024 在 23:04

太棒了!您始终如一地提供有价值的见解,我非常感激。谢谢你,尊敬的艾伦 爵士。

Allan Munene Mutiiria
Allan Munene Mutiiria | 13 9月 2024 在 02:44
Clemence Benjamin #:

这太棒了!您始终如一地提供有价值的见解,我非常感激。谢谢您,尊敬的艾伦 爵士。

@Clemence Benjamin 你真好。感谢您的善意反馈和认可。我们非常欢迎您。
Javier Santiago Gaston De Iriarte Cabrera
干得好谢谢!
Allan Munene Mutiiria
Allan Munene Mutiiria | 15 9月 2024 在 22:38
Javier Santiago Gaston De Iriarte Cabrera #:
干得好谢谢!

@Javier Santiago Gaston De Iriarte Cabrera 感谢您的善意反馈和认可。非常欢迎您的到来。

您应当知道的 MQL5 向导技术(第 39 部分):相对强度指数 您应当知道的 MQL5 向导技术(第 39 部分):相对强度指数
RSI 是一款流行的动量震荡指标,衡量证券近期价格变化的速度和规模,从而评估证券价格中被高估和低估的情况。这些对速度和幅度的洞察是定义反转点的关键。我们将这个振荡器放入另一个自定义信号类中工作,并验证其信号的一些特征。不过,我们先从总结我们之前在布林带的内容开始。
在 MQL5 中创建交易管理员面板(第五部分):双因素认证(2FA) 在 MQL5 中创建交易管理员面板(第五部分):双因素认证(2FA)
今天,我们将讨论如何增强当前正在开发的交易管理员面板的安全性。我们将探讨如何在新的安全策略中实施 MQL5,并将 Telegram API 集成到双因素认证(2FA)中。本次讨论将提供有关 MQL5 在加强安全措施方面的应用的宝贵见解。此外,我们还将研究 MathRand 函数,重点关注其功能以及如何在我们构建的安全框架中有效利用它。继续阅读以了解更多信息!
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
Connexus助手(第五部分):HTTP方法和状态码 Connexus助手(第五部分):HTTP方法和状态码
在本文中,我们将了解HTTP方法和状态码,这是网络上客户端与服务器之间通信的两个非常重要的部分。了解每种方法的作用,可以让您更精确地发出请求,告知服务器您想要执行的操作,从而提高效率。