Work with the Telegram chatbot from 1C. Interaction between a user and the bot

Alexander Biryukov

23.01.2020 27 min

In the first part, we developed a Telegram bot and learned how to transfer the simplest messages between the bot and a program based on the 1C platform. Well, now it’s time to complicate our solution. Let’s add some more features for the interaction between a user and the bot.

How might it work? For example, a user sends a command to the bot, and the bot responds with some actions – displays a list with all available commands, or generates and displays a report with the necessary data, or asks the user a question and suggests him/her several options to answer. You can create a lot of different scenarios for using the bot!

In our case, just using an external data processor is not enough, so we create a new empty configuration and name it TelegramBot.

Right here we add all the required objects to this configuration:

1. Telegram subsystem.

2. Common_Server common module. Note that the module must be server-based and support server calls.


3. Two enumerations: KindsMessage (with Incoming and Outcoming values) and TypesCommand (with Message, Poll and File values)


4. SystemCommands catalog where we will save the commands understandable by our bot.

Here we also add the following attributes:

  • CommandName, String 25 type

  • Source, String with non-limited length type

  • TypeCommand, EnumRef.TypesCommand type


Do not forget to add the catalog to the subsystem:


We add a /help command as a predefined catalog item – on this command the bot will return a list of all available commands.


Then we create an Element form for the catalog and place there our attributes as follows:

 

For the Source attribute, we manually set the maximum width and the maximum height:


5. Two information registers: MessageHistory – it will store the whole interaction history, Settings – it will store the settings for connecting to Telegram.

The first register must have the following attributes:

  • ID_Message, Number 15 type

  • KindMessage, EnumRef.KindsMessage type

  • ChatID, Number 15 type

  • Command, CatalogRef.SystemCommands type

  • Message, String with non-limited length type

For the second register, the attributes are the following:

  • api, String 150 type

  • token, String 200 type


Also, do not forget to add both registers to the subsystem:

6. Create the AllMessages report built on the data composition system (DCS)

This report itself is very simple – it displays all the records from the MessageHistory register, i.e. just displays the entire message history.

In the future, this report will be automatically sent by the bot at the user’s request.

Let us briefly describe the generation of this report:

We create a new report in the configuration, indicate its name and click the Open data composition schema button.


In the next window, click Finish:


The wizard for report creation will be opened. Now it’s required to add a data source, in this case, it will be a query:


Then the wizard will be converted to the following form:


Now we need to insert the following code in the Query window:

SELECT

     MessageHistory.Period AS Period,

     MessageHistory.ID_Message AS ID_Message,

     MessageHistory.KindMessage AS KindMessage,

     MessageHistory.Command AS Command,

     MessageHistory.Message AS Message

FROM

     InformationRegister.MessageHistory AS MessageHistory

After that, the wizard will be converted to the following form:


The sense of the query is to make a selection of all the records from the MessageHistory register.

In the wizard, we go to the Settings tab and click the Open setting wizard button:


Click Next and in the next window indicate the fields we want to be displayed in the report, in our case we select all the fields: 

 

Two more times click Next and in the last window, we select order fields. In our case we need the Period field – it means that the data will be displayed in chronological order:


After that, click OK and close the wizard for report creation. The AllMessage report is ready. We will need it later...

7. And the last object that we need to create is an external data processor DataProcess. There we will carry out the main work. 

So, let’s create a new external data processor and add a form: 


For the form it’s necessary to add three commands: LaunchingWaitingHandler, ReadMessages and SendMessage:

As well as the SendingText attribute of the String with non-limited length type:


Well, we’ve completed all the pre-development activities, now let’s proceed to the coding.

We need to write processors for the commands. Processor for the ReadMessages command looks like as follows:

&AtClient

Procedure ReadMessages(Command)

     ReadMessagesAtServer();

EndProcedure

&AtServer

Procedure ReadMessagesAtServer()

     Common_Server.ReadMessages();

EndProcedure


As you can see, the main code is implemented in the Common_Server common module, so let’s go there.

The main module procedure is ReadMessages():

