Simple Telegram bot demo

If you are new to Telegram bots first check the introduction to bots and the introduction to the bot API.

If you haven’t installed the package you may call either automated install with:

Get["https://raw.githubusercontent.com/dih5/TgBot/master/BootstrapInstall.m"]

Or online version with:

Get["https://raw.githubusercontent.com/dih5/TgBot/master/TgBot/TgBot.m"]

Needs["TgBot`"]

Path to a configuration file in JSON format

$BotConfigFile = FileNameJoin[{NotebookDirectory[],"demo.json"}];

Path to a temp directory used by the bot. Note this directory must exist.

$BotTmpDir = FileNameJoin[{NotebookDirectory[],"tmp"}];
If[Not[FileExistsQ[$BotTmpDir]],CreateDirectory[$BotTmpDir]]; (*Try to create the folder if it does not exist*)

The config file is a JSON where some parameters are defined. As a first example you may paste the following in the path selected by $BotConfigFile:

{
    “FriendList”: [ ],
    “Token”: “PasteYourTokenHere”,
    “MasterList”: [ ],
    “LastAnswered”: 1,
    “ErrorNoCmd”: “Please, send me a command.”,
    “ErrorUnknownCmd”: “I do not know such a command.”,
    “ErrorUnprivilegedCmd”: “You are not allowed to do that.”
}

Fields in the config file:
Token is the value authenticating your bot (see here). Get it and put it there.
MasterList is a list of integers identifying the users allowed to call any command (privilege 2). Might be empty []. Later you’d probably like to enter your user id in the MasterList. You can check it when the bot receives your messages.
FriendList is a list of integers identifying the users allowed to call some commands (privilege 1). Might be empty [].
Any user out of these lists will be assigned privilege 0.
If no risky command is allowed you may leave those empty and allow every user to execute any command.
LastAnswered is the integer identifying the last message answered. Messages are stored in the servers up to 24 hours, this is used to avoid answering multiple times when restarting the bot.
BEWARE: as such integer will eventually overflow the bot will stop answering when that happens. Manually change the parameter in the config file to 1 when that happens.

Apart from that, you can store any option in the config file. The sample file shows 3 strings, which are read by some default functions in the package.

The bot’s command list consists on:
1- A string identifying the command in Telegram. Messages starting with \thisString will be understood as calling this command. It should correspond to the ones registered in the bot (see here), but it is not mandatory.
2 - An element of {0, 1, 2} referring to the minimum privilege needed to execute the command. 0 = anyone, 1 = friends, 2 = masters.
3 - The Mathematica function associated with the command.
     These functions receive two JSON arguments: the message that initiated the call and the bot itself.
     They MUST return a list of elements:
     	i) A boolean value indicating if the message was considered complete or not. If # === False, the bot understands the command is not completed yet. The next message will be preprocessed to have the command added. This is useful for mobile interfaces, where the command is selected from a list. It can also be used to set up a ReplyKeyboard. Any other value from False is understood as completed.
     	ii) An object representing the result of the API called to answer the message. Package provided functions like BotAnswerMsg return appropriate JSON objects or $Failed if failed. This object is used to decide whether to retry API calling if failed (e.g., due to internet connection lost) and to update LastAnswered if succeeded.

$BotCmdList = {{"whoami", 0, CmdWhoami}, {"wolfram", 2, CmdWolfram}, {"shell",
    2, CmdShell}, {"ping", 0, CmdPing},{"music", 1, CmdMusic},{"customkb", 0, CmdCustomKb},{"weather", 1, CmdWeather}};

Implementation of demo commands.
Check the package provided functions with ?, e.g.:

?BotAnswerMsg

Return a sentence based on your privilege level

CmdWhoami[msg_,bot_] := 
 Block[{fromid, str, msgId},
fromid = JSONElement[msg,{"message","from","id"}];
  msgId = JSONElement[msg,{"message","message_id"}];
  str = Switch[BotUserPrivilege[fromid,bot], 2, 
    "Thou art our master! :D\nWe are pleased to see thee.", 1, 
    "Thou befriendest our master! :D\nWe are pleased to see thee.", _, 
    "And thou art... we don't know thee!\nWho art thou?"];
    {True,BotAnswerMsg[msg, str,bot]}
  ]

Execute a Wolfram statement (very dangerous, do not allow to strangers!)

CmdWolfram[msg_,bot_] := 
 Block[{fromid, strSplit,msgId,eval,filePath}, fromid = JSONElement[msg,{"message","from","id"}];
  msgId = JSONElement[msg,{"message","message_id"}];
strSplit=StringSplit[JSONElement[msg,{"message","text"}], Whitespace, 2];
If[Length[strSplit]<=1||strSplit[[2]]=="",
(*No additional arguments were given. Wait thus for them.*)
{False,Null},
(*Otherwise evaluate them*)
eval=ToExpression[strSplit[[2]]];
(*If the result is a Graphics object, export it and sent it.*)
Switch[Head[eval],Graphics,filePath=FileNameJoin[{$BotTmpDir,DateString[{"Day","Month","Year","Hour","Minute","Second"}]<>".png"}];
							Export[filePath,eval];
							{True,BotPhotoAnswerMsg[msg,filePath,bot]},
(*Otherwise, send the String*)
_,{True,BotAnswerMsg[msg,ToString[eval],bot]}
]
  ]
  ]

