Our First Telegram Bot (tutorial)

For the start, you need to create a bot and get it's token from the BotFather bot:

When you've got token, go to the REPL and define our bot:

CL-USER> (defvar *token* "52...")

CL-USER> (cl-telegram-bot2/bot:defbot test-bot ()
           ())

CL-USER> (cl-telegram-bot2/server:start-polling (make-test-bot *token*))

Last call will be interrupted with Required argument "Initial state is required argument." missing. error. This is because in second version of cl-telegram-bot bot always should have some state. From the current state depends bot's behaviour, commands which are available and a set of handlers for different events. Each chat has it's own current state. This allows bot to keep context when working with each user.

When you are creating a bot instance, you should give at a state definition. Let's create a bot with simple state which will great a new user:

CL-USER> (cl-telegram-bot2/server:start-polling
          (make-test-bot *token*
                         :initial-state
                         (cl-telegram-bot2/state:state
                          (cl-telegram-bot2/actions/send-text:send-text
                           "Hello from cl-telegram-bot!"))))
     
#<FUNCTION (FLET CL-TELEGRAM-BOT2/SERVER::STOP-BOT :IN CL-TELEGRAM-BOT2/SERVER:START-POLLING) {100B11524B}>

CL-USER> (defparameter *stop-func* *)

Note, the bot was started and a function which can stop it was returned from cl-telegram-bot2/server:start-polling function.

Now let's see how our bot will behave:

As you can see, our bot greets the user but does not respond ot it's message. What if we'll use send-text function to create an action for update event?

CL-USER> (funcall *stop-func*)

CL-USER> (setf *stop-func*
               (cl-telegram-bot2/server:start-polling
                (make-test-bot *token*
                               :initial-state
                               (cl-telegram-bot2/state:state
                                (cl-telegram-bot2/actions/send-text:send-text
                                 "Hello from cl-telegram-bot!")
                                :on-update (cl-telegram-bot2/actions/send-text:send-text
                                            "The response to the message")))))

Now our bot will respond to the message with a static message:

Good! But what if we want to execute some custom logic before reponse? The one way is to define your own action class, but the easiest way is to use a function. For demonstration, we'll create a function which will reply with a reversed text:

CL-USER> (funcall *stop-func*)

CL-USER> (defun reply-with-reversed-text (update)
           (cl-telegram-bot2/high:reply
            (reverse (cl-telegram-bot2/api:message-text
                      (cl-telegram-bot2/api:update-message update))))
           ;; Return no values because we don't want to change
           ;; the state:
           (values))