Procedure ReadMessages() Export

     structureSettings = GetSettingsTelegram();

     RequestText = "bot" + structureSettings.token + "/getUpdates";

     HTTPConnection = New HTTPConnection(structureSettings.api,443,,,,,New OpenSSLSecureConnection());

     HTTPRequest = New HTTPRequest(RequestText);

     HTTPRequest.Headers.Insert("Content-type", "application/json");

     HTTPAnswer = HTTPConnection.Get(HTTPRequest);

     If HTTPAnswer.StatusCode = 200 Then

           JSONReader = New JSONReader();

           JSONReader.SetString(HTTPAnswer.GetBodyAsString());

           Result = ReadJSON(JSONReader);

           JSONReader.Close();

           If Result.Property("result") then

                 arrayMessages = Result.result;

                 For Each structureMessage In arrayMessages Do

                       MessageID = structureMessage.update_id;

                       If CheckAvailabilityMessagesByID(MessageID) Then

                            Continue;

                       EndIf;

                       If structureMessage.Property("message") Then

                            ChatID = structureMessage.message.chat.id;

                            Command = GetCommandByName(structureMessage.message.text);

                            SaveMessagesHistory(structureMessage.message.text, MessageID, Command, ChatID);

                            ProcessUserResponse(Command, structureSettings, ChatID, MessageID);

                       ElsIf structureMessage.Property("callback_query") Then

                            ChatID = structureMessage.callback_query.message.chat.id;

                            Command = GetCommandByName(structureMessage.callback_query.data);

                            SaveMessagesHistory(structureMessage.callback_query.data, MessageID, Command, ChatID);

                            ProcessUserResponse(Command, structureSettings, ChatID, MessageID);

                       EndIf;

                 EndDo;

           EndIf;

     EndIf;

EndProcedure

The code is quite simple. At first, we get settings for connecting to Telegram (structureSettings = GetSettingsTelegram()), then the system makes an HTTP request to Telegram which returns us a list of recent messages. If the request is correct (HTTPAnswer.StatusCode = 200), we process the received answer from the server.

Since the request always returns us a list of all recent messages, we need to place each already read message to the register and check in the future if we’ve received a new message or not. The following code is responsible for that:

If CheckAvailabilityMessagesByID(MessageID) Then

Continue;

EndIf;

If we have a new message, the program goes on processing it, and here are two options:

  • structureMessage.Property("message") or

  • structureMessage.Property("callback_query").

In the first option, it is a simple message for the user, in the second one, it is feedback with the Telegram user – button click processing.

The command processing method in both condition branches is identical, so we are going to describe only the first condition branch:

ChatID = structureMessage.message.chat.id;

Command = GetCommandByName(structureMessage.message.text);

SaveMessagesHistory(structureMessage.message.text, MessageID, Command, ChatID);

ProcessUserResponse(Command, structureSettings, ChatID, MessageID);

We get ChatID, next we try to find a command in the command catalog by its name (GetCommandByName(structureMessage.message.text)), then the system places the message into the MessageHistory register (SaveMessagesHistory(structureMessage.message.text, MessageID, Command, ChatID);), and only in the end performs command processing, i.e. executes some pre-programed actions (ProcessUserResponse(Command, structureSettings, ChatID, MessageID);).

Now let’s look through the source code of all the above procedures and functions. The first function, GetSettingsTelegram, returns settings for connecting to the Telegram bot which are stored in the Settings register:

Function GetSettingsTelegram() Export

     structureSettings = New Structure;

     structureSettings.Insert("token","");

     structureSettings.Insert("api","");

     Query = New Query;

     Query.Text = "SELECT

                  |    Settings.token AS token,

                  |    Settings.api AS api

                  |FROM

                  |    InformationRegister.Settings AS Settings";

     Selection = Query.Execute().Select();

     If Selection.Count() > 0 Then

           Selection.Next();

           FillPropertyValues(structureSettings, Selection);

     EndIf;

     Return structureSettings;

EndFunction

The next function, CheckAvailabilityMessagesByID, checks whether a message is new or not. The query is the message ID, the function returns True or False.

Function CheckAvailabilityMessagesByID(MessageID)

     Query = New Query;

     Query.Text = "SELECT

                    |  MessageHistory.Period AS Period

                    |FROM

                    |  InformationRegister.MessageHistory AS MessageHistory

                    |WHERE

                    |  MessageHistory.ID_Message = &MessageID";

     Query.SetParameter("MessageID",MessageID);

     Selection = Query.Execute().Select();

     If Selection.Count() > 0 Then

           Return True;

     Else

           Return False;

     EndIf;