Execute a shell statement (very dangerous, do not allow to strangers!)

CmdShell[msg_,bot_] := 
 Block[{fromid, msgId, ret,strSplit}, 
  fromid = JSONElement[msg,{"message","from","id"}];
  msgId = JSONElement[msg,{"message","message_id"}];
strSplit=StringSplit[JSONElement[msg,{"message","text"}], Whitespace, 2];
If[Length[strSplit]<=1||strSplit[[2]]=="",
(*No additional arguments were given. Wait thus for them.*)
{False,Null},
(*Otherwise evaluate them*)
ret = RunProcess[$SystemShell, "StandardOutput", strSplit[[2]] <> "\nexit\n"];
(*Here is a example of using BotAPICall to do the same as BotAnswerMsg, manually controlling the parameters*)
  {True,BotAPICall[
   "sendMessage", {"chat_id" -> fromid, "text" -> ret, 
    "reply_to_message_id" -> msgId},bot]}
]
  ]

Measure time between received message time and bot’s local time when answering

CmdPing[msg_,bot_] := 
 Block[{time}, time = JSONElement[msg,{"message","date"}];
 {True, BotAnswerMsg[msg, "Pong: " <> ToString[UnixTime[] - time] <> " s.",bot]}
  ]

Sending a random music file

CmdMusic[msg_,bot_] := 
 Block[{filePath}, filePath=FileNameJoin[{$BotTmpDir,DateString[{"Day","Month","Year","Hour","Minute","Second"}]<>".mp3"}];
(*Generate random music using notes in the Aristoxenian scale*)
Export[filePath,Sound[Play[Sin[# 2Pi t],{t,0,0.5},SampleRate->2000]&/@RandomChoice[({1,9/8,5/4,4/3,3/2,5/3,15/8,2/1}*440*3/5),20]]]
 {True, BotAudioAnswerMsg[msg, filePath,bot]}
  ]

Setting an example custom keyboard

CmdCustomKb[msg_,bot_] := 
 Block[{fromid, strSplit,msgId,eval,filePath}, fromid = JSONElement[msg,{"message","from","id"}];
  msgId = JSONElement[msg,{"message","message_id"}];
strSplit=StringSplit[JSONElement[msg,{"message","text"}], Whitespace, 2];
{True,BotAPICall[ "sendMessage", {"chat_id" -> fromid, "text" -> "A nice demo keyboard for you :)",    "reply_to_message_id" -> msgId,"reply_markup"->BotPrepareKeyboard[{{"/whoami","/ping","/shell pwd"},{"/wolfram N[Pi,50]","/wolfram Plot[Erf[x],{x,-2,2}]","/wolfram Rasterize[Integrate[Log[x]*Sqrt[x],x]]"},{"/music"},{"I love you, Telegram bot <3"}}]},bot]}
  ]

Use a previously given location to find weather data. When a user sends its location, it is saved as a rule in BotUserLocation, which is read by the command.

CmdWeather[msg_,bot_] := 
 Block[{fromid,loc},
fromid = JSONElement[msg,{"message","from","id"}];
 loc = JSONElement[BotUserLocation,{fromid}];
If[loc===Null,
{True, BotAnswerMsg[msg, "First send me a location as attachment!",bot]},
{True, BotAnswerMsg[msg, ToString[WeatherData[{"latitude","longitude"}/.loc,"Temperature"]],bot]}
]
  ]

UnixTime is not defined in 10.0-. Let’s add compatibility:

If[Not[NumberQ[UnixTime[]]],UnixTime[]:=Floor[AbsoluteTime[]-2208988800-$TimeZone*3600]]

At the moment, the bot calls package provided functions BotErrorNoCmd, BotErrorUnknownCmd, BotErrorUnprivilegedCmd when errors occur.
You can override them at will.

BotErrorNoCmd[msg_,bot_]:={True,BotAnswerMsg[msg,"I'd love to talk, but I don't know how to do it!",bot]}

You can also override BotLog to change the way to see debug error (or suppress them)

BotLog[str_]:=Print[DateString[]<>": "<>str]

Example of manually calling an API. Test this to see if your token works.

BotAPICall["getUpdates",{},Import[$BotConfigFile,"JSON"]]

To start the bot call BotStart. To stop it abort the evaluation.

BotStart[2, $BotConfigFile, $BotCmdList]