Skip to content

Commit

Permalink
Fully working calculator bot defined in declarative way!
Browse files Browse the repository at this point in the history
  • Loading branch information
svetlyak40wt committed Nov 8, 2024
1 parent 7e03241 commit 5929b17
Show file tree
Hide file tree
Showing 18 changed files with 625 additions and 424 deletions.
13 changes: 13 additions & 0 deletions cl-telegram-bot2-examples.asd
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#-asdf3.1 (error "cl-telegram-bot requires ASDF 3.1 because for lower versions pathname does not work for package-inferred systems.")
(defsystem "cl-telegram-bot2-examples"
:description "Telegram Bot API, based on sovietspaceship's work but mostly rewritten."
:author "Alexander Artemenko <[email protected]>"
:license "MIT"
:homepage "https://40ants.com/cl-telegram-bot/"
:source-control (:git "https://github.com/40ants/cl-telegram-bot")
:bug-tracker "https://github.com/40ants/cl-telegram-bot/issues"
:class :40ants-asdf-system
:defsystem-depends-on ("40ants-asdf-system")
:pathname "examples"
:depends-on ("cl-telegram-bot2-examples/calc")
:in-order-to ((test-op (test-op "cl-telegram-bot2-tests"))))
4 changes: 2 additions & 2 deletions cl-telegram-bot2.asd
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
:class :40ants-asdf-system
:defsystem-depends-on ("40ants-asdf-system")
:pathname "v2"
:depends-on ("njson/jzon"
"cl-telegram-bot2/api"
:depends-on ("cl-telegram-bot2/api"
"cl-telegram-bot2/pipeline"
"cl-telegram-bot2/server")
:in-order-to ((test-op (test-op "cl-telegram-bot2-tests"))))