EndFunction

The GetCommandByName function searches for a command in the SystemCommand catalog by its name. If the command is not found in the catalog, it returns an empty reference:

Function GetCommandByName(CommandName)

     Query = New Query;

     Query.Text = "SELECT

                  |    SystemCommands.Ref AS Ref

                  |FROM

                  |    Catalog.SystemCommands AS SystemCommands

                  |WHERE

                  |    SystemCommands.CommandName = &CommandName";

     Query.SetParameter("CommandName", Lower((CommandName)));

     Selection = Query.Execute().Select();

     if Selection.Count() > 0 Then

           Selection.Next();

           Return Selection.Ref;

     Else

           Return Catalogs.SystemCommands.EmptyRef();

     EndIf;

EndFunction

The SaveMessagesHistory procedure writes the current message and some additional data to the MessageHistory register:

Procedure SaveMessagesHistory(Message, MessageID, Command, ChatID)

     RecordManager = InformationRegisters.MessageHistory.CreateRecordManager();

     RecordManager.Period          = CurrentDate();

     RecordManager.Message         = TrimAll(Message);

     RecordManager.Command         = Command;

     RecordManager.KindMessage     = Enums.KindsMessage.Incoming;

     RecordManager.ID_Message      = MessageID;

     RecordManager.ChatID          = ChatID;

     RecordManager.Write(True);

EndProcedure

The most important procedure is ProcessUserResponse since it performs data processing and generates a response for the user:

Procedure ProcessUserResponse(Command, structureSettings, ChatID, MessageID)

      If Not ValueIsFilled(Command) Then

           SystemAnswer = "Sorry, I don’t know this command" + Chars.LF + Chars.LF;

           SystemAnswer = SystemAnswer + GetListOfAllCommand();

     Else

           FileName          = "";

           SystemAnswer      = "";                                                                                                         

           Execute(Command.Source);

     EndIf;       

     If Command.TypeCommand = Enums.TypesCommand.File Then

           reportResult = New SpreadsheetDocument;

           Reports.AllMessages.Create().ComposeResult(reportResult);

           FileName = GetTempFileName("xls");

           reportResult.Write(FileName, SpreadsheetDocumentFileType.XLS);;

           Boundary = "----" + String(New UUID());

           ArrayFilesToMerge = New Array;

           FileNameSendBegin = GetTempFileName("txt");

           FileSendBegin = New TextWriter(FileNameSendBegin, TextEncoding.UTF8);

           FileNameSendEnd = GetTempFileName("txt");

           FileSendEnd = New TextWriter(FileNameSendEnd, TextEncoding.UTF8);

           SendingText = "";

           SendingText = SendingText + "--" + Boundary + Chars.LF;

           SendingText = SendingText + "Content-Disposition: form-data; name=""chat_id""" + Chars.LF + Chars.LF;

           SendingText = SendingText + StrReplace(Format(ChatID, "NFD=; NS=; NGS=."), ".", "") + Chars.LF;

           SendingText = SendingText + "--" + Boundary + Chars.LF;

           SendingText = SendingText + "Content-Disposition: form-data; name=""document""; filename=""report.xls""" + Chars.LF;

           FileSendBegin.WriteLine(SendingText );

           FileSendBegin.Close();

           ArrayFilesToMerge.Add(FileNameSendBegin);

           ArrayFilesToMerge.Add(TrimAll(FileName));

           SendingText = "" + Chars.LF;

           SendingText = SendingText + "--" + Boundary + "--";

           FileSendEnd.WriteLine(SendingText);

           FileSendEnd.Close();

           ArrayFilesToMerge.Add(FileNameSendEnd);

           FileNameSend = GetTempFileName("txt");

           MergeFiles(ArrayFilesToMerge, FileNameSend);

 

           HTTPRequest = New HTTPRequest;

           Headers     = New Map;

          

           HTTPRequest.Headers.Insert("Connection", "keep-alive");

           HTTPRequest.Headers.Insert("Content-Type", "multipart/form-data; boundary=" + Boundary);

          

           HTTPRequest.SetBodyFileName(FileNameSend);

           HTTPRequest.ResourceAddress = "/bot" + structureSettings.token + "/sendDocument";

          

           HTTPConnection = New HTTPConnection(structureSettings.api,,,,,, New OpenSSLSecureConnection());

           Try

                 HTTPAnswer = HTTPConnection.Post(HTTPRequest);

           Except     

           EndTry;

     ElsIf Command.TypeCommand = Enums.TypesCommand.Poll Then

           SystemAnswer = Command.Description;

           Receiver = "bot" + structureSettings.token + "/sendMessage?chat_id=" + StrReplace(Format(ChatID, "NFD=; NS=; NGS=."), ".", "") + "&text=" + SystemAnswer;

           HTTPConnection = New HTTPConnection(structureSettings.api,443,,,,,New OpenSSLSecureConnection());

           HTTPRequest       = New HTTPRequest(Receiver);

           HTTPAnswer = HTTPConnection.Get(HTTPRequest);

           SaveStory(SystemAnswer, Command, MessageID);

     Else  

           Receiver = "bot" + structureSettings.token + "/sendMessage?chat_id=" + StrReplace(Format(ChatID, "NFD=; NS=; NGS=."), ".", "") + "&text=" + SystemAnswer;

           HTTPConnection = New HTTPConnection(structureSettings.api,443,,,,,New OpenSSLSecureConnection());

           HTTPRequest       = New HTTPRequest(Receiver);

           HTTPAnswer = HTTPConnection.Get(HTTPRequest);

           SaveStory(SystemAnswer, Command, MessageID);

     EndIf;

