|
@@ -1,7 +1,72 @@
|
|
|
-(in-package #:chatikbot)
|
|
|
|
|
-
|
|
|
|
|
|
|
+(in-package :cl-user)
|
|
|
|
|
+(defpackage chatikbot.utils
|
|
|
|
|
+ (:use :cl)
|
|
|
|
|
+ (:export :*admins*
|
|
|
|
|
+ :*bot-name*
|
|
|
|
|
+ :*hooks*
|
|
|
|
|
+ :run-hooks
|
|
|
|
|
+ :add-hook
|
|
|
|
|
+ :remove-hook
|
|
|
|
|
+ :keyify
|
|
|
|
|
+ :dekeyify
|
|
|
|
|
+ :*settings*
|
|
|
|
|
+ :defsetting
|
|
|
|
|
+ :*backoff-start*
|
|
|
|
|
+ :*backoff-max*
|
|
|
|
|
+ :loop-with-error-backoff
|
|
|
|
|
+ :replace-all
|
|
|
|
|
+ :aget
|
|
|
|
|
+ :agets
|
|
|
|
|
+ :mappend
|
|
|
|
|
+ :random-elt
|
|
|
|
|
+ :flatten
|
|
|
|
|
+ :preprocess-input
|
|
|
|
|
+ :punctuation-p
|
|
|
|
|
+ :read-from-string-no-punct
|
|
|
|
|
+ :print-with-spaces
|
|
|
|
|
+ :spaced
|
|
|
|
|
+ :http-request
|
|
|
|
|
+ :xml-request
|
|
|
|
|
+ :get-by-tag
|
|
|
|
|
+ :select-text
|
|
|
|
|
+ :trim-nil
|
|
|
|
|
+ :text-with-cdata
|
|
|
|
|
+ :child-text
|
|
|
|
|
+ :clean-text
|
|
|
|
|
+ :json-request
|
|
|
|
|
+ :plist-hash
|
|
|
|
|
+ :plist-json
|
|
|
|
|
+ :format-ts
|
|
|
|
|
+ :parse-cmd
|
|
|
|
|
+ :parse-float
|
|
|
|
|
+ :smart-f
|
|
|
|
|
+ :format-size
|
|
|
|
|
+ :format-interval
|
|
|
|
|
+ :symbol-append
|
|
|
|
|
+ :message-id
|
|
|
|
|
+ :from-id
|
|
|
|
|
+ :chat-id
|
|
|
|
|
+ :text
|
|
|
|
|
+ :cmd
|
|
|
|
|
+ :args
|
|
|
|
|
+ :query-id
|
|
|
|
|
+ :from
|
|
|
|
|
+ :raw-data
|
|
|
|
|
+ :message
|
|
|
|
|
+ :data
|
|
|
|
|
+ :section
|
|
|
|
|
+ :code
|
|
|
|
|
+ :error
|
|
|
|
|
+ :raw-state
|
|
|
|
|
+ :state
|
|
|
|
|
+ :inline-message-id
|
|
|
|
|
+ :hook
|
|
|
|
|
+ :headers
|
|
|
|
|
+ :paths))
|
|
|
|
|
+(in-package #:chatikbot.utils)
|
|
|
|
|
+
|
|
|
|
|
+(defvar *admins* nil "Admins chat-ids")
|
|
|
(defvar *bot-name* nil "bot name to properly handle text input")
|
|
(defvar *bot-name* nil "bot name to properly handle text input")
|
|
|
-
|
|
|
|
|
(defvar *hooks* (make-hash-table) "Hooks storage")
|
|
(defvar *hooks* (make-hash-table) "Hooks storage")
|
|
|
|
|
|
|
|
(defun run-hooks (event &rest arguments)
|
|
(defun run-hooks (event &rest arguments)
|
|
@@ -101,42 +166,9 @@ is replaced with replacement."
|
|
|
(preprocess-input (subseq text (1+ first-space)))
|
|
(preprocess-input (subseq text (1+ first-space)))
|
|
|
(replace-all text *bot-name* "ты")))))
|
|
(replace-all text *bot-name* "ты")))))
|
|
|
|
|
|
|
|
-(defun punctuation-p (char)
|
|
|
|
|
- (find char ".,;:'!?#-()\\\""))
|
|
|
|
|
-
|
|
|
|
|
-(defun read-from-string-no-punct (input)
|
|
|
|
|
- "Read from an input string, ignoring punctuation."
|
|
|
|
|
- (let ((*package* (find-package 'chatikbot)))
|
|
|
|
|
- (read-from-string
|
|
|
|
|
- (concatenate 'string "(" (substitute-if #\space #'punctuation-p input) ")"))))
|
|
|
|
|
-
|
|
|
|
|
(defun print-with-spaces (list)
|
|
(defun print-with-spaces (list)
|
|
|
(format nil "~@(~{~a~^ ~}~)" list))
|
|
(format nil "~@(~{~a~^ ~}~)" list))
|
|
|
|
|
|
|
|
-(defun switch-viewpoint (words)
|
|
|
|
|
- "Change I to you and vice versa, and so on."
|
|
|
|
|
- (sublis '((I . you) (you . I) (me . you) (am . are)
|
|
|
|
|
- (я ты) (ты я) (меня тебя) (тебя меня))
|
|
|
|
|
- words))
|
|
|
|
|
-
|
|
|
|
|
-(defun use-eliza-rules (input rules)
|
|
|
|
|
- "Find some rule with which to transform the input."
|
|
|
|
|
- (rule-based-translator input rules
|
|
|
|
|
- :action #'(lambda (bindings responses)
|
|
|
|
|
- (sublis (switch-viewpoint bindings)
|
|
|
|
|
- (random-elt responses)))))
|
|
|
|
|
-
|
|
|
|
|
-(defun eliza (input rules)
|
|
|
|
|
- (let ((r (use-eliza-rules
|
|
|
|
|
- (read-from-string-no-punct input)
|
|
|
|
|
- rules)))
|
|
|
|
|
- (cond
|
|
|
|
|
- ((null r) nil)
|
|
|
|
|
- ((and (consp (car r)) (eq 'function (caar r)))
|
|
|
|
|
- (apply (cadar r) (cdr r)))
|
|
|
|
|
- ((keywordp (car r)) r)
|
|
|
|
|
- (t (print-with-spaces (flatten r))))))
|
|
|
|
|
-
|
|
|
|
|
(defun parse-cmd (text)
|
|
(defun parse-cmd (text)
|
|
|
(let* ((args (split-sequence:split-sequence #\Space (subseq text 1) :remove-empty-subseqs t))
|
|
(let* ((args (split-sequence:split-sequence #\Space (subseq text 1) :remove-empty-subseqs t))
|
|
|
(cmd (subseq (car args) 0 (position #\@ (car args)))))
|
|
(cmd (subseq (car args) 0 (position #\@ (car args)))))
|
|
@@ -217,13 +249,36 @@ is replaced with replacement."
|
|
|
(defun get-by-tag (node tag)
|
|
(defun get-by-tag (node tag)
|
|
|
(nreverse (org.shirakumo.plump.dom::get-elements-by-tag-name node tag)))
|
|
(nreverse (org.shirakumo.plump.dom::get-elements-by-tag-name node tag)))
|
|
|
|
|
|
|
|
-(defun select-text (selector node)
|
|
|
|
|
|
|
+(defun select-text (node &optional selector)
|
|
|
(ignore-errors
|
|
(ignore-errors
|
|
|
- (plump:text (plump:strip
|
|
|
|
|
- (let ((node (elt (clss:select selector node) 0)))
|
|
|
|
|
- (plump:traverse node #'(lambda (n) (setf (plump:text n) ""))
|
|
|
|
|
- :test #'plump:comment-p)
|
|
|
|
|
- node)))))
|
|
|
|
|
|
|
+ (when selector (setf node (elt (clss:select selector node) 0)))
|
|
|
|
|
+ (plump:traverse node #'(lambda (n) (setf (plump:text n) ""))
|
|
|
|
|
+ :test #'plump:comment-p)
|
|
|
|
|
+ (plump:text (plump:strip node))))
|
|
|
|
|
+
|
|
|
|
|
+(defun trim-nil (text)
|
|
|
|
|
+ (when text
|
|
|
|
|
+ (let ((text (string-trim " " text)))
|
|
|
|
|
+ (unless (zerop (length text))
|
|
|
|
|
+ text))))
|
|
|
|
|
+
|
|
|
|
|
+(defun text-with-cdata (node)
|
|
|
|
|
+ "Compiles all text nodes within the nesting-node into one string."
|
|
|
|
|
+ (with-output-to-string (stream)
|
|
|
|
|
+ (labels ((r (node)
|
|
|
|
|
+ (loop for child across (plump:children node)
|
|
|
|
|
+ do (typecase child
|
|
|
|
|
+ (plump:text-node (write-string (plump:text child) stream))
|
|
|
|
|
+ (plump:cdata (write-string (plump:text child) stream))
|
|
|
|
|
+ (plump:nesting-node (r child))))))
|
|
|
|
|
+ (r node))))
|
|
|
|
|
+
|
|
|
|
|
+(defun child-text (node tag)
|
|
|
|
|
+ (alexandria:when-let (child (car (get-by-tag node tag)))
|
|
|
|
|
+ (trim-nil (text-with-cdata child))))
|
|
|
|
|
+
|
|
|
|
|
+(defun clean-text (text)
|
|
|
|
|
+ (when text (trim-nil (plump:text (plump:parse text)))))
|
|
|
|
|
|
|
|
;; JSON processing
|
|
;; JSON processing
|
|
|
(defun json-request (url &rest args &key method parameters content headers basic-auth cookie-jar keep-alive use-connection-pool timeout ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path user-agent (object-as :alist))
|
|
(defun json-request (url &rest args &key method parameters content headers basic-auth cookie-jar keep-alive use-connection-pool timeout ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path user-agent (object-as :alist))
|
|
@@ -298,151 +353,10 @@ is replaced with replacement."
|
|
|
((< seconds (* 60 60 24 7 54)) (format nil "~A weeks" (round seconds (* 60 60 24 7))))
|
|
((< seconds (* 60 60 24 7 54)) (format nil "~A weeks" (round seconds (* 60 60 24 7))))
|
|
|
(:otherwise (format nil "~A years" (smart-f (/ seconds (* 60 60 24 365.25)) 1)))))
|
|
(:otherwise (format nil "~A years" (smart-f (/ seconds (* 60 60 24 365.25)) 1)))))
|
|
|
|
|
|
|
|
-(defun token-hmac (message &optional (hmac-length 12))
|
|
|
|
|
- (let ((hmac (crypto:make-hmac (crypto:ascii-string-to-byte-array *telegram-token*) :sha256)))
|
|
|
|
|
- (crypto:update-hmac hmac (crypto:ascii-string-to-byte-array message))
|
|
|
|
|
- (base64:usb8-array-to-base64-string
|
|
|
|
|
- (subseq (crypto:hmac-digest hmac) 0 hmac-length) :uri t)))
|
|
|
|
|
-
|
|
|
|
|
-(defun encode-callback-data (chat-id section data &optional (ttl 600) (hmac-length 12))
|
|
|
|
|
- (when (find #\: data)
|
|
|
|
|
- (error "Bad data."))
|
|
|
|
|
- (let* ((message (format nil "~A:~A:~A:~A"
|
|
|
|
|
- (base64:integer-to-base64-string chat-id)
|
|
|
|
|
- (base64:integer-to-base64-string
|
|
|
|
|
- (+ ttl (local-time:timestamp-to-universal (local-time:now))))
|
|
|
|
|
- section data))
|
|
|
|
|
- (encoded (format nil "~A$~A" message (token-hmac message hmac-length))))
|
|
|
|
|
- (when (> (length encoded) +telegram-max-callback-data-length+)
|
|
|
|
|
- (error "Max callback length exceeded"))
|
|
|
|
|
- encoded))
|
|
|
|
|
-
|
|
|
|
|
-(defun decode-callback-data (chat-id raw-data &optional (hmac-length 12))
|
|
|
|
|
- (destructuring-bind (message hmac)
|
|
|
|
|
- (split-sequence:split-sequence #\$ raw-data :from-end t :count 2)
|
|
|
|
|
- (destructuring-bind (cid expire section data)
|
|
|
|
|
- (split-sequence:split-sequence #\: message :count 4)
|
|
|
|
|
- (unless (= chat-id (base64:base64-string-to-integer cid))
|
|
|
|
|
- (error "Wrong chat id."))
|
|
|
|
|
- (unless (>= (base64:base64-string-to-integer expire)
|
|
|
|
|
- (local-time:timestamp-to-universal (local-time:now)))
|
|
|
|
|
- (error "Expired."))
|
|
|
|
|
- (unless (equal hmac (token-hmac message hmac-length))
|
|
|
|
|
- (error "Bad data."))
|
|
|
|
|
- (values data (intern (string-upcase section) "KEYWORD")))))
|
|
|
|
|
-
|
|
|
|
|
-(defmacro def-message-handler (name (message) &body body)
|
|
|
|
|
- `(progn
|
|
|
|
|
- (defun ,name (,message)
|
|
|
|
|
- (let ((message-id (aget "message_id" ,message))
|
|
|
|
|
- (from-id (aget "id" (aget "from" ,message)))
|
|
|
|
|
- (chat-id (aget "id" (aget "chat" ,message)))
|
|
|
|
|
- (text (aget "text" ,message)))
|
|
|
|
|
- (declare (ignorable message-id from-id chat-id text))
|
|
|
|
|
- (handler-case (progn ,@body)
|
|
|
|
|
- (error (e)
|
|
|
|
|
- (log:error "~A" e)
|
|
|
|
|
- (bot-send-message chat-id
|
|
|
|
|
- (format nil "Ошибочка вышла~@[: ~A~]"
|
|
|
|
|
- (when (member chat-id *admins*) e)))))))
|
|
|
|
|
- (add-hook :update-message ',name)))
|
|
|
|
|
-
|
|
|
|
|
-(defmacro def-message-cmd-handler (name (&rest commands) &body body)
|
|
|
|
|
- `(def-message-handler ,name (message)
|
|
|
|
|
- (when (and text (equal #\/ (char text 0)))
|
|
|
|
|
- (multiple-value-bind (cmd args) (parse-cmd text)
|
|
|
|
|
- (when (member cmd (list ,@commands))
|
|
|
|
|
- (log:info cmd message-id chat-id from-id args)
|
|
|
|
|
- ,@body
|
|
|
|
|
- t)))))
|
|
|
|
|
-
|
|
|
|
|
-(defmacro def-message-admin-cmd-handler (name (&rest commands) &body body)
|
|
|
|
|
- `(def-message-handler ,name (message)
|
|
|
|
|
- (when (and (member chat-id *admins*)
|
|
|
|
|
- text (equal #\/ (char text 0)))
|
|
|
|
|
- (multiple-value-bind (cmd args) (parse-cmd text)
|
|
|
|
|
- (when (member cmd (list ,@commands))
|
|
|
|
|
- (log:info cmd message-id chat-id from-id args)
|
|
|
|
|
- ,@body
|
|
|
|
|
- t)))))
|
|
|
|
|
-
|
|
|
|
|
-(defmacro def-callback-handler (name (callback) &body body)
|
|
|
|
|
- `(progn
|
|
|
|
|
- (defun ,name (,callback)
|
|
|
|
|
- (let* ((query-id (aget "id" ,callback))
|
|
|
|
|
- (from (aget "from" ,callback))
|
|
|
|
|
- (raw-data (aget "data" ,callback))
|
|
|
|
|
- (message (aget "message" ,callback))
|
|
|
|
|
- (inline-message-id (aget "inline_message_id" ,callback))
|
|
|
|
|
- (from-id (aget "id" from))
|
|
|
|
|
- (chat-id (aget "id" (aget "chat" message)))
|
|
|
|
|
- (message-id (aget "message_id" message)))
|
|
|
|
|
- (declare (ignorable query-id from raw-data message inline-message-id from-id chat-id message-id))
|
|
|
|
|
- (handler-case (progn ,@body)
|
|
|
|
|
- (error (e)
|
|
|
|
|
- (log:error "~A" e)
|
|
|
|
|
- (bot-send-message (or chat-id from-id)
|
|
|
|
|
- (format nil "Ошибочка вышла~@[: ~A~]"
|
|
|
|
|
- (when (member chat-id *admins*) e)))))))
|
|
|
|
|
- (add-hook :update-callback-query ',name)))
|
|
|
|
|
-
|
|
|
|
|
-(defmacro def-callback-section-handler (name (&rest sections) &body body)
|
|
|
|
|
- `(def-callback-handler ,name (callback)
|
|
|
|
|
- (when chat-id
|
|
|
|
|
- (multiple-value-bind (data section) (decode-callback-data chat-id raw-data)
|
|
|
|
|
- (when (member section (list ,@sections))
|
|
|
|
|
- (log:info query-id from-id chat-id message-id section data)
|
|
|
|
|
- ,@body
|
|
|
|
|
- t)))))
|
|
|
|
|
-
|
|
|
|
|
-(defun encode-oauth-state (section state)
|
|
|
|
|
- (format nil "~A$~A" section state))
|
|
|
|
|
-(defun decode-oauth-state (raw-state)
|
|
|
|
|
- (destructuring-bind (section data)
|
|
|
|
|
- (split-sequence:split-sequence #\$ raw-state :count 2)
|
|
|
|
|
- (values data (intern (string-upcase section) "KEYWORD"))))
|
|
|
|
|
-
|
|
|
|
|
-(defmacro def-oauth-handler (name (code error state) &body body)
|
|
|
|
|
- `(progn
|
|
|
|
|
- (defun ,name (,code ,error ,state)
|
|
|
|
|
- (declare (ignorable ,code ,error ,state))
|
|
|
|
|
- (handler-case (progn ,@body)
|
|
|
|
|
- (error (e)
|
|
|
|
|
- (log:error "~A" e)
|
|
|
|
|
- (hunchentoot:redirect "/error"))))
|
|
|
|
|
- (add-hook :oauth ',name)))
|
|
|
|
|
-
|
|
|
|
|
-(defmacro def-oauth-section-handler (name (&rest sections) &body body)
|
|
|
|
|
- `(def-oauth-handler ,name (code error raw-state)
|
|
|
|
|
- (multiple-value-bind (state section) (decode-oauth-state raw-state)
|
|
|
|
|
- (when (member section (list ,@sections))
|
|
|
|
|
- ,@body
|
|
|
|
|
- t))))
|
|
|
|
|
-
|
|
|
|
|
(defun symbol-append (&rest symbols)
|
|
(defun symbol-append (&rest symbols)
|
|
|
(intern (apply #'concatenate 'string
|
|
(intern (apply #'concatenate 'string
|
|
|
(mapcar #'symbol-name symbols))))
|
|
(mapcar #'symbol-name symbols))))
|
|
|
|
|
|
|
|
-;; Schedule
|
|
|
|
|
-(defmacro defcron (name (&rest schedule) &body body)
|
|
|
|
|
- (let ((schedule (or schedule '(:minute '* :hour '*)))
|
|
|
|
|
- (scheduler (symbol-append name '-scheduler)))
|
|
|
|
|
- `(progn
|
|
|
|
|
- (defun ,name ()
|
|
|
|
|
- (unwind-protect
|
|
|
|
|
- (handler-case (progn ,@body)
|
|
|
|
|
- (error (e) (log:error e)))
|
|
|
|
|
- (dex:clear-connection-pool)))
|
|
|
|
|
- (defun ,scheduler ()
|
|
|
|
|
- (clon:schedule-function
|
|
|
|
|
- ',name (clon:make-scheduler
|
|
|
|
|
- (clon:make-typed-cron-schedule
|
|
|
|
|
- ,@schedule)
|
|
|
|
|
- :allow-now-p t)
|
|
|
|
|
- :name ',name :thread t)
|
|
|
|
|
- (values))
|
|
|
|
|
- (add-hook :starting ',scheduler))))
|
|
|
|
|
-
|
|
|
|
|
|
|
|
|
|
;; Fix bug in local-time (following symlinks in /usr/share/zoneinfo/
|
|
;; Fix bug in local-time (following symlinks in /usr/share/zoneinfo/
|
|
|
;; leads to bad cutoff)
|
|
;; leads to bad cutoff)
|