1
0

utils.lisp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. (in-package #:chatikbot)
  2. (defvar *bot-name* nil "bot name to properly handle text input")
  3. (defvar *hooks* (make-hash-table) "Hooks storage")
  4. (defun run-hooks (event &rest arguments)
  5. (let ((hooks (gethash event *hooks*)))
  6. (labels ((try-handle (func)
  7. (apply func arguments)))
  8. (unless (some #'try-handle hooks)
  9. (log:info "unhandled" event arguments)))))
  10. (defun add-hook (event hook &optional append)
  11. (let ((existing (gethash event *hooks*)))
  12. (unless (member hook existing)
  13. (setf (gethash event *hooks*)
  14. (if append (append existing (list hook))
  15. (cons hook existing))))))
  16. (defun remove-hook (event hook)
  17. (setf (gethash event *hooks*)
  18. (remove hook (gethash event *hooks*))))
  19. (defun keyify (key)
  20. (intern (string-upcase (substitute #\- #\_ key)) :keyword))
  21. (defun dekeyify (keyword &optional preserve-dash)
  22. (let ((text (string-downcase (string keyword))))
  23. (if preserve-dash text (substitute #\_ #\- text))))
  24. ;; Settings
  25. (defvar *settings* nil "List of plugin's settings symbols")
  26. (defmacro defsetting (var &optional val doc)
  27. `(progn (defvar ,var ,val ,doc)
  28. (push ',var *settings*)))
  29. (defvar *backoff-start* 1 "Initial back-off")
  30. (defvar *backoff-max* 64 "Maximum back-off delay")
  31. (defun loop-with-error-backoff (func)
  32. (let ((backoff *backoff-start*))
  33. (loop
  34. do
  35. (handler-case
  36. (progn
  37. (funcall func)
  38. (setf backoff *backoff-start*))
  39. (error (e)
  40. (log:error e)
  41. (log:info "Backing off for" backoff)
  42. (sleep backoff)
  43. (setf backoff (min *backoff-max*
  44. (* 2 backoff))))
  45. (usocket:timeout-error (e)
  46. (log:error e)
  47. (log:info "Backing off for" backoff)
  48. (sleep backoff)
  49. (setf backoff (min *backoff-max*
  50. (* 2 backoff))))))))
  51. (defun replace-all (string part replacement &key (test #'char=))
  52. "Returns a new string in which all the occurences of the part
  53. is replaced with replacement."
  54. (with-output-to-string (out)
  55. (loop with part-length = (length part)
  56. for old-pos = 0 then (+ pos part-length)
  57. for pos = (search part string
  58. :start2 old-pos
  59. :test test)
  60. do (write-string string out
  61. :start old-pos
  62. :end (or pos (length string)))
  63. when pos do (write-string replacement out)
  64. while pos)))
  65. (defmacro aget (key alist)
  66. `(cdr (assoc ,key ,alist :test #'equal)))
  67. (defun agets (alist &rest keys)
  68. (reduce #'(lambda (a k) (aget k a)) keys :initial-value alist))
  69. (defun mappend (fn &rest lists)
  70. "Apply fn to each element of lists and append the results."
  71. (apply #'append (apply #'mapcar fn lists)))
  72. (defun random-elt (choices)
  73. "Choose an element from a list at random."
  74. (elt choices (random (length choices))))
  75. (defun flatten (the-list)
  76. "Append together elements (or lists) in the list."
  77. (mappend #'(lambda (x) (if (listp x) (flatten x) (list x))) the-list))
  78. (defun preprocess-input (text)
  79. (when text
  80. (let* ((text (subseq text (if (equal (char text 0) #\/) 1 0)))
  81. (first-space (position #\Space text))
  82. (first-word (subseq text 0 first-space)))
  83. (if (equal first-word *bot-name*)
  84. (preprocess-input (subseq text (1+ first-space)))
  85. (replace-all text *bot-name* "ты")))))
  86. (defun punctuation-p (char)
  87. (find char ".,;:'!?#-()\\\""))
  88. (defun read-from-string-no-punct (input)
  89. "Read from an input string, ignoring punctuation."
  90. (let ((*package* (find-package 'chatikbot)))
  91. (read-from-string
  92. (concatenate 'string "(" (substitute-if #\space #'punctuation-p input) ")"))))
  93. (defun print-with-spaces (list)
  94. (format nil "~@(~{~a~^ ~}~)" list))
  95. (defun switch-viewpoint (words)
  96. "Change I to you and vice versa, and so on."
  97. (sublis '((I . you) (you . I) (me . you) (am . are)
  98. (я ты) (ты я) (меня тебя) (тебя меня))
  99. words))
  100. (defun use-eliza-rules (input rules)
  101. "Find some rule with which to transform the input."
  102. (rule-based-translator input rules
  103. :action #'(lambda (bindings responses)
  104. (sublis (switch-viewpoint bindings)
  105. (random-elt responses)))))
  106. (defun eliza (input rules)
  107. (let ((r (use-eliza-rules
  108. (read-from-string-no-punct input)
  109. rules)))
  110. (cond
  111. ((null r) nil)
  112. ((and (consp (car r)) (eq 'function (caar r)))
  113. (apply (cadar r) (cdr r)))
  114. ((keywordp (car r)) r)
  115. (t (print-with-spaces (flatten r))))))
  116. (defun parse-cmd (text)
  117. (let* ((args (split-sequence:split-sequence #\Space (subseq text 1) :remove-empty-subseqs t))
  118. (cmd (subseq (car args) 0 (position #\@ (car args)))))
  119. (values (intern (string-upcase cmd) "KEYWORD") (rest args))))
  120. (defun spaced (list)
  121. (format nil "~{~A~^ ~}" list))
  122. (defun http-default (url &optional parameters)
  123. (let* ((uri (quri:uri url))
  124. (userinfo (quri:uri-userinfo uri)))
  125. (when parameters
  126. (let ((query (quri:url-encode-params parameters :encoding :utf-8)))
  127. (setf (quri:uri-query uri)
  128. (if (and (quri:uri-query uri)
  129. (string-not-equal (quri:uri-query uri) ""))
  130. (concatenate 'string (quri:uri-query uri) "&" query)
  131. query))))
  132. (when userinfo
  133. (setf (quri:uri-userinfo uri) nil))
  134. (unless (quri:uri-scheme uri)
  135. (setf (quri:uri-scheme uri) "http"))
  136. (values uri userinfo)))
  137. (defun http-request (url &rest args &key method version parameters content headers basic-auth cookie-jar keep-alive use-connection-pool (max-redirects 5) timeout force-binary want-stream ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path user-agent)
  138. (declare (ignore method version content basic-auth cookie-jar keep-alive use-connection-pool max-redirects timeout force-binary want-stream ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path))
  139. (multiple-value-bind (uri userinfo)
  140. (http-default url parameters)
  141. (when userinfo
  142. (push (cons :authorization (concatenate 'string "Basic "
  143. (base64:string-to-base64-string userinfo)))
  144. headers))
  145. (when user-agent
  146. (push (cons :user-agent user-agent) headers)
  147. (remf args :user-agent))
  148. (remf args :parameters)
  149. (remf args :headers)
  150. (apply #'dex:request uri :headers headers args)))
  151. ;; XML processing
  152. (defun xml-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 encoding)
  153. (declare (ignore method parameters headers content 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))
  154. (remf args :encoding)
  155. (multiple-value-bind (raw-body status headers uri)
  156. (apply #'http-request url :force-binary t args)
  157. (let ((encoding
  158. (or
  159. ;; 1. Provided encoding
  160. encoding
  161. ;; 2. Content-type header
  162. (ignore-errors
  163. (let ((ct (gethash "content-type" headers)))
  164. (subseq ct (1+ (position #\= ct)))))
  165. ;; 3. Parse first 1000 bytes
  166. (ignore-errors
  167. (let ((dom (plump:parse (flex:octets-to-string
  168. (subseq raw-body 0 (1+ (position (char-code #\>) raw-body :start 1000)))))))
  169. (or
  170. ;; 3.1 Content-type from http-equiv
  171. (ignore-errors
  172. (let ((ct (loop for meta in (get-by-tag dom "meta")
  173. for http-equiv = (plump:get-attribute meta "http-equiv")
  174. for content = (plump:get-attribute meta "content")
  175. when (equal http-equiv "Content-Type")
  176. return content)))
  177. (subseq ct (1+ (position #\= ct)))))
  178. ;; 3.2 'content' xml node attribute
  179. (ignore-errors (plump:get-attribute (plump:first-child dom) "encoding")))))
  180. ;; 4. Default 'utf-8'
  181. "utf-8")))
  182. (values
  183. (handler-bind ((flex:external-format-encoding-error
  184. (lambda (c) (use-value #\? c))))
  185. (plump:parse
  186. (flex:octets-to-string raw-body :external-format (intern encoding 'keyword))))
  187. status headers uri))))
  188. (defun get-by-tag (node tag)
  189. (nreverse (org.shirakumo.plump.dom::get-elements-by-tag-name node tag)))
  190. (defun select-text (selector node)
  191. (ignore-errors
  192. (plump:text (plump:strip
  193. (let ((node (elt (clss:select selector node) 0)))
  194. (plump:traverse node #'(lambda (n) (setf (plump:text n) ""))
  195. :test #'plump:comment-p)
  196. node)))))
  197. ;; JSON processing
  198. (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))
  199. (declare (ignore method parameters 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))
  200. (remf args :object-as)
  201. (when content
  202. (push (cons :content-type "application/json") headers))
  203. (multiple-value-bind (body status headers uri)
  204. (apply #'http-request url args)
  205. (unless (stringp body)
  206. (setf body (babel:octets-to-string body :encoding :utf-8)))
  207. (values (yason:parse body :object-as object-as) status headers uri)))
  208. (defun plist-hash (plist &optional skip-nil (format-key #'identity) &rest hash-table-initargs)
  209. (cond
  210. ((and (consp plist) (keywordp (car plist)))
  211. (let ((table (apply #'make-hash-table hash-table-initargs)))
  212. (do ((tail plist (cddr tail)))
  213. ((not tail))
  214. (let ((key (funcall format-key (car tail)))
  215. (value (cadr tail)))
  216. (when (or value (not skip-nil))
  217. (setf (gethash key table)
  218. (if (listp value)
  219. (apply #'plist-hash value skip-nil format-key hash-table-initargs)
  220. value)))))
  221. table))
  222. ((consp plist)
  223. (loop for item in plist collect (apply #'plist-hash item skip-nil format-key hash-table-initargs)))
  224. (:default plist)))
  225. (defmethod yason:encode ((object (eql 'f)) &optional (stream *standard-output*))
  226. (write-string "false" stream)
  227. object)
  228. (defun plist-json (plist)
  229. (with-output-to-string (stream)
  230. (yason:encode (plist-hash plist t #'dekeyify) stream)))
  231. (defun format-ts (ts)
  232. (local-time:format-timestring nil ts
  233. :format '(:year "-" (:month 2) "-" (:day 2) " "
  234. (:hour 2) ":" (:min 2) ":" (:sec 2))))
  235. (defun parse-float (string)
  236. (let ((*read-eval* nil))
  237. (with-input-from-string (stream string)
  238. (read stream nil nil))))
  239. (defun smart-f (arg &optional digits)
  240. (with-output-to-string (s)
  241. (prin1 (cond ((= (round arg) arg) (round arg))
  242. (digits (float (/ (round (* arg (expt 10 digits)))
  243. (expt 10 digits))))
  244. (t arg))
  245. s)))
  246. (defun format-size (bytes)
  247. (cond
  248. ((< bytes 512) (smart-f bytes))
  249. ((< bytes (* 512 1024)) (format nil "~A KiB" (smart-f (/ bytes 1024) 1)))
  250. ((< bytes (* 512 1024 1024)) (format nil "~A MiB" (smart-f (/ bytes 1024 1024) 1)))
  251. ((< bytes (* 512 1024 1024 1024)) (format nil "~A GiB" (smart-f (/ bytes 1024 1024 1024) 1)))
  252. (:otherwise (format nil "~A TiB" (smart-f (/ bytes 1024 1024 1024 1024) 1)))))
  253. (defun format-interval (seconds)
  254. (cond
  255. ((< seconds 60) (format nil "~A sec" seconds))
  256. ((< seconds (* 60 60)) (format nil "~A mins" (round seconds 60)))
  257. ((< seconds (* 60 60 24)) (format nil "~A hours" (round seconds (* 60 60))))
  258. ((< seconds (* 60 60 24 7)) (format nil "~A days" (round seconds (* 60 60 24))))
  259. ((< seconds (* 60 60 24 7 54)) (format nil "~A weeks" (round seconds (* 60 60 24 7))))
  260. (:otherwise (format nil "~A years" (smart-f (/ seconds (* 60 60 24 365.25)) 1)))))
  261. (defun token-hmac (message &optional (hmac-length 12))
  262. (let ((hmac (crypto:make-hmac (crypto:ascii-string-to-byte-array *telegram-token*) :sha256)))
  263. (crypto:update-hmac hmac (crypto:ascii-string-to-byte-array message))
  264. (base64:usb8-array-to-base64-string
  265. (subseq (crypto:hmac-digest hmac) 0 hmac-length) :uri t)))
  266. (defun encode-callback-data (chat-id section data &optional (ttl 600) (hmac-length 12))
  267. (when (find #\: data)
  268. (error "Bad data."))
  269. (let* ((message (format nil "~A:~A:~A:~A"
  270. (base64:integer-to-base64-string chat-id)
  271. (base64:integer-to-base64-string
  272. (+ ttl (local-time:timestamp-to-universal (local-time:now))))
  273. section data))
  274. (encoded (format nil "~A$~A" message (token-hmac message hmac-length))))
  275. (when (> (length encoded) +telegram-max-callback-data-length+)
  276. (error "Max callback length exceeded"))
  277. encoded))
  278. (defun decode-callback-data (chat-id raw-data &optional (hmac-length 12))
  279. (destructuring-bind (message hmac)
  280. (split-sequence:split-sequence #\$ raw-data :from-end t :count 2)
  281. (destructuring-bind (cid expire section data)
  282. (split-sequence:split-sequence #\: message :count 4)
  283. (unless (= chat-id (base64:base64-string-to-integer cid))
  284. (error "Wrong chat id."))
  285. (unless (>= (base64:base64-string-to-integer expire)
  286. (local-time:timestamp-to-universal (local-time:now)))
  287. (error "Expired."))
  288. (unless (equal hmac (token-hmac message hmac-length))
  289. (error "Bad data."))
  290. (values data (intern (string-upcase section) "KEYWORD")))))
  291. (defmacro def-message-handler (name (message) &body body)
  292. `(progn
  293. (defun ,name (,message)
  294. (let ((message-id (aget "message_id" ,message))
  295. (from-id (aget "id" (aget "from" ,message)))
  296. (chat-id (aget "id" (aget "chat" ,message)))
  297. (text (aget "text" ,message)))
  298. (declare (ignorable message-id from-id chat-id text))
  299. (handler-case (progn ,@body)
  300. (error (e)
  301. (log:error "~A" e)
  302. (bot-send-message chat-id
  303. (format nil "Ошибочка вышла~@[: ~A~]"
  304. (when (member chat-id *admins*) e)))))))
  305. (add-hook :update-message ',name)))
  306. (defmacro def-message-cmd-handler (name (&rest commands) &body body)
  307. `(def-message-handler ,name (message)
  308. (when (and text (equal #\/ (char text 0)))
  309. (multiple-value-bind (cmd args) (parse-cmd text)
  310. (when (member cmd (list ,@commands))
  311. (log:info cmd message-id chat-id from-id args)
  312. ,@body
  313. t)))))
  314. (defmacro def-message-admin-cmd-handler (name (&rest commands) &body body)
  315. `(def-message-handler ,name (message)
  316. (when (and (member chat-id *admins*)
  317. text (equal #\/ (char text 0)))
  318. (multiple-value-bind (cmd args) (parse-cmd text)
  319. (when (member cmd (list ,@commands))
  320. (log:info cmd message-id chat-id from-id args)
  321. ,@body
  322. t)))))
  323. (defmacro def-callback-handler (name (callback) &body body)
  324. `(progn
  325. (defun ,name (,callback)
  326. (let* ((query-id (aget "id" ,callback))
  327. (from (aget "from" ,callback))
  328. (raw-data (aget "data" ,callback))
  329. (message (aget "message" ,callback))
  330. (inline-message-id (aget "inline_message_id" ,callback))
  331. (from-id (aget "id" from))
  332. (chat-id (aget "id" (aget "chat" message)))
  333. (message-id (aget "message_id" message)))
  334. (declare (ignorable query-id from raw-data message inline-message-id from-id chat-id message-id))
  335. (handler-case (progn ,@body)
  336. (error (e)
  337. (log:error "~A" e)
  338. (bot-send-message (or chat-id from-id)
  339. (format nil "Ошибочка вышла~@[: ~A~]"
  340. (when (member chat-id *admins*) e)))))))
  341. (add-hook :update-callback-query ',name)))
  342. (defmacro def-callback-section-handler (name (&rest sections) &body body)
  343. `(def-callback-handler ,name (callback)
  344. (when chat-id
  345. (multiple-value-bind (data section) (decode-callback-data chat-id raw-data)
  346. (when (member section (list ,@sections))
  347. (log:info query-id from-id chat-id message-id section data)
  348. ,@body
  349. t)))))
  350. (defun encode-oauth-state (section state)
  351. (format nil "~A$~A" section state))
  352. (defun decode-oauth-state (raw-state)
  353. (destructuring-bind (section data)
  354. (split-sequence:split-sequence #\$ raw-state :count 2)
  355. (values data (intern (string-upcase section) "KEYWORD"))))
  356. (defmacro def-oauth-handler (name (code error state) &body body)
  357. `(progn
  358. (defun ,name (,code ,error ,state)
  359. (declare (ignorable ,code ,error ,state))
  360. (handler-case (progn ,@body)
  361. (error (e)
  362. (log:error "~A" e)
  363. (hunchentoot:redirect "/error"))))
  364. (add-hook :oauth ',name)))
  365. (defmacro def-oauth-section-handler (name (&rest sections) &body body)
  366. `(def-oauth-handler ,name (code error raw-state)
  367. (multiple-value-bind (state section) (decode-oauth-state raw-state)
  368. (when (member section (list ,@sections))
  369. ,@body
  370. t))))
  371. (defun symbol-append (&rest symbols)
  372. (intern (apply #'concatenate 'string
  373. (mapcar #'symbol-name symbols))))
  374. ;; Schedule
  375. (defmacro defcron (name (&rest schedule) &body body)
  376. (let ((schedule (or schedule '(:minute '* :hour '*)))
  377. (scheduler (symbol-append name '-scheduler)))
  378. `(progn
  379. (defun ,name ()
  380. (unwind-protect
  381. (handler-case (progn ,@body)
  382. (error (e) (log:error e)))
  383. (dex:clear-connection-pool)))
  384. (defun ,scheduler ()
  385. (clon:schedule-function
  386. ',name (clon:make-scheduler
  387. (clon:make-typed-cron-schedule
  388. ,@schedule)
  389. :allow-now-p t)
  390. :name ',name :thread t)
  391. (values))
  392. (add-hook :starting ',scheduler))))
  393. ;; Fix bug in local-time (following symlinks in /usr/share/zoneinfo/
  394. ;; leads to bad cutoff)
  395. (in-package #:local-time)
  396. (defun reread-timezone-repository (&key (timezone-repository *default-timezone-repository-path*))
  397. (check-type timezone-repository (or pathname string))
  398. (multiple-value-bind (valid? error)
  399. (ignore-errors
  400. (truename timezone-repository)
  401. t)
  402. (unless valid?
  403. (error "REREAD-TIMEZONE-REPOSITORY was called with invalid PROJECT-DIRECTORY (~A). The error is ~A."
  404. timezone-repository error)))
  405. (let* ((root-directory timezone-repository)
  406. (cutoff-position (length (princ-to-string root-directory))))
  407. (flet ((visitor (file)
  408. (handler-case
  409. (let* ((full-name (subseq (princ-to-string file) cutoff-position))
  410. (name (pathname-name file))
  411. (timezone (%realize-timezone (make-timezone :path file :name name))))
  412. (setf (gethash full-name *location-name->timezone*) timezone)
  413. (map nil (lambda (subzone)
  414. (push timezone (gethash (subzone-abbrev subzone)
  415. *abbreviated-subzone-name->timezone-list*)))
  416. (timezone-subzones timezone)))
  417. (invalid-timezone-file () nil))))
  418. (setf *location-name->timezone* (make-hash-table :test 'equal))
  419. (setf *abbreviated-subzone-name->timezone-list* (make-hash-table :test 'equal))
  420. (cl-fad:walk-directory root-directory #'visitor :directories nil :follow-symlinks nil
  421. :test (lambda (file)
  422. (not (find "Etc" (pathname-directory file) :test #'string=))))
  423. (cl-fad:walk-directory (merge-pathnames "Etc/" root-directory) #'visitor :directories nil))))
  424. (let ((zonepath "/usr/share/zoneinfo/"))
  425. (when (directory zonepath)
  426. (local-time:reread-timezone-repository :timezone-repository zonepath)))