EndProcedure

The sense of the procedure is that the response to the user is generated in the form of a SystemAnswer variable which is then sent back to the user.

We send a command to the system, if this command was not previously found in the Command catalog, then it answers: “Sorry, I don’t know this command”, and additionally displays a list of all existing commands in the system (GetListOfAllCommand()).

Further processing is divided depending on the type of command received.

The system first processes a command of the File type. In the given example, it is strictly programmed that the AllMessages report is automatically generated in this case. Then the report is saved in the XLS format and sent through HTTP to the user.

Processing of commands of the Poll and Message types is practically equal, the difference lies in the principle of filling the SystemAnswer variable. For the rest, the code is identical, all the required data is also sent via HTTP.

For the moment we almost have everything done to start testing out the program, but before that, let’s introduce some more details into the external DataProcess data processor.

Note that in order to read new messages a user will have to constantly click the Read messages button. Let’s make 1C itself periodically read new messages.

We return to our external data processor and code a handler for the LaunchingWaitingHandler command:

&AtClient

Procedure LaunchingWaitingHandler(Command)

     AttachIdleHandler("WaitProcessing", 2);

EndProcedure

&AtClient

Procedure WaitProcessing()

     Common_Server.ReadMessages();

EndProcedure

What is this code for?

We launch waiting handler which calls the WaitProcessing procedure every two seconds. Then, this procedure reads messages Common_Server.ReadMessages(), i.e. our program connects to Telegram every two seconds, reads the new message (if any) and somehow responds to them.

Well, now everything is ready! Start 1C.

Introduce our settings for the Telegram bot into Settings:


Open the System commands catalog and customize it.

If you remember, there is one predefined command in this catalog which returns a list of all available commands in the system – /help. Open settings of this command and customize it as follows:


Type command is Message, description is List of available command. Next, write the following code in the Source space:

SystemAnswer = "";

Query = New Query;

     Query.Text =

"SELECT

|    SystemCommands.CommandName AS CommandName,

|    SystemCommands.Description AS Description

|FROM

|    Catalog.SystemCommands AS SystemCommands

|WHERE

|    NOT SystemCommands.TypeCommand = VALUE(ENUM.typescommand.poll)

|

|ORDER BY

|    SystemCommands.Code";

QueryResult = Query.Execute();

SelectionDetailRecords = QueryResult.Select();

While SelectionDetailRecords.Next() Do

    SystemAnswer = SystemAnswer + " " + SelectionDetailRecords.CommandName + " - " + SelectionDetailRecords.Description + Chars.LF;

EndDo;

The code is simple – it also generates the SystemAnswer variable. To do this, the code executes a query to the SystemCommands catalog and returns all current commands (except for commands of the Poll type).

In general, in the Source space of the SystemCommands catalog, you can write almost any algorithm capable to perform some actions, and these actions will be fulfilled when this command is received from the user.