CL-USER> (setf *stop-func*
               (cl-telegram-bot2/server:start-polling
                (make-test-bot *token*
                               :initial-state
                               (cl-telegram-bot2/state:state
                                (cl-telegram-bot2/actions/send-text:send-text
                                 "Hello from cl-telegram-bot!")
                                ;; Note, here we specify as a handler the fbound symbol:
                                :on-update 'reply-with-reversed-text))))

Now let's combine two actions together. First we'll send a static text, then call a function and send the reversed user input:

CL-USER> (funcall *stop-func*)

CL-USER> (setf *stop-func*
               (cl-telegram-bot2/server:start-polling
                (make-test-bot *token*
                               :initial-state
                               (cl-telegram-bot2/state:state
                                (cl-telegram-bot2/actions/send-text:send-text
                                 "Hello from cl-telegram-bot!")
                                ;; Note, here we specify as a handler the list of an action
                                ;; and a fbound symbol:
                                :on-update (list (cl-telegram-bot2/actions/send-text:send-text
                                                  "Here how you text will look like when reversed:")
                                                 'reply-with-reversed-text)))))

As we said in the beginning of the tutorial, the real power of the second version of cl-telegram-bot is it's ability to keep context as the current state. At the next step we'll create the second state at which bot will calculate the number of symbols in the user input.

Here is how the workflow will work:

CL-USER> (defun reply-with-num-symbols (update)
           (let ((input-text
                   (cl-telegram-bot2/api:message-text
                      (cl-telegram-bot2/api:update-message update))))
             (cl-telegram-bot2/high:reply
              (format nil "Your input has ~A chars."
                      (length input-text)))
             ;; Return no values because we don't want to change
             ;; the state:
             (values)))

CL-USER> (funcall *stop-func*)

CL-USER> (setf *stop-func*
               (cl-telegram-bot2/server:start-polling
                (make-test-bot *token*
                               :initial-state
                               (cl-telegram-bot2/state:state
                                (cl-telegram-bot2/actions/send-text:send-text
                                 "Hello from cl-telegram-bot!")
                                ;; Note, here we specify as a handler the list of an action
                                ;; and a fbound symbol:
                                :on-update (list (cl-telegram-bot2/actions/send-text:send-text
                                                  "Here how you text will look like when reversed:")
                                                 'reply-with-reversed-text
                                                 ;; Now switch to the second state
                                                 (cl-telegram-bot2/state:state
                                                  (cl-telegram-bot2/actions/send-text:send-text
                                                   "Now bot is in the second state.")
                                                  ;; This is how we count the symbols in user input
                                                  :on-update 'reply-with-num-symbols))))))

As you can see, now our bot has stuck in the second state and there is no way to jump back to the first one. How would we do this?

State change inside the bot creates a stack of states:

There are special actions which allows to unwind this stack to one of the previous states:

Let's try to use the simplest form to return to the first state:

CL-USER> (funcall *stop-func*)

CL-USER> (setf *stop-func*
               (cl-telegram-bot2/server:start-polling
                (make-test-bot *token*
                               :initial-state
                               (cl-telegram-bot2/state:state
                                (cl-telegram-bot2/actions/send-text:send-text
                                 "Hello from cl-telegram-bot!")
                                ;; Note, here we specify as a handler the list of an action
                                ;; and a fbound symbol:
                                :on-update (list (cl-telegram-bot2/actions/send-text:send-text
                                                  "Here how you text will look like when reversed:")
                                                 'reply-with-reversed-text
                                                 ;; Now switch to the second state
                                                 (cl-telegram-bot2/state:state
                                                  (cl-telegram-bot2/actions/send-text:send-text
                                                   "Now bot is in the second state.")
                                                  ;; This is how we count the symbols in user input
                                                  ;; and return the the previous state:
                                                  :on-update (list 'reply-with-num-symbols
                                                                   (cl-telegram-bot2/term/back:back))))))))

As you can see, now bot switches between first and second states. But back function can do more, because this kind of actions are special and is able not only to switch current bot's state, but also to return some results to this parent state.

To return some result we should give it as an optional argument to the cl-telegram-bot2/term/back:back function:

(cl-telegram-bot2/term/back:back "Some result")

and to process this result, we have to specify on-result event handler on the first state. Here is how complete example will look like:

CL-USER> (funcall *stop-func*)

CL-USER> (defun reply-with-num-symbols (update)
           (let* ((input-text
                    (cl-telegram-bot2/api:message-text
                     (cl-telegram-bot2/api:update-message update)))
                  (num-symbols
                    (length input-text)))
             (cl-telegram-bot2/high:reply
              (format nil "Your input has ~A chars."
                      num-symbols))
             ;; Return BACK action to return num symbols to the first state:
             (cl-telegram-bot2/term/back:back num-symbols)))

CL-USER> (defun process-result (num-symbols)
           (cl-telegram-bot2/high:reply
            (format nil "Now we are in the first state and the second state returned ~A chars."
                    num-symbols))
           (values))

CL-USER> (setf *stop-func*
               (cl-telegram-bot2/server:start-polling
                (make-test-bot *token*
                               :initial-state
                               (cl-telegram-bot2/state:state
                                (cl-telegram-bot2/actions/send-text:send-text
                                 "Hello from cl-telegram-bot!")
                                ;; Note, here we specify as a handler the list of an action
                                ;; and a fbound symbol:
                                :on-update (list (cl-telegram-bot2/actions/send-text:send-text
                                                  "Here how you text will look like when reversed:")
                                                 'reply-with-reversed-text
                                                 ;; Now switch to the second state
                                                 (cl-telegram-bot2/state:state
                                                  (cl-telegram-bot2/actions/send-text:send-text
                                                   "Now bot is in the second state.")
                                                  ;; This is how we count the symbols in user input
                                                  ;; and return it to the initial state:
                                                  :on-update 'reply-with-num-symbols))
                                :on-result 'process-result))))

This is all for now. In the next tutorial we'll see how to define a custom states to make some building blocks for our workflow.