Jelajahi Sumber

[REFACTOR] Packagize!

Innocenty Enikeew 8 tahun lalu
induk
melakukan
c92d34b1e8

+ 51 - 0
bot.lisp

@@ -0,0 +1,51 @@
+(in-package :cl-user)
+(defpackage chatikbot.bot
+  (:use :cl :chatikbot.utils :chatikbot.macros)
+  (:import-from :chatikbot.db
+                :set-setting)
+  (:import-from :chatikbot.telegram
+                :telegram-get-updates
+                :send-response)
+  (:export :handle-update))
+(in-package :chatikbot.bot)
+
+(defvar *telegram-last-update* 0 "Telegram last update_id")
+
+;; getUpdates handling
+(defun process-updates ()
+  (loop for update in (telegram-get-updates :offset (and *telegram-last-update*
+                                                         (1+ *telegram-last-update*))
+                                            :timeout 300)
+     ;; do (setf *telegram-last-update*
+     ;;          (max *telegram-last-update* (aget "update_id" update)))
+     do (handle-update update)))
+
+(defun handle-update (update)
+  (log:info update)
+  (let ((update-id (aget "update_id" update))
+        (reply-to (aget "id" (aget "from" (aget "reply_to_message" (aget "message" update))))))
+    (if (> update-id *telegram-last-update*)
+        (progn
+          (if (and reply-to (not (equal reply-to *bot-user-id*)))
+              (log:info "Reply not to bot")
+              (loop for (key . value) in update
+                 unless (equal "update_id" key)
+                 do (run-hooks (keyify (format nil "update-~A" key)) value)))
+          (setf *telegram-last-update* update-id))
+        (log:warn "Out-of-order update" update-id))))
+
+(defvar *start-message* "Hello" "Welcome message. Override it")
+(def-message-cmd-handler handle-start (:start)
+  (send-response chat-id *start-message*))
+
+(def-message-admin-cmd-handler handle-admin-settings (:settings)
+  (send-response chat-id
+                 (format nil "~{~{~A~@[ (~A)~]: ~A~}~^~%~}"
+                         (loop for symbol in *settings*
+                            collect (list symbol (documentation symbol 'variable) (symbol-value symbol))))))
+
+(def-message-admin-cmd-handler handle-admin-setsetting (:setsetting)
+  (let* ((*package* (find-package :chatikbot))
+         (var (read-from-string (car args)))
+         (val (read-from-string (format nil "~{~A~^ ~}" (rest args)))))
+    (send-response chat-id (format nil "OK, ~A" (set-setting var val)))))

+ 6 - 2
chatikbot.asd

@@ -23,11 +23,15 @@
                #:quri
                #:quri
                #:yason)
                #:yason)
   :serial t
   :serial t
-  :components ((:file "package")
-               (:file "patmatch")
+  :components ((:file "patmatch")
                (:file "utils")
                (:file "utils")
+               (:file "eliza")
                (:file "secrets")
                (:file "secrets")
                (:file "db")
                (:file "db")
                (:file "telegram")
                (:file "telegram")
+               (:file "crypto")
+               (:file "macros")
+               (:file "bot")
                (:file "server")
                (:file "server")
+               (:file "common")
                (:file "chatikbot")))
                (:file "chatikbot")))

+ 80 - 65
chatikbot.lisp

@@ -1,71 +1,86 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot
+  (:use :cl)
+  (:import-from :chatikbot.db
+                :with-db
+                :db-init
+                :db-execute
+                :db-select
+                :load-settings)
+  (:import-from :chatikbot.utils
+                :*bot-name*
+                :*admins*
+                :aget
+                :flatten
+                :format-ts
+                :run-hooks
+                :loop-with-error-backoff)
+  (:import-from :chatikbot.telegram
+                :*telegram-token*
+                :telegram-get-me
+                :telegram-set-webhook
+                :telegram-send-message
+                :send-response)
+  (:import-from :chatikbot.secrets
+                :*secret-ring*
+                :*secret-pass-store*
+                :*secret-pass-bin*)
+  (:import-from :chatikbot.macros
+                :defcron)
+  (:import-from :chatikbot.bot
+                :process-updates)
+  (:import-from :chatikbot.server
+                :*web-path*
+                :*web-iface*
+                :*web-port*)
+  (:export :start))
+
+(in-package :chatikbot)
 
 
-(defvar *admins* nil "Admins chat-ids")
 (defvar *bot-user-id* nil "Bot user_id")
 (defvar *bot-user-id* nil "Bot user_id")
+(defvar *plugins* nil "list of enabled plugins.")
+
+(defun plugins-db-init ()
+  (db-execute "create table if not exists plugins (name)")
+  (db-execute "create unique index if not exists plugins_name_unique on plugins (name)"))
+
+(defun enable-plugin (name)
+  (load (merge-pathnames (format nil "plugins/~A.lisp" name)
+                         (asdf:component-pathname
+                          (asdf:find-system '#:chatikbot))))
+  (db-execute "replace into plugins (name) values (?)" name)
+  (push name *plugins*))
+
+(defun disable-plugin (name)
+  (db-execute "delete from plugins where name = ?" name)
+  (setf *plugins* (delete name *plugins* :test #'equal)))
+
+(eval-when (:load-toplevel :execute)
+  ;; Load config file
+  (alexandria:when-let (file (probe-file
+                              (merge-pathnames "config.lisp"
+                                               (asdf:component-pathname
+                                                (asdf:find-system '#:chatikbot)))))
+    (load file))
+
+  ;; Init database
+  (db-init)
+
+  ;; Load plugins
+  (plugins-db-init)
+  (setf *plugins* (flatten (db-select "select name from plugins")))
+  (dolist (plugin *plugins*)
+    (handler-case
+        (load (merge-pathnames (format nil "plugins/~A.lisp" plugin)
+                               (asdf:component-pathname
+                                (asdf:find-system '#:chatikbot))))
+      (error (e) (log:error e))))
+  ;; Load settings
+  (load-settings)
+  ;; Init plugin's database
+  (with-db (db)
+    (run-hooks :db-init)))
 
 
-;; Load config file
-(alexandria:when-let (file (probe-file
-                            (merge-pathnames "config.lisp"
-                                             (asdf:component-pathname
-                                              (asdf:find-system '#:chatikbot)))))
-  (load file))
-
-;; Init database
-(db-init)
-;; Load settings
-(load-settings)
-;; Load plugins
-(defsetting *plugins* nil "List of enabled plugins")
-(dolist (plugin *plugins*)
-  (handler-case
-      (load (merge-pathnames (format nil "plugins/~A.lisp" plugin)
-                             (asdf:component-pathname
-                              (asdf:find-system '#:chatikbot))))
-    (error (e) (log:error e))))
-;; Init plugin's database
-(with-db (db)
-  (run-hooks :db-init))
-
-(defvar *telegram-last-update* 0 "Telegram last update_id")
-
-;; getUpdates handling
-(defun process-updates ()
-  (loop for update in (telegram-get-updates :offset (and *telegram-last-update*
-                                                         (1+ *telegram-last-update*))
-                                            :timeout 300)
-     ;; do (setf *telegram-last-update*
-     ;;          (max *telegram-last-update* (aget "update_id" update)))
-     do (handle-update update)))
-
-(defun handle-update (update)
-  (log:info update)
-  (let ((update-id (aget "update_id" update))
-        (reply-to (aget "id" (aget "from" (aget "reply_to_message" (aget "message" update))))))
-    (if (> update-id *telegram-last-update*)
-        (progn
-          (if (and reply-to (not (equal reply-to *bot-user-id*)))
-              (log:info "Reply not to bot")
-              (loop for (key . value) in update
-                 unless (equal "update_id" key)
-                 do (run-hooks (keyify (format nil "update-~A" key)) value)))
-          (setf *telegram-last-update* update-id))
-        (log:warn "Out-of-order update" update-id))))
-
-(defvar *start-message* "Hello" "Welcome message. Override it")
-(def-message-cmd-handler handle-start (:start)
-  (send-response chat-id *start-message*))
-
-(def-message-admin-cmd-handler handle-admin-settings (:settings)
-  (send-response chat-id
-                 (format nil "~{~{~A~@[ (~A)~]: ~A~}~^~%~}"
-                         (loop for symbol in *settings*
-                            collect (list symbol (documentation symbol 'variable) (symbol-value symbol))))))
-
-(def-message-admin-cmd-handler handle-admin-setsetting (:setsetting)
-  (let* ((*package* (find-package :chatikbot))
-         (var (read-from-string (car args)))
-         (val (read-from-string (format nil "~{~A~^ ~}" (rest args)))))
-    (send-response chat-id (format nil "OK, ~A" (set-setting var val)))))
 
 
 (defcron process-watchdog ()
 (defcron process-watchdog ()
   (close
   (close

+ 123 - 0
common.lisp

@@ -0,0 +1,123 @@
+(in-package :cl-user)
+(defpackage chatikbot.common
+  (:use :cl
+        :chatikbot.db
+        :chatikbot.utils
+        :chatikbot.telegram
+        :chatikbot.crypto
+        :chatikbot.secrets
+        :chatikbot.server
+        :chatikbot.macros)
+  (:export :db-transaction
+           :db-execute
+           :db-select
+           :db-single
+           :set-setting
+           :lists-set-entry
+           :lists-get
+           :*admins*
+           :*bot-name*
+           :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
+           :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-float
+           :smart-f
+           :format-size
+           :format-interval
+           :symbol-append
+           :telegram-get-me
+           :telegram-send-message
+           :telegram-forward-message
+           :telegram-send-photo
+           :telegram-send-audio
+           :telegram-send-document
+           :telegram-send-sticker
+           :telegram-send-video
+           :telegram-send-voice
+           :telegram-send-location
+           :telegram-send-chat-action
+           :telegram-send-get-user-profile-photos
+           :telegram-send-get-file
+           :telegram-answer-callback-query
+           :telegram-edit-message-text
+           :telegram-edit-message-caption
+           :telegram-edit-message-reply-markup
+           :telegram-answer-inline-query
+           :telegram-file-contents
+           :telegram-inline-keyboard-markup
+           :telegram-reply-keyboard-markup
+           :telegram-reply-keyboard-hide
+           :telegram-force-reply
+           :send-response
+           :bot-send-message
+           :token-hmac
+           :encode-callback-data
+           :decode-callback-data
+           :encode-oauth-state
+           :decode-oauth-state
+           :secret-get
+           :secret-set
+           :secret-del
+           :secret-wipe
+           :with-secret
+           :def-db-init
+           :def-webhook-handler
+           :get-webhook-url
+           :get-oauth-url
+           :hook
+           :data
+           :headers
+           :paths
+           :def-db-init
+           :def-message-handler
+           :message-id
+           :from-id
+           :chat-id
+           :text
+           :def-message-cmd-handler
+           :def-message-admin-cmd-handler
+           :cmd
+           :args
+           :def-callback-handler
+           :query-id
+           :from
+           :raw-data
+           :message
+           :inline-message-id
+           :def-callback-section-handler
+           :data
+           :section
+           :def-oauth-handler
+           :def-oauth-section-handler
+           :code
+           :error
+           :raw-state
+           :state
+           :defcron))
+(in-package :chatikbot.common)

+ 53 - 0
crypto.lisp

@@ -0,0 +1,53 @@
+(in-package :cl-user)
+(defpackage chatikbot.crypto
+  (:use :cl)
+  (:import-from :chatikbot.telegram
+                :*telegram-token*
+                :+telegram-max-callback-data-length+)
+  (:export :token-hmac
+           :encode-callback-data
+           :decode-callback-data
+           :encode-oauth-state
+           :decode-oauth-state))
+(in-package #:chatikbot.crypto)
+
+(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")))))
+
+(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"))))

+ 19 - 11
db.lisp

@@ -1,4 +1,18 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.db
+  (:use :cl)
+  (:import-from :chatikbot.utils
+                :dekeyify)
+  (:export :db-transaction
+           :db-execute
+           :db-select
+           :db-single
+           :db-init
+           :load-settings
+           :set-setting
+           :lists-set-entry
+           :lists-get))
+(in-package :chatikbot.db)
 
 
 (defvar *db-name* "db.sqlite" "SQLite database name")
 (defvar *db-name* "db.sqlite" "SQLite database name")
 
 
@@ -43,12 +57,6 @@
   (with-db (db)
   (with-db (db)
     (apply #'sqlite:execute-single db sql parameters)))
     (apply #'sqlite:execute-single db sql parameters)))
 
 
-(defmacro def-db-init (&body body)
-  `(add-hook :db-init #'(lambda ()
-                          (handler-case (progn ,@body)
-                            (error (e) (log:error e)))
-                          (values))))
-
 (defun db-init ()
 (defun db-init ()
   (with-db (db)
   (with-db (db)
     (db-execute "create table if not exists settings (var, val)")
     (db-execute "create table if not exists settings (var, val)")
@@ -59,15 +67,15 @@
 (defun load-settings ()
 (defun load-settings ()
   (let ((*package* (find-package :chatikbot)))
   (let ((*package* (find-package :chatikbot)))
     (loop for (var val) in (db-select "select var, val from settings")
     (loop for (var val) in (db-select "select var, val from settings")
-       do (setf (symbol-value (intern var))
-                (handler-case (read-from-string val)
-                  (error (e) (log:error e)))))))
+       do (handler-case (setf (symbol-value (read-from-string var))
+                              (read-from-string val))
+            (error (e) (log:error "~A" e))))))
 
 
 (defun set-setting (symbol value)
 (defun set-setting (symbol value)
   (handler-case
   (handler-case
       (let ((*package* (find-package :chatikbot)))
       (let ((*package* (find-package :chatikbot)))
         (db-execute "replace into settings (var, val) values (?, ?)"
         (db-execute "replace into settings (var, val) values (?, ?)"
-                    (symbol-name symbol)
+                    (write-to-string symbol)
                     (write-to-string value))
                     (write-to-string value))
         (setf (symbol-value symbol) value))
         (setf (symbol-value symbol) value))
     (error (e) (log:error e))))
     (error (e) (log:error e))))

+ 46 - 0
eliza.lisp

@@ -0,0 +1,46 @@
+(in-package :cl-user)
+(defpackage chatikbot.eliza
+  (:use :cl
+        :chatikbot.patmatch
+        :chatikbot.utils)
+  (:export :?is
+           :?or
+           :?and
+           :?not
+           :?*
+           :?+
+           :??
+           :?if
+           :eliza
+           :read-from-string-no-punct))
+(in-package :chatikbot.eliza)
+
+(defun punctuation-p (char)
+  (find char ".,;:'!?#-()\\\""))
+
+(defun read-from-string-no-punct (input)
+  "Read from an input string, ignoring punctuation."
+  (read-from-string
+   (concatenate 'string "(" (substitute-if #\space #'punctuation-p input) ")")))
+
+(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 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))))))

+ 120 - 0
macros.lisp

@@ -0,0 +1,120 @@
+(in-package :cl-user)
+(defpackage chatikbot.macros
+  (:use :cl :chatikbot.utils :chatikbot.telegram :chatikbot.crypto)
+  (:export :def-db-init
+           :def-message-handler
+           :def-message-cmd-handler
+           :def-message-admin-cmd-handler
+           :def-callback-handler
+           :def-callback-section-handler
+           :def-oauth-handler
+           :def-oauth-section-handler
+           :defcron))
+(in-package #:chatikbot.macros)
+
+(defmacro def-db-init (&body body)
+  `(add-hook :db-init #'(lambda ()
+                          (handler-case (progn ,@body)
+                            (error (e) (log:error e)))
+                          (values))))
+
+(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)))))
+
+(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))))
+
+;; 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))))

+ 0 - 5
package.lisp

@@ -1,5 +0,0 @@
-(defpackage #:chatikbot
-  (:use #:cl)
-  (:export #:start))
-
-(in-package #:chatikbot)

+ 23 - 11
patmatch.lisp

@@ -8,7 +8,19 @@
 
 
 ;;; The basic are in auxfns.lisp; look for "PATTERN MATCHING FACILITY"
 ;;; The basic are in auxfns.lisp; look for "PATTERN MATCHING FACILITY"
 
 
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.patmatch
+  (:use :cl)
+  (:export :?is
+           :?or
+           :?and
+           :?not
+           :?*
+           :?+
+           :??
+           :?if
+           :rule-based-translator))
+(in-package #:chatikbot.patmatch)
 
 
 (defconstant fail nil "Indicates pat-match failure")
 (defconstant fail nil "Indicates pat-match failure")
 
 
@@ -45,13 +57,13 @@
         ((variable-p pattern)
         ((variable-p pattern)
          (match-variable pattern input bindings))
          (match-variable pattern input bindings))
         ((eql pattern input) bindings)
         ((eql pattern input) bindings)
-        ((segment-pattern-p pattern)                
-         (segment-matcher pattern input bindings))  
+        ((segment-pattern-p pattern)
+         (segment-matcher pattern input bindings))
         ((single-pattern-p pattern)                 ; ***
         ((single-pattern-p pattern)                 ; ***
          (single-matcher pattern input bindings))   ; ***
          (single-matcher pattern input bindings))   ; ***
-        ((and (consp pattern) (consp input)) 
+        ((and (consp pattern) (consp input))
          (pat-match (rest pattern) (rest input)
          (pat-match (rest pattern) (rest input)
-                    (pat-match (first pattern) (first input) 
+                    (pat-match (first pattern) (first input)
                                bindings)))
                                bindings)))
         (t fail)))
         (t fail)))
 
 
@@ -68,7 +80,7 @@
 
 
 (defun segment-pattern-p (pattern)
 (defun segment-pattern-p (pattern)
   "Is this a segment-matching pattern like ((?* var) . pat)?"
   "Is this a segment-matching pattern like ((?* var) . pat)?"
-  (and (consp pattern) (consp (first pattern)) 
+  (and (consp pattern) (consp (first pattern))
        (symbolp (first (first pattern)))
        (symbolp (first (first pattern)))
        (segment-match-fn (first (first pattern)))))
        (segment-match-fn (first (first pattern)))))
 
 
@@ -89,12 +101,12 @@
            (rest pattern) input bindings))
            (rest pattern) input bindings))
 
 
 (defun segment-match-fn (x)
 (defun segment-match-fn (x)
-  "Get the segment-match function for x, 
+  "Get the segment-match function for x,
   if it is a symbol that has one."
   if it is a symbol that has one."
   (when (symbolp x) (get x 'segment-match)))
   (when (symbolp x) (get x 'segment-match)))
 
 
 (defun single-match-fn (x)
 (defun single-match-fn (x)
-  "Get the single-match function for x, 
+  "Get the single-match function for x,
   if it is a symbol that has one."
   if it is a symbol that has one."
   (when (symbolp x) (get x 'single-match)))
   (when (symbolp x) (get x 'single-match)))
 
 
@@ -121,7 +133,7 @@
   "Succeed if any one of the patterns match the input."
   "Succeed if any one of the patterns match the input."
   (if (null patterns)
   (if (null patterns)
       fail
       fail
-      (let ((new-bindings (pat-match (first patterns) 
+      (let ((new-bindings (pat-match (first patterns)
                                      input bindings)))
                                      input bindings)))
         (if (eq new-bindings fail)
         (if (eq new-bindings fail)
             (match-or (rest patterns) input bindings)
             (match-or (rest patterns) input bindings)
@@ -179,11 +191,11 @@
   (and (progv (mapcar #'car bindings)
   (and (progv (mapcar #'car bindings)
               (mapcar #'cdr bindings)
               (mapcar #'cdr bindings)
           (eval (second (first pattern))))
           (eval (second (first pattern))))
-       (pat-match (rest pattern) input bindings)))  
+       (pat-match (rest pattern) input bindings)))
 
 
 (defun pat-match-abbrev (symbol expansion)
 (defun pat-match-abbrev (symbol expansion)
   "Define symbol as a macro standing for a pat-match pattern."
   "Define symbol as a macro standing for a pat-match pattern."
-  (setf (get symbol 'expand-pat-match-abbrev) 
+  (setf (get symbol 'expand-pat-match-abbrev)
     (expand-pat-match-abbrev expansion)))
     (expand-pat-match-abbrev expansion)))
 
 
 (defun expand-pat-match-abbrev (pat)
 (defun expand-pat-match-abbrev (pat)

+ 5 - 2
plugins/admin.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.admin
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.admin)
 
 
 (defmacro handling-errors (&body body)
 (defmacro handling-errors (&body body)
   `(handler-case (progn ,@body)
   `(handler-case (progn ,@body)
@@ -15,7 +18,7 @@
 (defun rep (input)
 (defun rep (input)
   (when input
   (when input
     (with-output-to-string (*standard-output*)
     (with-output-to-string (*standard-output*)
-      (let ((*package* (find-package 'chatikbot))
+      (let ((*package* (find-package 'chatikbot.common))
             (*error-output* *standard-output*))
             (*error-output* *standard-output*))
         (handling-errors
         (handling-errors
           (format t "~{~S~^ ;~%     ~}~%"
           (format t "~{~S~^ ;~%     ~}~%"

+ 4 - 1
plugins/finance.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.finance
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.finance)
 
 
 (defparameter +yahoo-url+ "https://query1.finance.yahoo.com/v7/finance/quote?lang=en-US&region=US&corsDomain=finance.yahoo.com&fields=regularMarketPrice" "Yahoo Finance API endpoint")
 (defparameter +yahoo-url+ "https://query1.finance.yahoo.com/v7/finance/quote?lang=en-US&region=US&corsDomain=finance.yahoo.com&fields=regularMarketPrice" "Yahoo Finance API endpoint")
 (defparameter +brent-url+ "http://www.cmegroup.com/CmeWS/mvc/Quotes/Future/424/G")
 (defparameter +brent-url+ "http://www.cmegroup.com/CmeWS/mvc/Quotes/Future/424/G")

+ 5 - 1
plugins/forecast.lisp

@@ -1,4 +1,8 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.forecast
+  (:use :cl :chatikbot.common)
+  (:export :*forecast-api-key*))
+(in-package :chatikbot.plugins.forecast)
 
 
 (defsetting *forecast-api-key* nil "forecast.io APIKEY")
 (defsetting *forecast-api-key* nil "forecast.io APIKEY")
 (defparameter +forecast-api-url+ "https://api.darksky.net/forecast" "forecast.io API endpoint")
 (defparameter +forecast-api-url+ "https://api.darksky.net/forecast" "forecast.io API endpoint")

+ 9 - 4
plugins/foursquare.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.foursquare
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.foursquare)
 
 
 (defparameter *fsq-checkins-url* "https://api.foursquare.com/v2/checkins/recent"
 (defparameter *fsq-checkins-url* "https://api.foursquare.com/v2/checkins/recent"
   "URL of recent checkins API")
   "URL of recent checkins API")
@@ -192,6 +195,8 @@
       (when users
       (when users
         (bot-send-message chat-id
         (bot-send-message chat-id
                           (format nil "~{~A~^~%~}"
                           (format nil "~{~A~^~%~}"
-                                  (loop for checkin in (fsq-fetch-checkins token)
-                                     if (member (aget "id" (aget "user" checkin)) users :test #'equal)
-                                     collect (fsq-format-checkin checkin t))))))))
+                                  (or
+                                   (loop for checkin in (fsq-fetch-checkins token)
+                                      if (member (aget "id" (aget "user" checkin)) users :test #'equal)
+                                      collect (fsq-format-checkin checkin t))
+                                   '("Нету"))))))))

+ 6 - 3
plugins/google.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.google
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.google)
 
 
 (defparameter +google-search-url+ "http://www.google.com/search")
 (defparameter +google-search-url+ "http://www.google.com/search")
 
 
@@ -13,8 +16,8 @@
      when (and q (equal (quri:uri-path uri) "/url"))
      when (and q (equal (quri:uri-path uri) "/url"))
      collect (list
      collect (list
               (cons :url q)
               (cons :url q)
-              (cons :title (select-text "" a))
-              (cons :desc (select-text ".st" result)))))
+              (cons :title (select-text a))
+              (cons :desc (select-text result ".st")))))
 
 
 (defun google-format-search-results (results)
 (defun google-format-search-results (results)
   (format nil "~{~A. ~A~^~%~}"
   (format nil "~{~A. ~A~^~%~}"

+ 8 - 11
plugins/gsheets.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.gsheets
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.gsheets)
 
 
 (defsetting *gsheets-client-id* nil "google oauth2 client id")
 (defsetting *gsheets-client-id* nil "google oauth2 client id")
 (defsetting *gsheets-client-secret* nil "client secret")
 (defsetting *gsheets-client-secret* nil "client secret")
@@ -29,9 +32,7 @@
     (quri:make-uri :query (quri:url-encode-params
     (quri:make-uri :query (quri:url-encode-params
                            `(("response_type" . "code")
                            `(("response_type" . "code")
                              ("client_id" . ,*gsheets-client-id*)
                              ("client_id" . ,*gsheets-client-id*)
-                             ("redirect_uri" . ,(quri:render-uri
-                                                 (quri:merge-uris (quri:uri "/oauth")
-                                                                  (quri:uri *web-path*))))
+                             ("redirect_uri" . ,(get-oauth-url))
                              ("scope" . ,+gsheets-scope+)
                              ("scope" . ,+gsheets-scope+)
                              ("state" . ,(encode-oauth-state :gsheets state))
                              ("state" . ,(encode-oauth-state :gsheets state))
                              ("access_type" . "offline")
                              ("access_type" . "offline")
@@ -63,9 +64,7 @@
                                              (cons "code" code)
                                              (cons "code" code)
                                              (cons "client_id" *gsheets-client-id*)
                                              (cons "client_id" *gsheets-client-id*)
                                              (cons "client_secret" *gsheets-client-secret*)
                                              (cons "client_secret" *gsheets-client-secret*)
-                                             (cons "redirect_uri" (quri:render-uri
-                                                                   (quri:merge-uris (quri:uri "/oauth")
-                                                                                    (quri:uri *web-path*))))
+                                             (cons "redirect_uri" (get-oauth-url))
                                              (cons "grant_type" "authorization_code"))))
                                              (cons "grant_type" "authorization_code"))))
                (access-token (aget "access_token" resp))
                (access-token (aget "access_token" resp))
                (refresh-token (aget "refresh_token" resp))
                (refresh-token (aget "refresh_token" resp))
@@ -93,7 +92,7 @@
            (response (json-request
            (response (json-request
                       (quri:render-uri (quri:merge-uris (quri:make-uri :path path) (quri:uri base-url)))
                       (quri:render-uri (quri:merge-uris (quri:make-uri :path path) (quri:uri base-url)))
                       :method method :parameters parameters :content content
                       :method method :parameters parameters :content content
-                      :additional-headers (list (cons "Authorization" (format nil "Bearer ~A" access-token)))))
+                      :headers (list (cons "Authorization" (format nil "Bearer ~A" access-token)))))
            (err (aget "error" response)))
            (err (aget "error" response)))
       (if (and err (equal 401 (aget "code" err)) (not is-retry) (gsheets-refresh-access-token token-id))
       (if (and err (equal 401 (aget "code" err)) (not is-retry) (gsheets-refresh-access-token token-id))
           ;; Retry in case of auth error and successful token refresh
           ;; Retry in case of auth error and successful token refresh
@@ -116,9 +115,7 @@
                      (list "kind" "api#channel"
                      (list "kind" "api#channel"
                            "id" (princ-to-string (uuid:make-v4-uuid))
                            "id" (princ-to-string (uuid:make-v4-uuid))
                            "type" "web_hook"
                            "type" "web_hook"
-                           "address" (quri:render-uri
-                                      (quri:merge-uris (quri:uri (format nil "/hook/~A" webhook))
-                                                       (quri:uri *web-path*))))
+                           "address" (get-webhook-url webhook))
                      (when token (list "token" token))
                      (when token (list "token" token))
                      (when expiration (list "expiration" expiration))
                      (when expiration (list "expiration" expiration))
                      (when payload (list "payload" "true"))
                      (when payload (list "payload" "true"))

+ 6 - 3
plugins/huiza.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.huiza
+  (:use :cl :chatikbot.common :chatikbot.eliza))
+(in-package :chatikbot.plugins.huiza)
 
 
 (defparameter *fuck-off*
 (defparameter *fuck-off*
   '((отъебись) (мне похуй) (ебаный ты нахуй!))
   '((отъебись) (мне похуй) (ебаный ты нахуй!))
@@ -44,8 +47,8 @@
      (:sticker . "BQADBAADQAEAAnscSQABqWydSKTnASoC"))))
      (:sticker . "BQADBAADQAEAAnscSQABqWydSKTnASoC"))))
 
 
 (defun send-dont-understand (chat-id &optional text reply-id)
 (defun send-dont-understand (chat-id &optional text reply-id)
-  (let ((resp (eliza text *huiza-rules*)))
-    (log:info text resp)
+  (let* ((*package* (find-package :chatikbot.plugins.huiza))
+         (resp (eliza (read-from-string-no-punct text) *huiza-rules*)))
     (when resp
     (when resp
       (send-response chat-id resp reply-id))))
       (send-response chat-id resp reply-id))))
 
 

+ 25 - 16
plugins/ledger.lisp

@@ -1,24 +1,34 @@
-(in-package #:chatikbot)
-(ql:quickload :pta-ledger)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.ledger
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.ledger)
+
+(eval-when (:compile-toplevel :load-toplevel :execute)
+  (ql:quickload :pta-ledger))
 
 
 (defsetting *ledger/default-timezone* -3 "Default timezone for time display. GMT+3")
 (defsetting *ledger/default-timezone* -3 "Default timezone for time display. GMT+3")
 (defvar *ledger/chat-journals* (make-hash-table))
 (defvar *ledger/chat-journals* (make-hash-table))
 
 
 (defun ledger/get-hook-url (chat-id url)
 (defun ledger/get-hook-url (chat-id url)
   (declare (ignore url))
   (declare (ignore url))
-  (get-webhook-url "ledger" chat-id (token-hmac (format nil "~A" chat-id))))
+  (get-webhook-url "ledger" chat-id (token-hmac (write-to-string chat-id))))
 
 
 (defun ledger/parse-uri (chat-id uri)
 (defun ledger/parse-uri (chat-id uri)
   (setf (gethash chat-id *ledger/chat-journals*)
   (setf (gethash chat-id *ledger/chat-journals*)
         (cons (pta-ledger:parse-journal (http-request uri))
         (cons (pta-ledger:parse-journal (http-request uri))
               (get-universal-time))))
               (get-universal-time))))
 
 
+(defun ledger/format-uri (url)
+  (let ((uri (quri:uri url)))
+    (quri:render-uri (quri:make-uri :userinfo (when (quri:uri-userinfo uri) "*:*")
+                                    :defaults url))))
+
 (defun ledger/handle-set-uri (chat-id uri)
 (defun ledger/handle-set-uri (chat-id uri)
   (handler-case
   (handler-case
       (destructuring-bind (journal . ut)
       (destructuring-bind (journal . ut)
           (ledger/parse-uri chat-id uri)
           (ledger/parse-uri chat-id uri)
         (declare (ignore ut))
         (declare (ignore ut))
-        (secret/set (list :ledger chat-id) uri)
+        (secret-set (list :ledger chat-id) uri)
         (send-response chat-id (format nil "Добавил журнал с ~D записями. Веб-хук для обновления: ~A"
         (send-response chat-id (format nil "Добавил журнал с ~D записями. Веб-хук для обновления: ~A"
                                        (length journal)
                                        (length journal)
                                        (ledger/get-hook-url chat-id uri))))
                                        (ledger/get-hook-url chat-id uri))))
@@ -36,15 +46,14 @@
               year month day hour min sec))))
               year month day hour min sec))))
 
 
 (defun ledger/handle-info (chat-id)
 (defun ledger/handle-info (chat-id)
-  (secret/with (uri (list :ledger chat-id))
+  (with-secret (uri (list :ledger chat-id))
     (if uri
     (if uri
         (destructuring-bind (journal . ut)
         (destructuring-bind (journal . ut)
             (or (gethash chat-id *ledger/chat-journals*)
             (or (gethash chat-id *ledger/chat-journals*)
                 (ledger/parse-uri chat-id uri))
                 (ledger/parse-uri chat-id uri))
           (bot-send-message chat-id (format nil "Журнал с ~D записями, из ~A, обновлён ~A.~%Веб-хук: ~A"
           (bot-send-message chat-id (format nil "Журнал с ~D записями, из ~A, обновлён ~A.~%Веб-хук: ~A"
                                             (length journal)
                                             (length journal)
-                                            (quri:render-uri (quri:make-uri :userinfo nil
-                                                                            :defaults uri))
+                                            (ledger/format-uri uri)
                                             (ledger/format-time ut)
                                             (ledger/format-time ut)
                                             (ledger/get-hook-url chat-id uri))
                                             (ledger/get-hook-url chat-id uri))
                             :disable-web-preview t))
                             :disable-web-preview t))
@@ -56,14 +65,14 @@
     (:otherwise (ledger/handle-info chat-id))))
     (:otherwise (ledger/handle-info chat-id))))
 
 
 (defun ledger/handle-balance (chat-id query)
 (defun ledger/handle-balance (chat-id query)
-  (destructuring-bind (journal . ut)
-      (gethash chat-id *ledger/chat-journals*)
-    (if journal
-        (bot-send-message chat-id (format nil "```~%~A~%```Обновлено: ~A"
-                                          (pta-ledger:journal-balance journal query)
-                                          (ledger/format-time ut))
-                          :parse-mode "markdown")
-        (secret/with (uri (list :ledger chat-id))
+  (let ((pair (gethash chat-id *ledger/chat-journals*)))
+    (if pair
+        (destructuring-bind (journal . ut) pair
+          (bot-send-message chat-id (format nil "```~%~A~%```Обновлено: ~A"
+                                            (pta-ledger:journal-balance journal query)
+                                            (ledger/format-time ut))
+                            :parse-mode "markdown"))
+        (with-secret (uri (list :ledger chat-id))
           (if uri
           (if uri
               (progn (ledger/parse-uri chat-id uri)
               (progn (ledger/parse-uri chat-id uri)
                      (ledger/handle-balance chat-id query))
                      (ledger/handle-balance chat-id query))
@@ -79,6 +88,6 @@
     (destructuring-bind (chat-id hmac) paths
     (destructuring-bind (chat-id hmac) paths
       (let ((true-hmac (token-hmac chat-id)))
       (let ((true-hmac (token-hmac chat-id)))
         (when (string= true-hmac hmac)
         (when (string= true-hmac hmac)
-          (secret/with (uri (list :ledger chat-id))
+          (with-secret (uri (list :ledger chat-id))
             (when uri
             (when uri
               (ledger/parse-uri (parse-integer chat-id) uri))))))))
               (ledger/parse-uri (parse-integer chat-id) uri))))))))

+ 12 - 19
plugins/nalunch.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.nalunch
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.nalunch)
 
 
 (defvar *nalunch/calend* nil "Working calendar exceptions")
 (defvar *nalunch/calend* nil "Working calendar exceptions")
 
 
@@ -49,10 +52,10 @@
              (balance (parse-integer (plump:text (elt (clss:select ".newswire-header_balance" dom) 0))))
              (balance (parse-integer (plump:text (elt (clss:select ".newswire-header_balance" dom) 0))))
              (recent (loop for day across (clss:select ".day-feed" dom)
              (recent (loop for day across (clss:select ".day-feed" dom)
                         append (loop for el across (clss:select ".media" day)
                         append (loop for el across (clss:select ".media" day)
-                                  for date = (select-text ".day-feed_date" day)
-                                  for time = (select-text ".transaction_time" el)
-                                  for price = (parse-integer (select-text ".transaction_price" el))
-                                  for place = (select-text ".transaction-title" el)
+                                  for date = (select-text day ".day-feed_date")
+                                  for time = (select-text el ".transaction_time")
+                                  for price = (parse-integer (select-text el ".transaction_price"))
+                                  for place = (select-text el ".transaction-title")
                                   collect (list (cons :time (format nil "~A ~A" date time))
                                   collect (list (cons :time (format nil "~A ~A" date time))
                                                 (cons :price price)
                                                 (cons :price price)
                                                 (cons :place place))))))
                                                 (cons :place place))))))
@@ -98,7 +101,7 @@
 (defvar *nalunch/jars* (make-hash-table) "Cookie jars")
 (defvar *nalunch/jars* (make-hash-table) "Cookie jars")
 (defcron process-nalunch (:minute '(member 0 10 20 30 40 50))
 (defcron process-nalunch (:minute '(member 0 10 20 30 40 50))
   (dolist (chat-id (lists-get :nalunch))
   (dolist (chat-id (lists-get :nalunch))
-    (secret/with (login-pass (list :nalunch chat-id))
+    (with-secret (login-pass (list :nalunch chat-id))
       (if login-pass
       (if login-pass
           (let* ((cookie-jar (or (gethash chat-id *nalunch/jars*)
           (let* ((cookie-jar (or (gethash chat-id *nalunch/jars*)
                                  (cl-cookie:make-cookie-jar)))
                                  (cl-cookie:make-cookie-jar)))
@@ -111,19 +114,9 @@
               (setf (gethash chat-id *nalunch/last-results*) new
               (setf (gethash chat-id *nalunch/last-results*) new
                     (gethash chat-id *nalunch/jars*) cookie-jar)))
                     (gethash chat-id *nalunch/jars*) cookie-jar)))
           (progn
           (progn
-            (log:warn "nalunch no login/pass for" chat-id)
-;;            (lists-set-entry :nalunch chat-id nil)  ;; Comment out for now
-            )))))
+            (log:warn "nalunch no login/pass for" chat-id))))))
 
 
 ;; Hooks
 ;; Hooks
-(def-message-cmd-handler handle-cmd-nalunch (:nalunch)
-  (if (member chat-id *admins*)
-      (send-response chat-id (nalunch-format
-                              (or *nalunch-last-result*
-                                  (setf *nalunch-last-result*
-                                        (nalunch-recent)))))
-      (send-dont-understand chat-id)))
-
 (defun nalunch/handle-set-cron (chat-id enable)
 (defun nalunch/handle-set-cron (chat-id enable)
   (lists-set-entry :nalunch chat-id enable)
   (lists-set-entry :nalunch chat-id enable)
   (bot-send-message chat-id
   (bot-send-message chat-id
@@ -136,12 +129,12 @@
     (handler-case
     (handler-case
         (progn
         (progn
           (nalunch/auth login pass cookies)
           (nalunch/auth login pass cookies)
-          (secret/set `(:nalunch ,chat-id) (cons login pass))
+          (secret-set `(:nalunch ,chat-id) (cons login pass))
           (nalunch/handle-set-cron chat-id t))
           (nalunch/handle-set-cron chat-id t))
       (error () (bot-send-message chat-id "Чот не смог, пропробуй другие.")))))
       (error () (bot-send-message chat-id "Чот не смог, пропробуй другие.")))))
 
 
 (defun nalunch/handle-recent (chat-id)
 (defun nalunch/handle-recent (chat-id)
-  (secret/with (login-pass (list :nalunch chat-id))
+  (with-secret (login-pass (list :nalunch chat-id))
     (bot-send-message chat-id
     (bot-send-message chat-id
                       (if login-pass
                       (if login-pass
                           (let* ((cookies (or (gethash chat-id *nalunch/jars*)
                           (let* ((cookies (or (gethash chat-id *nalunch/jars*)

+ 4 - 25
plugins/rss.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.rss
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.rss)
 
 
 (defstruct feed id url title next-fetch (period 300))
 (defstruct feed id url title next-fetch (period 300))
 (defstruct feed-item feed guid link title description published)
 (defstruct feed-item feed guid link title description published)
@@ -46,30 +49,6 @@
           (feed-next-fetch feed) (local-time:timestamp+ (local-time:now) new-period :sec))
           (feed-next-fetch feed) (local-time:timestamp+ (local-time:now) new-period :sec))
     items))
     items))
 
 
-(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)))))
-
 (defun %send-feeds (chat-id feeds)
 (defun %send-feeds (chat-id feeds)
   (bot-send-message chat-id
   (bot-send-message chat-id
                     (if (null feeds)
                     (if (null feeds)

+ 4 - 1
plugins/saver.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.saver
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.saver)
 
 
 (defsetting *saver-default-timezone* -3 "Default timezone for *saver-notify-hour* calculation. GMT+3")
 (defsetting *saver-default-timezone* -3 "Default timezone for *saver-notify-hour* calculation. GMT+3")
 (defsetting *saver-notify-hour* 11 "Notify with upcoming payments and saves at this time")
 (defsetting *saver-notify-hour* 11 "Notify with upcoming payments and saves at this time")

+ 24 - 21
plugins/transmission.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.transmission
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.transmission)
 
 
 (defsetting *transmission-settings* nil "ALIST of (chat-id . url)")
 (defsetting *transmission-settings* nil "ALIST of (chat-id . url)")
 (defsetting *transmission-locations* nil "ALIST of (name . location)")
 (defsetting *transmission-locations* nil "ALIST of (name . location)")
@@ -15,8 +18,8 @@
 
 
 (defun transmission-set-session (url session-id)
 (defun transmission-set-session (url session-id)
   (setf *transmission-sessions*
   (setf *transmission-sessions*
-	(cons (cons url session-id)
-	      (remove url *transmission-sessions* :test #'equal :key #'car))))
+    (cons (cons url session-id)
+          (remove url *transmission-sessions* :test #'equal :key #'car))))
 
 
 (defun transmission-request (url method &rest arguments)
 (defun transmission-request (url method &rest arguments)
   (let ((retries (getf arguments :retries 0)))
   (let ((retries (getf arguments :retries 0)))
@@ -24,25 +27,25 @@
       (error "Too many retries"))
       (error "Too many retries"))
     (remf arguments :retries)
     (remf arguments :retries)
     (let ((session-id (aget url *transmission-sessions*))
     (let ((session-id (aget url *transmission-sessions*))
-	  (content
-	   (with-output-to-string (stream)
-	     (yason:encode (alexandria:plist-hash-table
-			    (list "method" (dekeyify method t)
-				  "arguments" (alexandria:plist-hash-table
-					       (loop for (key value) on arguments by #'cddr
-						  when value
-						  appending (list (dekeyify key) value)))))
-			   stream))))
+      (content
+       (with-output-to-string (stream)
+         (yason:encode (alexandria:plist-hash-table
+                (list "method" (dekeyify method t)
+                  "arguments" (alexandria:plist-hash-table
+                           (loop for (key value) on arguments by #'cddr
+                          when value
+                          appending (list (dekeyify key) value)))))
+               stream))))
       (handler-case
       (handler-case
-	  (let* ((response (json-request url :method :post :content content
-					 :headers (list (cons :x-transmission-session-id session-id))))
-		 (result (aget "result" response)))
-	    (unless (equal "success" result)
-	      (error result))
-	    (aget "arguments" response))
-	(dex:http-request-conflict (e)
-	  (transmission-set-session url (gethash "x-transmission-session-id" (dex:response-headers e)))
-	  (apply #'transmission-request url method :retries (1+ retries) arguments))))))
+      (let* ((response (json-request url :method :post :content content
+                     :headers (list (cons :x-transmission-session-id session-id))))
+         (result (aget "result" response)))
+        (unless (equal "success" result)
+          (error result))
+        (aget "arguments" response))
+    (dex:http-request-conflict (e)
+      (transmission-set-session url (gethash "x-transmission-session-id" (dex:response-headers e)))
+      (apply #'transmission-request url method :retries (1+ retries) arguments))))))
 
 
 (defun transmission-get-torrents (url &optional ids (fields '("id" "name" "status" "percentDone" "eta" "totalSize")))
 (defun transmission-get-torrents (url &optional ids (fields '("id" "name" "status" "percentDone" "eta" "totalSize")))
   (aget "torrents" (transmission-request url :torrent-get :ids ids :fields fields)))
   (aget "torrents" (transmission-request url :torrent-get :ids ids :fields fields)))

+ 9 - 4
plugins/tumblr.lisp

@@ -1,4 +1,7 @@
-(in-package :chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.tumblr
+  (:use :cl :chatikbot.common :chatikbot.eliza))
+(in-package :chatikbot.plugins.tumblr)
 
 
 (defvar *tumblr-roll* nil "List of tumblr to select from")
 (defvar *tumblr-roll* nil "List of tumblr to select from")
 (defparameter *read-timeout* 5 "API request timeout")
 (defparameter *read-timeout* 5 "API request timeout")
@@ -53,6 +56,8 @@
   (tumblr-random-post :roll roll :type :photo :num num))
   (tumblr-random-post :roll roll :type :photo :num num))
 
 
 (def-message-handler handle-tumblr (message)
 (def-message-handler handle-tumblr (message)
-  (alexandria:when-let ((resp (eliza (preprocess-input text) *tumblr-rules*)))
-    (log:info resp)
-    (send-response chat-id resp)))
+  (let* ((*package* (find-package :chatikbot.plugins.tumblr))
+         (resp (eliza (read-from-string-no-punct text) *tumblr-rules*)))
+    (when resp
+      (log:info resp)
+      (send-response chat-id resp))))

+ 4 - 1
plugins/twitter.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.twitter
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.twitter)
 
 
 (defsetting *twitter-access-token* nil "OAuth access token")
 (defsetting *twitter-access-token* nil "OAuth access token")
 
 

+ 68 - 0
plugins/udmx.lisp

@@ -0,0 +1,68 @@
+(in-package #:chatikbot)
+
+(defparameter +udmx-vendor-id+ #x16C0 "VOTI")
+(defparameter +udmx-product-id+ #x05DC "Obdev's free shared PID")
+
+(defparameter +udmx-req-set-single-channel+ 1 "usb request for cmd_SetSingleChannel:
+        bmRequestType:	ignored by device, should be USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT
+        bRequest:		cmd_SetSingleChannel
+        wValue:			value of channel to set [0 .. 255]
+        wIndex:			channel index to set [0 .. 511]
+        wLength:		ignored")
+
+(defparameter +udmx-req-set-channel-range+ 2 "usb request for cmd_SetChannelRange:
+              bmRequestType:	ignored by device, should be USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT
+              bRequest:		cmd_SetChannelRange
+              wValue:			number of channels to set [1 .. 512-wIndex]
+              wIndex:			index of first channel to set [0 .. 511]
+              wLength:		length of data, must be >= wValue")
+
+(defparameter +udmx-req-start-bootloader+ #xf8 "Start Bootloader for Software updates")
+
+(defparameter +udmx-err-bad-channel+ 1)
+(defparameter +udmx-err-bad-value+ 2)
+
+(defparameter +udmx-timeout+ 5000)
+
+(defparameter +udmx-manufacturer+ "www.anyma.ch")
+(defparameter +udmx-product+ "uDMX")
+
+(defun udmx-find-device ()
+  (loop for device in (cl-libusb:usb-get-devices-by-ids +udmx-vendor-id+ +udmx-product-id+)
+     do (unwind-protect
+             (progn
+               (cl-libusb:usb-open device)
+               (when (and (equal (cl-libusb:usb-get-string device :manufacturer) +udmx-manufacturer+)
+                          (equal (cl-libusb:usb-get-string device :product) +udmx-product+))
+                 (return device)))
+          (when (cl-libusb:usb-open-p device)
+            (cl-libusb:usb-close device)))))
+
+(defparameter +udmx-request-type+
+  (logior (cffi:foreign-enum-value 'libusb-ffi::type :vendor)
+          (cffi:foreign-enum-value 'libusb-ffi::recip :device)
+          (cffi:foreign-enum-value 'libusb-ffi::endpoint :out)))
+
+(defun %udmx-set-single-channel (device channel value)
+  (declare (type (integer 0 511) channel)
+           (type (integer 0 255) value))
+  (static-vectors:with-static-vector (buffer 1)
+    (cl-libusb:usb-control-msg device +udmx-request-type+ +udmx-req-set-single-channel+
+                               value channel buffer +udmx-timeout+)))
+
+(defun %udmx-set-channel-range (device first-channel values)
+  (declare (type (integer 0 511) first-channel))
+  (static-vectors:with-static-vector (buffer (length values) :initial-contents values)
+    (cl-libusb:usb-control-msg device +udmx-request-type+ +udmx-req-set-channel-range+
+                               (length buffer) first-channel buffer +udmx-timeout+)))
+
+(defun udmx-set-channel (device channel value &rest more-values)
+  (unless (cl-libusb:usb-open-p device)
+    (cl-libusb:usb-open device))
+  (loop for retries from 0 to 100
+     do (ignore-errors
+          (return (values (if (consp more-values)
+                              (%udmx-set-channel-range device channel (cons value more-values))
+                              (%udmx-set-single-channel device channel value))
+                          retries)))
+     finally (return (values nil retries))))

+ 39 - 36
plugins/vk.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.vk
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.vk)
 
 
 (defparameter +vk-api-ver+ "5.53" "vk api version to use")
 (defparameter +vk-api-ver+ "5.53" "vk api version to use")
 (defparameter +vk-api-url+ "https://api.vk.com/method/~A?v=~A" "vk.com API endpoint")
 (defparameter +vk-api-url+ "https://api.vk.com/method/~A?v=~A" "vk.com API endpoint")
@@ -14,8 +17,8 @@
 
 
 (defun vk-get-authorization-url (&optional state &rest scopes)
 (defun vk-get-authorization-url (&optional state &rest scopes)
   (let ((scope (apply #'+ (mapcar #'(lambda (k) (cdr (assoc k +vk-scope-mapping+))) scopes))))
   (let ((scope (apply #'+ (mapcar #'(lambda (k) (cdr (assoc k +vk-scope-mapping+))) scopes))))
-    (format nil "~A?v=~A&client_id=~A&redirect_uri=~A/oauth~@[&scope=~A~]~@[&state=~A~]"
-            +vk-oauth-authorize+ +vk-api-ver+ *vk-app-client-id* *web-path* (unless (zerop scope) scope) state)))
+    (format nil "~A?v=~A&client_id=~A&redirect_uri=~A~@[&scope=~A~]~@[&state=~A~]"
+            +vk-oauth-authorize+ +vk-api-ver+ *vk-app-client-id* (get-oauth-url) (unless (zerop scope) scope) state)))
 
 
 (defun %vk-api-call (method &optional args)
 (defun %vk-api-call (method &optional args)
   (let* ((params (loop for (k . v) in args
   (let* ((params (loop for (k . v) in args
@@ -31,6 +34,38 @@
       (error (aget "error_msg" (aget "error" response))))
       (error (aget "error_msg" (aget "error" response))))
     (aget "response" response)))
     (aget "response" response)))
 
 
+;; Database
+(def-db-init
+  (db-execute "create table if not exists vk_walls (domain, last_id, next_fetch, period)")
+  (db-execute "create unique index if not exists vk_walls_domain_idx on vk_walls (domain)")
+
+  (db-execute "create table if not exists vk_chat_walls (chat_id, domain)")
+  (db-execute "create index if not exists vk_chat_walls_chat_idx on vk_chat_walls (chat_id)")
+  (db-execute "create index if not exists vk_chat_walls_domain_idx on vk_chat_walls (domain)"))
+
+(defun db/vk-ensure-domain (domain last-id)
+  (db-transaction
+    (unless (db-single "select domain from vk_walls where domain = ?" domain)
+      (db-execute "insert into vk_walls (domain, last_id, period) values (?, ?, 300)" domain last-id))))
+
+(defun db/vk-get-domain-chats (domain)
+  (flatten (db-select "select chat_id from vk_chat_walls where domain = ?" domain)))
+
+(defun db/vk-get-chat-domains (chat-id)
+  (flatten (db-select "select domain from vk_chat_walls where chat_id  = ?" chat-id)))
+
+(defun db/vk-add-chat-domain (chat-id domain)
+  (db-execute "insert into vk_chat_walls (chat_id, domain) values (?, ?)" chat-id domain))
+
+(defun db/vk-remove-chat-domain (chat-id domain)
+  (db-execute "delete from vk_chat_walls where chat_id = ? and domain = ?" chat-id domain))
+
+(defun db/vk-get-active-walls ()
+  (db-select "select domain, last_id, next_fetch, period from vk_walls w where exists (select 1 from vk_chat_walls where domain=w.domain)"))
+
+(defun db/vk-update-wall (domain last-id next-fetch period)
+  (db-execute "update vk_walls set last_id = ?, next_fetch = ?, period = ? where domain = ?" last-id next-fetch period domain))
+
 (defun vk-wall-get (&key owner-id domain offset count filter extended fields)
 (defun vk-wall-get (&key owner-id domain offset count filter extended fields)
   (%vk-api-call "wall.get"
   (%vk-api-call "wall.get"
                 `(("owner_id" . ,owner-id)
                 `(("owner_id" . ,owner-id)
@@ -136,38 +171,6 @@
              text)
              text)
      (if preview 0 1))))
      (if preview 0 1))))
 
 
-;; Database
-(def-db-init
-  (db-execute "create table if not exists vk_walls (domain, last_id, next_fetch, period)")
-  (db-execute "create unique index if not exists vk_walls_domain_idx on vk_walls (domain)")
-
-  (db-execute "create table if not exists vk_chat_walls (chat_id, domain)")
-  (db-execute "create index if not exists vk_chat_walls_chat_idx on vk_chat_walls (chat_id)")
-  (db-execute "create index if not exists vk_chat_walls_domain_idx on vk_chat_walls (domain)"))
-
-(defun db/vk-ensure-domain (domain last-id)
-  (db-transaction
-    (unless (db-single "select domain from vk_walls where domain = ?" domain)
-      (db-execute "insert into vk_walls (domain, last_id, period) values (?, ?, 300)" domain last-id))))
-
-(defun db/vk-get-domain-chats (domain)
-  (flatten (db-select "select chat_id from vk_chat_walls where domain = ?" domain)))
-
-(defun db/vk-get-chat-domains (chat-id)
-  (flatten (db-select "select domain from vk_chat_walls where chat_id  = ?" chat-id)))
-
-(defun db/vk-add-chat-domain (chat-id domain)
-  (db-execute "insert into vk_chat_walls (chat_id, domain) values (?, ?)" chat-id domain))
-
-(defun db/vk-remove-chat-domain (chat-id domain)
-  (db-execute "delete from vk_chat_walls where chat_id = ? and domain = ?" chat-id domain))
-
-(defun db/vk-get-active-walls ()
-  (db-select "select domain, last_id, next_fetch, period from vk_walls w where exists (select 1 from vk_chat_walls where domain=w.domain)"))
-
-(defun db/vk-update-wall (domain last-id next-fetch period)
-  (db-execute "update vk_walls set last_id = ?, next_fetch = ?, period = ? where domain = ?" last-id next-fetch period domain))
-
 ;; Cron
 ;; Cron
 (defcron process-walls ()
 (defcron process-walls ()
   (loop for (domain last-id next-fetch period) in (db/vk-get-active-walls)
   (loop for (domain last-id next-fetch period) in (db/vk-get-active-walls)
@@ -180,7 +183,7 @@
                      (remove last-id (reverse (aget "items" (vk-wall-get :domain domain)))
                      (remove last-id (reverse (aget "items" (vk-wall-get :domain domain)))
                              :test #'>= :key (lambda (p) (aget "id" p))))
                              :test #'>= :key (lambda (p) (aget "id" p))))
                     name)
                     name)
-                (setf period (adjust-period period (length new-posts)))
+                (setf period (chatikbot.plugins.rss::adjust-period period (length new-posts)))
                 (when new-posts
                 (when new-posts
                   (setf name (vk-get-name domain)))
                   (setf name (vk-get-name domain)))
                 (dolist (post new-posts)
                 (dolist (post new-posts)

+ 30 - 0
plugins/yit.lisp

@@ -0,0 +1,30 @@
+(in-package :cl-user)
+(defpackage chatikbot.plugins.yit
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.yit)
+
+(defun yit-info ()
+  (labels ((get-rows (url)
+             (rest (get-by-tag (plump:get-element-by-id (xml-request url) "apartmentList") "tr")))
+           (row-data (row)
+             (mapcar (lambda (e) (string-trim '(#\Newline #\Space) (plump:text e)))
+                     (get-by-tag row "td")))
+           (format-data (data)
+             (format nil "~{~A~^ ~}" (mapcar (lambda (n) (nth n data)) '(1 2 3 4 7 6))))
+           (get-intresting (rows)
+             (loop for row in rows
+                for data = (row-data row)
+                for rooms = (parse-integer (nth 2 data))
+                for area = (parse-float:parse-float (replace-all (nth 3 data) "," "."))
+                when (= rooms 3)
+                when (< 65 area 75)
+                collect data))
+           (format-apts (url)
+             (let ((apts (get-intresting (get-rows url))))
+               (format nil "~A~%~{~A~^~%~}~%~A/~A" url (mapcar #'format-data apts)
+                       (length (remove "забронировано" apts :test #'equal :key #'(lambda (r) (nth 7 r)) ))
+                       (length apts)))))
+    (format nil "~{~A~^~%~%~}"
+            (mapcar #'format-apts
+                    '("http://www.yitspb.ru/yit_spb/catalog/apartments/novoorlovskiy-korpus-1-1-1"
+                      "http://www.yitspb.ru/yit_spb/catalog/apartments/novoorlovskiy-korpus-1-1-2")))))

+ 4 - 1
plugins/zhanna.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.zhanna
+  (:use :cl :chatikbot.common :chatikbot.gsheets))
+(in-package :chatikbot.plugins.zhanna)
 
 
 (defsetting *zhanna-token-id* nil)
 (defsetting *zhanna-token-id* nil)
 (defsetting *zhanna-sheet-id* "1kLBodFUwcfbpdqe_d2d01MHij95NAVcKrmpjotYsUQk")
 (defsetting *zhanna-sheet-id* "1kLBodFUwcfbpdqe_d2d01MHij95NAVcKrmpjotYsUQk")

+ 9 - 8
plugins/zsd.lisp

@@ -1,4 +1,7 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.plugins.zsd
+  (:use :cl :chatikbot.common))
+(in-package :chatikbot.plugins.zsd)
 
 
 (defparameter +zsd-api-url+ "https://mcabinet.nch-spb.com/onyma/system/api/json")
 (defparameter +zsd-api-url+ "https://mcabinet.nch-spb.com/onyma/system/api/json")
 (defparameter +zsd-auth-url+ "https://mcabinet.nch-spb.com/onyma/system/api/jsonex?function=open_session")
 (defparameter +zsd-auth-url+ "https://mcabinet.nch-spb.com/onyma/system/api/jsonex?function=open_session")
@@ -53,7 +56,7 @@
     (when wall-diff
     (when wall-diff
       (format nil "ЗСД остаток: *~$р.*~%~%~{~A~^~%~}"
       (format nil "ЗСД остаток: *~$р.*~%~%~{~A~^~%~}"
               (parse-float (aget "remainder" (car (aget "contract" new))))
               (parse-float (aget "remainder" (car (aget "contract" new))))
-              (loop for item in (reverse wall-diff)
+              (loop for item in wall-diff
                  collect (%zsd/format-wall item (aget "pan" new)))))))
                  collect (%zsd/format-wall item (aget "pan" new)))))))
 
 
 (defun zsd/handle-set-cron (chat-id enable)
 (defun zsd/handle-set-cron (chat-id enable)
@@ -67,12 +70,12 @@
   (let ((token (zsd/auth login pass)))
   (let ((token (zsd/auth login pass)))
     (if token
     (if token
         (progn
         (progn
-          (secret/set `(:zsd ,chat-id) token)
+          (secret-set `(:zsd ,chat-id) token)
           (zsd/handle-set-cron chat-id t))
           (zsd/handle-set-cron chat-id t))
         (bot-send-message chat-id "Чот не смог, пропробуй другие."))))
         (bot-send-message chat-id "Чот не смог, пропробуй другие."))))
 
 
 (defun zsd/handle-recent (chat-id)
 (defun zsd/handle-recent (chat-id)
-  (secret/with (token (list :zsd chat-id))
+  (with-secret (token (list :zsd chat-id))
     (bot-send-message chat-id
     (bot-send-message chat-id
                       (if token
                       (if token
                           (let ((data (zsd/load-data token)))
                           (let ((data (zsd/load-data token)))
@@ -92,7 +95,7 @@
 (defvar *zsd/last-results* (make-hash-table) "Last check results")
 (defvar *zsd/last-results* (make-hash-table) "Last check results")
 (defcron process-zsd (:minute '(member 0 10 20 30 40 50))
 (defcron process-zsd (:minute '(member 0 10 20 30 40 50))
   (dolist (chat-id (lists-get :zsd))
   (dolist (chat-id (lists-get :zsd))
-    (secret/with (token (list :zsd chat-id))
+    (with-secret (token (list :zsd chat-id))
       (if token
       (if token
           (let ((old (gethash chat-id *zsd/last-results*))
           (let ((old (gethash chat-id *zsd/last-results*))
                 (new (zsd/load-data token)))
                 (new (zsd/load-data token)))
@@ -102,6 +105,4 @@
                   (bot-send-message chat-id changes :parse-mode "markdown")))
                   (bot-send-message chat-id changes :parse-mode "markdown")))
               (setf (gethash chat-id *zsd/last-results*) new)))
               (setf (gethash chat-id *zsd/last-results*) new)))
           (progn
           (progn
-            (log:warn "zsd no token for" chat-id)
-            ;;(lists-set-entry :zsd chat-id nil)
-            )))))
+            (log:warn "zsd no token for" chat-id))))))

+ 26 - 15
secrets.lisp

@@ -1,10 +1,21 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.secrets
+  (:use :cl)
+  (:export :*secret-ring*
+           :*secret-pass-store*
+           :*secret-pass-bin*
+           :secret-get
+           :secret-set
+           :secret-del
+           :secret-wipe
+           :with-secret))
+(in-package :chatikbot.secrets)
 
 
 (defvar *secret-ring* nil "GPG keyring path")
 (defvar *secret-ring* nil "GPG keyring path")
 (defvar *secret-pass-store* nil "pass store dir")
 (defvar *secret-pass-store* nil "pass store dir")
 (defvar *secret-pass-bin* "pass" "pass util binary")
 (defvar *secret-pass-bin* "pass" "pass util binary")
 
 
-(defun %secret/pass (cmd path &key input (output :string) error-output)
+(defun pass (cmd path &key input (output :string) error-output)
   (let ((input-stream (when input (make-string-input-stream input))))
   (let ((input-stream (when input (make-string-input-stream input))))
     (unwind-protect
     (unwind-protect
          (uiop:run-program
          (uiop:run-program
@@ -15,29 +26,29 @@
       (when input-stream
       (when input-stream
         (close input-stream)))))
         (close input-stream)))))
 
 
-(defun secret/get (path)
+(defun secret-get (path)
   (handler-case
   (handler-case
       (let ((*read-eval* nil))
       (let ((*read-eval* nil))
-        (values (read-from-string (%secret/pass "show" path))))
+        (values (read-from-string (pass "show" path))))
     (error () (values))))
     (error () (values))))
 
 
-(defun secret/set (path value)
-  (%secret/pass "insert --force --multiline" path
-                :input (prin1-to-string value) :output nil :error-output :string))
+(defun secret-set (path value)
+  (pass "insert --force --multiline" path
+        :input (prin1-to-string value) :output nil :error-output :string))
 
 
-(defun secret/del (path)
-  (%secret/pass "rm --force" path))
+(defun secret-del (path)
+  (pass "rm --force" path))
 
 
-(defun secret/wipe (data)
+(defun secret-wipe (data)
   (cond
   (cond
     ((stringp data) (fill data #\Space))
     ((stringp data) (fill data #\Space))
     ((vectorp data) (fill data 0))
     ((vectorp data) (fill data 0))
     ((consp data)
     ((consp data)
-     (secret/wipe (car data))
-     (secret/wipe (cdr data)))))
+     (secret-wipe (car data))
+     (secret-wipe (cdr data)))))
 
 
-(defmacro secret/with ((var path) &body body)
-  `(let ((,var (ignore-errors (secret/get ,path))))
+(defmacro with-secret ((var path) &body body)
+  `(let ((,var (ignore-errors (secret-get ,path))))
      (unwind-protect
      (unwind-protect
           (progn ,@body)
           (progn ,@body)
-       (secret/wipe ,var))))
+       (secret-wipe ,var))))

+ 16 - 1
server.lisp

@@ -1,4 +1,14 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.server
+  (:use :cl :chatikbot.utils)
+  (:import-from :chatikbot.bot
+                :handle-update)
+  (:import-from :chatikbot.telegram
+                :*telegram-token*)
+  (:export :def-webhook-handler
+           :get-webhook-url
+           :get-oauth-url))
+(in-package :chatikbot.server)
 
 
 (defvar *web-path* nil "Set to externally accessible url")
 (defvar *web-path* nil "Set to externally accessible url")
 (defvar *web-iface* nil "Interface to listen on")
 (defvar *web-iface* nil "Interface to listen on")
@@ -29,6 +39,11 @@
     (error (e) (log:error e)))
     (error (e) (log:error e)))
   "OK")
   "OK")
 
 
+(defun get-oauth-url ()
+  (quri:render-uri
+   (quri:merge-uris (quri:uri "/oauth")
+                    (quri:uri *web-path*))))
+
 (hunchentoot:define-easy-handler (oauth-handler :uri "/oauth") (code error state)
 (hunchentoot:define-easy-handler (oauth-handler :uri "/oauth") (code error state)
   (handler-case
   (handler-case
       (run-hooks :oauth code error state)
       (run-hooks :oauth code error state)

+ 33 - 1
telegram.lisp

@@ -1,4 +1,36 @@
-(in-package #:chatikbot)
+(in-package :cl-user)
+(defpackage chatikbot.telegram
+  (:use :cl :chatikbot.utils)
+  (:export :*telegram-token*
+           :+telegram-max-callback-data-length+
+           :telegram-get-updates
+           :telegram-get-me
+           :telegram-set-webhook
+           :telegram-send-message
+           :telegram-forward-message
+           :telegram-send-photo
+           :telegram-send-audio
+           :telegram-send-document
+           :telegram-send-sticker
+           :telegram-send-video
+           :telegram-send-voice
+           :telegram-send-location
+           :telegram-send-chat-action
+           :telegram-send-get-user-profile-photos
+           :telegram-send-get-file
+           :telegram-answer-callback-query
+           :telegram-edit-message-text
+           :telegram-edit-message-caption
+           :telegram-edit-message-reply-markup
+           :telegram-answer-inline-query
+           :telegram-file-contents
+           :telegram-inline-keyboard-markup
+           :telegram-reply-keyboard-markup
+           :telegram-reply-keyboard-hide
+           :telegram-force-reply
+           :send-response
+           :bot-send-message))
+(in-package :chatikbot.telegram)
 
 
 (defvar *telegram-token* nil "Telegram bot token")
 (defvar *telegram-token* nil "Telegram bot token")
 (defparameter +telegram-api-format+ "https://api.telegram.org/bot~A/~A")
 (defparameter +telegram-api-format+ "https://api.telegram.org/bot~A/~A")

+ 97 - 183
utils.lisp

@@ -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)