Let’s add some more commands to the catalog:


The /who are you and /where are you from commands return respectively I’m Telegram bot and I’m from London answers.

The /report command doesn’t contain any source code (the Source space is empty), this command is processed separately, it’s precisely that command that will generate and send the report that we created earlier.

So, we have everything ready to test our program! Open the DataProcess processor:


and click Launching the waiting handler:


Our program begins to connect to Telegram every 2 seconds and try to read new messages.

Now let’s proceed to Telegram on a mobile device. Enter our bot and send him any command, it doesn’t matter what exactly. In response, the bot informs us that he doesn’t know such a command and displays a list of commands that we earlier added to the catalog:


Now let’s introduce, for example, the /who_are_you command – the bot will respond:


Next, let’s test the /report command. In response, the bot sends us a file in .xls format where all already read messages are stored:


Thus, by adding the necessary commands in the SystemCommands catalog it’s possible to implement fairly complicated program algorithms.

However this is not all the capabilities of the Telegram bot, and, of course, not all the features of the 1C platform.

For example, we want to carry out a small survey among our clients and then process the survey results. It’s very easy!

We return to the Designer, open DataProcess and write a handler for the Send Message command in the processor’s form:


The handler’s source code in the form:

&AtClient

Procedure SendMessage(Command)

     Common_Server.SendMessage(SendingText);

EndProcedure

The source code for the Send Message procedure in the common module:

Procedure SendMessage(SendingText) Экспорт

     structureSettings = GetSettingsTelegram();

     arrayButtons = New Array;

     arrayButtons.Add("Yes");

     arrayButtons.Add("No");

     Buttons = New Array;

     For Each Button In arrayButtons Do

           Buttons.Add(New Structure("text, callback_data", Button, StrReplace(Button, " ", "")));

     EndDo;

     Lines = New Array;

     Lines.Add(Buttons);

     ButtonsJSON = JSONWrite(New Structure("inline_keyboard", Lines));

     Query = New Query;

     Query.Text = "SELECT

                  |    MessageHistory.ChatID AS ChatID

                  |FROM

                  |    InformationRegister.MessageHistory AS MessageHistory

                  |WHERE

                  |    NOT MessageHistory.ChatID = 0

                  |

                  |GROUP BY

                  |    MessageHistory.ChatID";

    

     Selection = Query.Execute().Select();

     While Selection.Next() Do

           Receiver = "bot" + structureSettings.token + "/sendMessage?chat_id=" + StrReplace(Format(Selection.ChatID, "NFD=; NS=; NGS=."), ".", "") + "&text=" + SendingText + "&reply_markup=" + ButtonsJSON;

           HTTPConnection = New HTTPConnection(structureSettings.api,443,,,,,New OpenSSLSecureConnection());

           HTTPRequest       = New HTTPRequest(Receiver);

           HTTPAnswer = HTTPConnection.Get(HTTPRequest);

           SaveStory(SendingText, Catalogs.SystemCommands.Help,"");

     EndDo;

EndProcedure

Function JSONWrite(structureJSON)

     JSONWriter = New JSONWriter;

     JSONWriter.SetString();

     WriteJSON(JSONWriter, structureJSON);

     Return JSONWriter.Close();

EndFunction

This procedure sends the user not only some text SendingText but additionally Yes and No buttons that will be displayed on the user’s mobile device and clicking these buttons can also be processed later.

Let’s return to the System commands catalog and create there two more commands – Yes and No of the Poll type:


Now let’s test how this mechanism works. Open DataProcess again and write a text of the survey that we want to ask our customers in the SendingText field:


This question almost immediately appears on the mobile device:


Now, depending on the answer that we select, this o another command from the catalog will work:


Well, here we can stop, since all the above code makes it possible to create a bot that will perform fairly complicated logic.

However, as you remember, there is the AttachIdleHandler("WaitProcessing", 2) code in the DataProcess processor.

This code is not optimal in respect of neither productivity nor architecture.


You can download this Example (*.cf) for your own application.

Please, supplement your suggestions for the development of demo applications in our forum.


What can we improve here and how? This is the theme of the next article. Stay with us! :-)

Be the first to know tips & tricks on business application development!

A confirmation e-mail has been sent to the e-mail address you provided .

Click the link in the e-mail to confirm and activate the subscription.