(asdf:register-system-packages "log4cl" '("LOG"))
(asdf:register-system-packages "utilities.print-items" '("PRINT-ITEMS"))
(asdf:register-system-packages "dexador" '("DEX"))
(asdf:register-system-packages "sento" '("SENTO.ACTOR-SYSTEM"
"SENTO.ACTOR"))
312 changes: 60 additions & 252 deletions examples/calc.lisp
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
(uiop:define-package #:test-bot
(uiop:define-package #:cl-telegram-bot2/calc
(:use #:cl)
(:import-from #:cl-telegram-bot2/state
#:state)
(:import-from #:cl-telegram-bot2/actions/send-text
#:send-text)
(:import-from #:cl-telegram-bot2/bot
#:defbot)
(:import-from #:cl-telegram-bot2/server
Expand All @@ -9,259 +13,63 @@
#:reply
#:chat-state)
(:import-from #:serapeum
#:dict
#:fmt)
(:import-from #:cl-telegram-bot2/pipeline
#:back-to-nth-parent
#:back-to
#:back)
(:import-from #:cl-telegram-bot2/api
#:message-message-id))
(in-package #:test-bot)


;; (defun initial (bot state)
;; (declare (ignore bot))
;; (values state))

;; (defun show-help (bot state)
;; (declare (ignore bot))
;; (values state))

;; (defun ask-for-number (bot state)
;; (declare (ignore bot))
;; (values state))


(defclass calc (chat-state)
())

(defmethod cl-telegram-bot2/generics:process ((state calc) (update cl-telegram-bot2/api:update))
(reply "Давай посчитаем!")

(make-instance 'ask-for-number
:prompt "Введите первое число:"))

(defmethod cl-telegram-bot2/generics:on-state-activation ((state calc))
(reply "Давай посчитаем!")

(make-instance 'ask-for-number
:prompt "Введите первое число:"))


(defmethod cl-telegram-bot2/generics:on-result ((state calc) (result t))
(cond
(result
(make-instance 'has-one-number
:first-num result))
;; Если вернулись без значения, значит уже посчитали и надо начать все сначала.
(t
(make-instance 'ask-for-number
:prompt "Введите первое число:"))))


(defclass has-one-number (chat-state)
((first-num :initarg :first-num
:type integer
:accessor first-num)))


;; (defmethod cl-telegram-bot2/generics:process ((state has-one-number) (update cl-telegram-bot2/api:update))
;; ;; (make-instance 'ask-for-number
;; ;; :prompt "Введите второе число:")
;; )

(defmethod cl-telegram-bot2/generics:on-state-activation ((state has-one-number))
(make-instance 'ask-for-number
:prompt "Введите второе число:"))


(defmethod cl-telegram-bot2/generics:on-result ((state has-one-number) (result t))
(when result
(make-instance 'has-two-numbers
:first-num (first-num state)
:second-num result)))


(defclass has-two-numbers (chat-state)
((first-num :initarg :first-num
:type integer
:accessor first-num)
(second-num :initarg :second-num
:type integer
:accessor second-num)))


;; (defmethod cl-telegram-bot2/generics:process ((state has-two-numbers) (update cl-telegram-bot2/api:update))
;; ;; (reply (fmt "~A + ~A = ~A"
;; ;; (first-num state)
;; ;; (second-num state)
;; ;; (+ (first-num state)
;; ;; (second-num state))))
;; ;; (back-to 'calc)
;; )

(defmethod cl-telegram-bot2/generics:on-state-activation ((state has-two-numbers))
(reply (fmt "~A + ~A = ~A"
(first-num state)
(second-num state)
(+ (first-num state)
(second-num state))))
(back-to 'calc))



(defclass initial (chat-state)
((first-num :initform nil
:type (or null integer)
:accessor first-num)
(second-num :initform nil
:type (or null integer)
:accessor second-num)))


(defmethod cl-telegram-bot2/generics:process ((state initial) (update cl-telegram-bot2/api:update))
(reply "Переходим к ask-for-choice.")

(make-instance 'ask-for-choice
:buttons '("Foo" "Bar" "Blah"))

;; (progn

;; (reply "Мне нужны два числа.")

;; (cond
;; ((null (first-num state))
;; (make-instance 'ask-for-number
;; :prompt "Введите первое число:"
;; :on-number (lambda (num)
;; (setf (first-num state)
;; num))))
;; ((null (second-num state))
;; (make-instance 'ask-for-number
;; :prompt "Введите второе число:"
;; :on-number (lambda (num)
;; (setf (second-num state)
;; num))))
;; (t
;; (reply (fmt "Вот их сумма: ~A" (+ (first-num state)
;; (second-num state)))))))
)


(defmethod cl-telegram-bot2/generics:on-result ((state initial) (result t))
(reply (fmt "Получен результат: ~A" result))

(reply "Снова переходим к ask-for-choice.")

(make-instance 'ask-for-choice
:buttons '("Foo" "Bar" "Blah")))


(defclass ask-for-number (chat-state)
((prompt :initarg :prompt
:reader prompt)
(on-number :initarg :on-number
:reader on-number)))


(defmethod cl-telegram-bot2/generics:on-state-activation ((state ask-for-number))
(reply (prompt state))
(values))


(defmethod cl-telegram-bot2/generics:process ((state ask-for-number) (update cl-telegram-bot2/api:update))
(let* ((message (cl-telegram-bot2/api:update-message update))
(text (cl-telegram-bot2/api:message-text message))
(a-number (ignore-errors
(parse-integer
text))))
(cond
(a-number
(back a-number))
(t
(reply (fmt "\"~A\" не число. Введи число."
text))
(values)))))


(defclass ask-for-choice (chat-state)
((buttons :initarg :buttons
:reader buttons)
(message-id :initform nil
:accessor message-id)))


(defmethod cl-telegram-bot2/generics:on-state-activation ((state ask-for-choice))
;; (reply "прячем клаву"
;; :reply-markup
;; (make-instance 'cl-telegram-bot2/api:reply-keyboard-remove
;; :remove-keyboard t))
(let* ((message
(reply "Выберите один из вариантов:"
:reply-markup (make-instance 'cl-telegram-bot2/api:inline-keyboard-markup
:inline-keyboard
;; :input-field-placeholder "Выбери вариант:"
;; :keyboard
(list
(loop for choice in (buttons state)
collect (make-instance 'cl-telegram-bot2/api:inline-keyboard-button
:text choice
:callback-data choice)))))))
(setf (message-id state)
(message-message-id message)))
(values))


;; (defmethod cl-telegram-bot2/generics:on-state-deletion ((state ask-for-choice))
;; ;; (when (message-id state)
;; ;; (let ((chat-id (cl-telegram-bot2/api:chat-id cl-telegram-bot2/vars::*current-chat*)))
;; ;; (ignore-errors
;; ;; (cl-telegram-bot2/api:delete-message chat-id
;; ;; (message-id state)))))
;; (values))


(defmethod cl-telegram-bot2/generics:process ((state ask-for-choice) (update cl-telegram-bot2/api:update))
(let* ((message (cl-telegram-bot2/api:update-message update))
(text (when message
(cl-telegram-bot2/api:message-text message)))
(callback (cl-telegram-bot2/api:update-callback-query update))
(callback-choice
(when callback
(cl-telegram-bot2/api:callback-query-data callback))))

(cond
((and text
(string-equal text "back"))
(reply "Возвращаемся к предыдущему состоянию.")
(back))
(text
(reply (fmt "Нужно выбрать ответ (text = ~A)."
text))
(values))
(callback-choice
(reply (fmt "Выбран ответ: ~A"
callback-choice))
(back callback-choice)))))



;; (defmethod cl-telegram-bot2/generics:process ((state ask-for-number) (update cl-telegram-bot2/api:update))
;; (let* ((message (cl-telegram-bot2/api:update-message update))
;; (chat (cl-telegram-bot2/api:message-chat message))
;; (chat-id (cl-telegram-bot2/api:chat-id chat)))
;; (cl-telegram-bot2/api:send-message chat-id "Введите число.")
;; :back
;; ;; (make-instance 'initial)
;; ))
#:message-message-id)
(:import-from #:cl-telegram-bot2/states/ask-for-number
#:ask-for-number)
(:import-from #:cl-telegram-bot2/state
#:result-var)
(:import-from #:cl-telegram-bot2/states/ask-for-choice
#:ask-for-choice)
(:import-from #:40ants-logging))
(in-package #:cl-telegram-bot2/calc)



(defun calc-result ()
(let* ((num1 (result-var "first-num"))
(num2 (result-var "second-num"))
(op-name (result-var "operation-name"))
(op (gethash op-name
(dict "+" #'+
"-" #'-
"*" #'*
"/" #'/))))
(format nil "Result is: ~A"
(funcall op num1
num2))))


(defun make-prompt-for-op-choice ()
(fmt "Select an operation to apply to ~A and ~A:"
(result-var "first-num")
(result-var "second-num")))


(defbot test-bot ()
()
(:initial-state 'calc)
;; (:states (:initial initial)
;; (:help show-help)
;; (:ask-for-number ask-for-number))
)
(:initial-state
(state nil
:on-update (state (list
(send-text "Let's calculate!")
(ask-for-number "Enter the first number:"
:to "first-num"
:on-validation-error (send-text "Enter the number, please.")
:on-success (ask-for-number "Enter the second number:"
:to "second-num"
:on-validation-error (send-text "Enter the number, please.")
:on-success (ask-for-choice
'make-prompt-for-op-choice
'("+" "-" "*" "/")
:to "operation-name"
:on-success (list (send-text 'calc-result)
(back-to-nth-parent 3))))))))))


(defvar *bot* nil)
Expand All @@ -270,28 +78,28 @@
(defun stop ()
(when *bot*
(stop-polling *bot*)
(setf *bot* nil)))
(setf *bot* nil)

(sleep 1)
(bt:all-threads)))


(defun start ()
(stop)

(40ants-logging:setup-for-repl :level :warn)

(unless *bot*
(setf *bot*
(make-test-bot (uiop:getenv "TELEGRAM_TOKEN"))))

(start-polling *bot* :debug t))


(defun test-processing (msg)
(log:info "TRACE" msg))


(defun clean-threads ()
"TODO: надо разобраться почему треды не подчищаются. Возможно это происходит когда случаются ошибки?"
(loop for tr in (bt:all-threads)
when (or (str:starts-with? "message-thread" (bt:thread-name tr))
(str:starts-with? "timer-wheel" (bt:thread-name tr))
(str:starts-with? "telegram-bot" (bt:thread-name tr)))
do (bt:destroy-thread tr))
)
do (bt:destroy-thread tr)))
Loading

0 comments on commit 5929b17

Please sign in to comment.