Add reply-checker

This commit is contained in:
Valentin Boettcher 2019-08-18 22:29:48 +02:00
parent 5e77e1ccac
commit d16266cbd2
4 changed files with 1122 additions and 1189 deletions

1299
API.lisp

File diff suppressed because it is too large Load diff

View file

@ -2,8 +2,8 @@
:description "Telegram Bot API"
:author "Rei <https://github.com/sovietspaceship>"
:license "MIT"
:depends-on (#:cl-json #:alexandria #:closer-mop #:dexador)
:depends-on (#:cl-json #:alexandria #:closer-mop #:dexador #:lparallel #:trivial-types)
:serial t
:components ((:file "package")
(:file "cl-telegram-bot")
(:file "API")))
(:file "API")
(:file "cl-telegram-bot")))

View file

@ -22,10 +22,16 @@
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
;; SOFTWARE.
(in-package :cl-telegram-bot)
(alexandria:define-constant +http-ok+ 200 :test #'=)
(defvar *bot* nil)
(alexandria:define-constant +return-var+ '*)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Basic BOT Implementation ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defclass bot ()
((id
:documentation "Update id"
@ -48,6 +54,14 @@
:initarg :file-endpoint
:accessor file-endpoint
:documentation "HTTPS file-endpoint"
:initform nil)
(update-hooks
:documentation "A list of functions to call after retrieving updates by FETCH-UPDATES."
:type (proper-list function)
:initform nil)
(reply-queue
:type (proper-list function)
:documentation "A queue for storing reply fetchers."
:initform nil)))
(defmethod initialize-instance :after ((object bot) &key &allow-other-keys)
@ -58,6 +72,31 @@
(setf endpoint (concatenate 'string api-uri "bot" token "/")
file-endpoint (concatenate 'string api-uri "file/" "bot" token "/"))))
(defgeneric add-reply-fetcher (bot fetcher)
(:documentation "Adds a reply fetcher function that takes one argument of type *UPDATE and returns T if the update is the desired reply. Returns a PROMISE."))
(defmethod add-reply-fetcher ((bot bot) fetcher)
(let ((promise (lparallel:promise)))
(push `(,promise . ,fetcher) (slot-value bot 'reply-queue))
promise))
(defgeneric process-updates (bot updates)
(:documentation "Processes the updates fetched by FETCH-UPDATES to detect commands and replies."))
;; check types before
(defmethod process-updates :before (bot updates)
(declare (type (vector *update) updates)))
(defmethod process-updates ((bot bot) updates)
(with-slots (reply-queue) bot ; Process reply-fetchers
(let ((unresolved nil))
(loop for update across updates do
(dolist (fetcher-cons reply-queue)
(if (funcall (cdr fetcher-cons) update)
(lparallel:fulfill (car fetcher-cons) update)
(push fetcher-cons unresolved))))
(setf reply-queue unresolved))))
(defun make-bot (token)
"Create a new bot instance. Takes a token string."
(make-instance 'bot :token token))
@ -93,19 +132,22 @@
(recursive-change-class value type))))))
object)
(defun make-request (b name options &key (streamp nil) (return-type nil))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; HELPERS ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun make-request (b name options &key (return-type nil))
"Perform HTTP request to 'name API method with 'options JSON-encoded object."
(let* ((results (multiple-value-list
(handler-bind ((dex:http-request-bad-request #'dex:ignore-and-continue))
(dex:request
(concatenate 'string (endpoint b) name)
:method :post
:want-stream streamp
:want-stream t
:headers '(("Content-Type" . "Application/Json"))
:content (json:encode-json-alist-to-string options)))))
(message (nth 0 results)))
;; (when (<= 400 status 599)
;; (error 'request-error :what (format nil "request to ~A returned ~A (~A)" name status reason)))
(with-slots (ok result description) (decode message)
(if ok
(if return-type ; wether to cast into a known custom class or not
@ -136,7 +178,7 @@
(defgeneric decode (object))
(defmethod decode ((object stream))
(let ((json:*json-symbols-package* nil))
(let ((json:*json-symbols-package* :cl-telegram-bot))
(json:with-decoder-simple-clos-semantics
(prog1
(json:decode-json object)
@ -188,17 +230,87 @@
"Finds the latest update id from a sequence of updates."
(reduce #'max updates :key #'tg-update--id :from-end t))
(defun get-updates (b &key limit timeout)
"https://core.telegram.org/bots/api#getupdates"
(defgeneric fetch-updates (bot &key limit timeout)
(:documentation "Fetches updates from the API. See https://core.telegram.org/bots/api#getupdates."))
(defmethod fetch-updates ((b bot) &key limit timeout)
(let* ((current-id (id b))
(results (make-request b "getUpdates"
`(,(cons :offset current-id)
,(when limit `(cons :limit ,limit))
,(when timeout `(cons :limit ,timeout)))
:streamp t
:return-type '*UPDATE)))
(results ($ (get-updates
:limit limit
:timeout timeout
:offset current-id)
(:bot b))))
(when (> (length results) 0)
(let ((id (get-latest-update-id results)))
(setf (id b) id)
(incf (id b) 1)))
(process-updates b results)
results))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; CONVENIENCE INTERFACE ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun make-optional-body (body return-var return-val-sym)
"Make the body part of the $ (api call macro)."
(if body
`(let ((,return-var ,return-val-sym))
(wrap-$ ,@body))
return-val-sym))
(defmacro wrap-$ (&rest body)
"Wraps all forms following (:INLINE [$|$*] ...) into ([$|$*] ...)$."
(let ((index
(position nil body
:test #'(lambda (_ el)
(declare (ignore _))
(and (listp el) (eq (car el) :inline)
(or (eq (second el) '$*)
(eq (second el) '$)))))))
(if index
`(progn ,@(subseq body 0 index) ,(nconc (cdr (nth index body)) (subseq body (1+ index))))
`(progn ,@body))))
(defun make-$-method-call (method bot args)
"Generate a call to MAKE-REQUEST."
`(apply #'make-request (cons ,bot (,method ,@args))))
(defun make-$-reply-fetcher (bot reply-fetcher)
`(add-reply-fetcher ,bot ,(cdr reply-fetcher)))
(defmacro $* ((method &rest args) &body body)
"Call api method with standard BOT and RESULT-VAR. See $."
`($ (,method ,@args) () ,@body))
(defmacro $ ((method &rest args)
(&key (bot '*bot*) (return-var +return-var+) (parallel nil) (with-reply nil))
&body body)
"Call an API method. If a body is given the result of the call will be bound to RETURN-VAR and the body will be included. Subsequent calls to $ can be inlined like ($ ... FORMS (:INLINE $ ...) FORMS*) => ($ ... FORMS ($ ... FORMS*))."
(when (not (find method *api-methods*)) (error "No such API method."))
(let ((return-val-sym (gensym)))
`(let ((,return-val-sym ,(if parallel
`(lparallel:future ,(make-$-method-call method bot args))
(make-$-method-call method bot args)))
,@(when with-reply
(check-type with-reply cons)
`((,(car with-reply)
,(make-$-reply-fetcher bot with-reply)
))))
,(make-optional-body body return-var return-val-sym))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Convenience Wrappers ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun make-inline-keyboard (keys)
"Make an inline keyboard markup from an array of arrays of initializer lists of *INLINE-KEYBOARD-BUTTON."
(declare (type list keys))
(make-instance
'*inline-keyboard-markup
:inline--keyboard (mapcar
#'(lambda (keys)
(mapcar #'(lambda (key)
(apply #'make-instance `(*inline-keyboard-button ,@key)))
keys))
keys)))

View file

@ -1,153 +1,13 @@
(defpackage :cl-telegram-bot
(:nicknames :telegram-bot :tg-bot)
(:size 55)
(:use :closer-common-lisp :closer-mop)
(:EXPORT :*USER
:*CHAT
:*MESSAGE
:*MESSAGE-ENTITY
:*PHOTO-SIZE
:*AUDIO
:*DOCUMENT
:*VIDEO
:*ANIMATION
:*VOICE
:*VIDEO-NOTE
:*CONTACT
:*LOCATION
:*VENUE
:*POLL-OPTION
:*POLL
:*USER-PROFILE-PHOTOS
:*FILE
:*REPLY-KEYBOARD-MARKUP
:*KEYBOARD-BUTTON
:*REPLY-KEYBOARD-REMOVE
:*INLINE-KEYBOARD-MARKUP
:*INLINE-KEYBOARD-BUTTON
:*LOGIN-URL
:*CALLBACK-QUERY
:*FORCE-REPLY
:*CHAT-PHOTO
:*CHAT-MEMBER
:*CHAT-PERMISSIONS
:*RESPONSE-PARAMETERS
:*INPUT-MEDIA
:*INPUT-MEDIA-PHOTO
:*INPUT-MEDIA-VIDEO
:*INPUT-MEDIA-ANIMATION
:*INPUT-MEDIA-AUDIO
:*INPUT-MEDIA-DOCUMENT
:*INPUT-FILE
:GET-ME
:SEND-MESSAGE
:FORWARD-MESSAGE
:SEND-PHOTO
:SEND-AUDIO
:SEND-DOCUMENT
:SEND-VIDEO
:SEND-ANIMATION
:SEND-VOICE
:SEND-VIDEO-NOTE
:SEND-MEDIA-GROUP
:SEND-LOCATION
:EDIT-MESSAGE-LIVE-LOCATION
:STOP-MESSAGE-LIVE-LOCATION
:SEND-VENUE
:SEND-CONTACT
:SEND-POLL
:SEND-CHAT-ACTION
:GET-USER-PROFILE-PHOTOS
:GET-FILE
:KICK-CHAT-MEMBER
:UNBAN-CHAT-MEMBER
:RESTRICT-CHAT-MEMBER
:PROMOTE-CHAT-MEMBER
:SET-CHAT-PERMISSIONS
:EXPORT-CHAT-INVITE-LINK
:SET-CHAT-PHOTO
:DELETE-CHAT-PHOTO
:SET-CHAT-TITLE
:SET-CHAT-DESCRIPTION
:PIN-CHAT-MESSAGE
:UNPIN-CHAT-MESSAGE
:LEAVE-CHAT
:GET-CHAT
:GET-CHAT-ADMINISTRATORS
:GET-CHAT-MEMBERS-COUNT
:GET-CHAT-MEMBER
:SET-CHAT-STICKER-SET
:DELETE-CHAT-STICKER-SET
:ANSWER-CALLBACK-QUERY
:EDIT-MESSAGE-TEXT
:EDIT-MESSAGE-CAPTION
:EDIT-MESSAGE-MEDIA
:EDIT-MESSAGE-REPLY-MARKUP
:STOP-POLL
:DELETE-MESSAGE
:*STICKER
:*STICKER-SET
:*MASK-POSITION
:SEND-STICKER
:GET-STICKER-SET
:UPLOAD-STICKER-FILE
:CREATE-NEW-STICKER-SET
:ADD-STICKER-TO-SET
:SET-STICKER-POSITION-IN-SET
:DELETE-STICKER-FROM-SET
:*INLINE-QUERY
:ANSWER-INLINE-QUERY
:*INLINE-QUERY-RESULT
:*INLINE-QUERY-RESULT-ARTICLE
:*INLINE-QUERY-RESULT-PHOTO
:*INLINE-QUERY-RESULT-GIF
:*INLINE-QUERY-RESULT-MPEG-4-*GIF
:*INLINE-QUERY-RESULT-VIDEO
:*INLINE-QUERY-RESULT-AUDIO
:*INLINE-QUERY-RESULT-VOICE
:*INLINE-QUERY-RESULT-DOCUMENT
:*INLINE-QUERY-RESULT-LOCATION
:*INLINE-QUERY-RESULT-VENUE
:*INLINE-QUERY-RESULT-CONTACT
:*INLINE-QUERY-RESULT-GAME
:*INLINE-QUERY-RESULT-CACHED-PHOTO
:*INLINE-QUERY-RESULT-CACHED-GIF
:*INLINE-QUERY-RESULT-CACHED-MPEG-4-*GIF
:*INLINE-QUERY-RESULT-CACHED-STICKER
:*INLINE-QUERY-RESULT-CACHED-DOCUMENT
:*INLINE-QUERY-RESULT-CACHED-VIDEO
:*INLINE-QUERY-RESULT-CACHED-VOICE
:*INLINE-QUERY-RESULT-CACHED-AUDIO
:*INPUT-MESSAGE-CONTENT
:*INPUT-TEXT-MESSAGE-CONTENT
:*INPUT-LOCATION-MESSAGE-CONTENT
:*INPUT-VENUE-MESSAGE-CONTENT
:*INPUT-CONTACT-MESSAGE-CONTENT
:*CHOSEN-INLINE-RESULT
:*PASSPORT-DATA
:*PASSPORT-FILE
:*ENCRYPTED-PASSPORT-ELEMENT
:*ENCRYPTED-CREDENTIALS
:SET-PASSPORT-DATA-ERRORS
:*PASSPORT-ELEMENT-ERROR
:*PASSPORT-ELEMENT-ERROR-DATA-FIELD
:*PASSPORT-ELEMENT-ERROR-FRONT-SIDE
:*PASSPORT-ELEMENT-ERROR-REVERSE-SIDE
:*PASSPORT-ELEMENT-ERROR-SELFIE
:*PASSPORT-ELEMENT-ERROR-FILE
:*PASSPORT-ELEMENT-ERROR-FILES
:*PASSPORT-ELEMENT-ERROR-TRANSLATION-FILE
:*PASSPORT-ELEMENT-ERROR-TRANSLATION-FILES
:*PASSPORT-ELEMENT-ERROR-UNSPECIFIED
:SEND-GAME
:*GAME
:SET-GAME-SCORE
:GET-GAME-HIGH-SCORES
:*GAME-HIGH-SCORE)
(:use :closer-common-lisp :closer-mop :trivial-types)
(:export
#:bot
#:make-bot
#:access
#:get-updates
#:set-webhook
#:get-webhook-info))
#:get-webhook-info
#:$
#:$*))