utils.lisp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  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. (apply #'dex:request uri :headers headers args)))
  150. ;; XML processing
  151. (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)
  152. (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))
  153. (remf args :encoding)
  154. (multiple-value-bind (raw-body status headers uri)
  155. (apply #'http-request url :force-binary t args)
  156. (let ((encoding
  157. (or
  158. ;; 1. Provided encoding
  159. encoding
  160. ;; 2. Content-type header
  161. (ignore-errors
  162. (let ((ct (gethash "content-type" headers)))
  163. (subseq ct (1+ (position #\= ct)))))
  164. ;; 3. Parse first 1000 bytes
  165. (ignore-errors
  166. (let ((dom (plump:parse (flex:octets-to-string
  167. (subseq raw-body 0 (1+ (position (char-code #\>) raw-body :start 1000)))))))
  168. (or
  169. ;; 3.1 Content-type from http-equiv
  170. (ignore-errors
  171. (let ((ct (loop for meta in (get-by-tag dom "meta")
  172. for http-equiv = (plump:get-attribute meta "http-equiv")
  173. for content = (plump:get-attribute meta "content")
  174. when (equal http-equiv "Content-Type")
  175. return content)))
  176. (subseq ct (1+ (position #\= ct)))))
  177. ;; 3.2 'content' xml node attribute
  178. (ignore-errors (plump:get-attribute (plump:first-child dom) "encoding")))))
  179. ;; 4. Default 'utf-8'
  180. "utf-8")))
  181. (values
  182. (handler-bind ((flex:external-format-encoding-error
  183. (lambda (c) (use-value #\? c))))
  184. (plump:parse
  185. (flex:octets-to-string raw-body :external-format (intern encoding 'keyword))))
  186. status headers uri))))
  187. (defun get-by-tag (node tag)
  188. (nreverse (org.shirakumo.plump.dom::get-elements-by-tag-name node tag)))
  189. (defun select-text (selector node)
  190. (ignore-errors
  191. (string-trim '(#\Newline #\Space #\Return) (plump:text (elt (clss:select selector node) 0)))))
  192. ;; JSON processing
  193. (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))
  194. (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))
  195. (remf args :object-as)
  196. (when content
  197. (push (cons :content-type "application/json") headers))
  198. (multiple-value-bind (body status headers uri)
  199. (apply #'http-request url args)
  200. (unless (stringp body)
  201. (setf body (babel:octets-to-string body :encoding :utf-8)))
  202. (values (yason:parse body :object-as object-as) status headers uri)))
  203. (defun plist-hash (plist &optional skip-nil (format-key #'identity) &rest hash-table-initargs)
  204. (cond
  205. ((and (consp plist) (keywordp (car plist)))
  206. (let ((table (apply #'make-hash-table hash-table-initargs)))
  207. (do ((tail plist (cddr tail)))
  208. ((not tail))
  209. (let ((key (funcall format-key (car tail)))
  210. (value (cadr tail)))
  211. (when (or value (not skip-nil))
  212. (setf (gethash key table)
  213. (if (listp value)
  214. (apply #'plist-hash value skip-nil format-key hash-table-initargs)
  215. value)))))
  216. table))
  217. ((consp plist)
  218. (loop for item in plist collect (apply #'plist-hash item skip-nil format-key hash-table-initargs)))
  219. (:default plist)))
  220. (defmethod yason:encode ((object (eql 'f)) &optional (stream *standard-output*))
  221. (write-string "false" stream)
  222. object)
  223. (defun plist-json (plist)
  224. (with-output-to-string (stream)
  225. (yason:encode (plist-hash plist t #'dekeyify) stream)))
  226. (defun format-ts (ts)
  227. (local-time:format-timestring nil ts
  228. :format '(:year "-" (:month 2) "-" (:day 2) " "
  229. (:hour 2) ":" (:min 2) ":" (:sec 2))))
  230. (defun parse-float (string)
  231. (let ((*read-eval* nil))
  232. (with-input-from-string (stream string)
  233. (read stream nil nil))))
  234. (defun smart-f (arg &optional digits)
  235. (with-output-to-string (s)
  236. (prin1 (cond ((= (round arg) arg) (round arg))
  237. (digits (float (/ (round (* arg (expt 10 digits)))
  238. (expt 10 digits))))
  239. (t arg))
  240. s)))
  241. (defun format-size (bytes)
  242. (cond
  243. ((< bytes 512) (smart-f bytes))
  244. ((< bytes (* 512 1024)) (format nil "~A KiB" (smart-f (/ bytes 1024) 1)))
  245. ((< bytes (* 512 1024 1024)) (format nil "~A MiB" (smart-f (/ bytes 1024 1024) 1)))
  246. ((< bytes (* 512 1024 1024 1024)) (format nil "~A GiB" (smart-f (/ bytes 1024 1024 1024) 1)))
  247. (:otherwise (format nil "~A TiB" (smart-f (/ bytes 1024 1024 1024 1024) 1)))))
  248. (defun format-interval (seconds)
  249. (cond
  250. ((< seconds 60) (format nil "~A sec" seconds))
  251. ((< seconds (* 60 60)) (format nil "~A mins" (round seconds 60)))
  252. ((< seconds (* 60 60 24)) (format nil "~A hours" (round seconds (* 60 60))))
  253. ((< seconds (* 60 60 24 7)) (format nil "~A days" (round seconds (* 60 60 24))))
  254. ((< seconds (* 60 60 24 7 54)) (format nil "~A weeks" (round seconds (* 60 60 24 7))))
  255. (:otherwise (format nil "~A years" (smart-f (/ seconds (* 60 60 24 365.25)) 1)))))
  256. (defun token-hmac (message &optional (hmac-length 12))
  257. (let ((hmac (crypto:make-hmac (crypto:ascii-string-to-byte-array *telegram-token*) :sha256)))
  258. (crypto:update-hmac hmac (crypto:ascii-string-to-byte-array message))
  259. (base64:usb8-array-to-base64-string
  260. (subseq (crypto:hmac-digest hmac) 0 hmac-length) :uri t)))
  261. (defun encode-callback-data (chat-id section data &optional (ttl 600) (hmac-length 12))
  262. (when (find #\: data)
  263. (error "Bad data."))
  264. (let* ((message (format nil "~A:~A:~A:~A"
  265. (base64:integer-to-base64-string chat-id)
  266. (base64:integer-to-base64-string
  267. (+ ttl (local-time:timestamp-to-universal (local-time:now))))
  268. section data))
  269. (encoded (format nil "~A$~A" message (token-hmac message hmac-length))))
  270. (when (> (length encoded) +telegram-max-callback-data-length+)
  271. (error "Max callback length exceeded"))
  272. encoded))
  273. (defun decode-callback-data (chat-id raw-data &optional (hmac-length 12))
  274. (destructuring-bind (message hmac)
  275. (split-sequence:split-sequence #\$ raw-data :from-end t :count 2)
  276. (destructuring-bind (cid expire section data)
  277. (split-sequence:split-sequence #\: message :count 4)
  278. (unless (= chat-id (base64:base64-string-to-integer cid))
  279. (error "Wrong chat id."))
  280. (unless (>= (base64:base64-string-to-integer expire)
  281. (local-time:timestamp-to-universal (local-time:now)))
  282. (error "Expired."))
  283. (unless (equal hmac (token-hmac message hmac-length))
  284. (error "Bad data."))
  285. (values data (intern (string-upcase section) "KEYWORD")))))
  286. (defmacro def-message-handler (name (message) &body body)
  287. `(progn
  288. (defun ,name (,message)
  289. (let ((message-id (aget "message_id" ,message))
  290. (from-id (aget "id" (aget "from" ,message)))
  291. (chat-id (aget "id" (aget "chat" ,message)))
  292. (text (aget "text" ,message)))
  293. (declare (ignorable message-id from-id chat-id text))
  294. (handler-case (progn ,@body)
  295. (error (e)
  296. (log:error "~A" e)
  297. (bot-send-message chat-id
  298. (format nil "Ошибочка вышла~@[: ~A~]"
  299. (when (member chat-id *admins*) e)))))))
  300. (add-hook :update-message ',name)))
  301. (defmacro def-message-cmd-handler (name (&rest commands) &body body)
  302. `(def-message-handler ,name (message)
  303. (when (and text (equal #\/ (char text 0)))
  304. (multiple-value-bind (cmd args) (parse-cmd text)
  305. (when (member cmd (list ,@commands))
  306. (log:info cmd message-id chat-id from-id args)
  307. ,@body
  308. t)))))
  309. (defmacro def-message-admin-cmd-handler (name (&rest commands) &body body)
  310. `(def-message-handler ,name (message)
  311. (when (and (member chat-id *admins*)
  312. text (equal #\/ (char text 0)))
  313. (multiple-value-bind (cmd args) (parse-cmd text)
  314. (when (member cmd (list ,@commands))
  315. (log:info cmd message-id chat-id from-id args)
  316. ,@body
  317. t)))))
  318. (defmacro def-callback-handler (name (callback) &body body)
  319. `(progn
  320. (defun ,name (,callback)
  321. (let* ((query-id (aget "id" ,callback))
  322. (from (aget "from" ,callback))
  323. (raw-data (aget "data" ,callback))
  324. (message (aget "message" ,callback))
  325. (inline-message-id (aget "inline_message_id" ,callback))
  326. (from-id (aget "id" from))
  327. (chat-id (aget "id" (aget "chat" message)))
  328. (message-id (aget "message_id" message)))
  329. (declare (ignorable query-id from raw-data message inline-message-id from-id chat-id message-id))
  330. (handler-case (progn ,@body)
  331. (error (e)
  332. (log:error "~A" e)
  333. (bot-send-message (or chat-id from-id)
  334. (format nil "Ошибочка вышла~@[: ~A~]"
  335. (when (member chat-id *admins*) e)))))))
  336. (add-hook :update-callback-query ',name)))
  337. (defmacro def-callback-section-handler (name (&rest sections) &body body)
  338. `(def-callback-handler ,name (callback)
  339. (when chat-id
  340. (multiple-value-bind (data section) (decode-callback-data chat-id raw-data)
  341. (when (member section (list ,@sections))
  342. (log:info query-id from-id chat-id message-id section data)
  343. ,@body
  344. t)))))
  345. (defun encode-oauth-state (section state)
  346. (format nil "~A$~A" section state))
  347. (defun decode-oauth-state (raw-state)
  348. (destructuring-bind (section data)
  349. (split-sequence:split-sequence #\$ raw-state :count 2)
  350. (values data (intern (string-upcase section) "KEYWORD"))))
  351. (defmacro def-oauth-handler (name (code error state) &body body)
  352. `(progn
  353. (defun ,name (,code ,error ,state)
  354. (declare (ignorable ,code ,error ,state))
  355. (handler-case (progn ,@body)
  356. (error (e)
  357. (log:error "~A" e)
  358. (hunchentoot:redirect "/error"))))
  359. (add-hook :oauth ',name)))
  360. (defmacro def-oauth-section-handler (name (&rest sections) &body body)
  361. `(def-oauth-handler ,name (code error raw-state)
  362. (multiple-value-bind (state section) (decode-oauth-state raw-state)
  363. (when (member section (list ,@sections))
  364. ,@body
  365. t))))
  366. (defun symbol-append (&rest symbols)
  367. (intern (apply #'concatenate 'string
  368. (mapcar #'symbol-name symbols))))
  369. ;; Schedule
  370. (defmacro defcron (name (&rest schedule) &body body)
  371. (let ((schedule (or schedule '(:minute '* :hour '*)))
  372. (scheduler (symbol-append name '-scheduler)))
  373. `(progn
  374. (defun ,name ()
  375. (unwind-protect
  376. (handler-case (progn ,@body)
  377. (error (e) (log:error e)))
  378. (dex:clear-connection-pool)))
  379. (defun ,scheduler ()
  380. (clon:schedule-function
  381. ',name (clon:make-scheduler
  382. (clon:make-typed-cron-schedule
  383. ,@schedule)
  384. :allow-now-p t)
  385. :name ',name :thread t)
  386. (values))
  387. (add-hook :starting ',scheduler))))
  388. ;; Fix bug in local-time (following symlinks in /usr/share/zoneinfo/
  389. ;; leads to bad cutoff)
  390. (in-package #:local-time)
  391. (defun reread-timezone-repository (&key (timezone-repository *default-timezone-repository-path*))
  392. (check-type timezone-repository (or pathname string))
  393. (multiple-value-bind (valid? error)
  394. (ignore-errors
  395. (truename timezone-repository)
  396. t)
  397. (unless valid?
  398. (error "REREAD-TIMEZONE-REPOSITORY was called with invalid PROJECT-DIRECTORY (~A). The error is ~A."
  399. timezone-repository error)))
  400. (let* ((root-directory timezone-repository)
  401. (cutoff-position (length (princ-to-string root-directory))))
  402. (flet ((visitor (file)
  403. (handler-case
  404. (let* ((full-name (subseq (princ-to-string file) cutoff-position))
  405. (name (pathname-name file))
  406. (timezone (%realize-timezone (make-timezone :path file :name name))))
  407. (setf (gethash full-name *location-name->timezone*) timezone)
  408. (map nil (lambda (subzone)
  409. (push timezone (gethash (subzone-abbrev subzone)
  410. *abbreviated-subzone-name->timezone-list*)))
  411. (timezone-subzones timezone)))
  412. (invalid-timezone-file () nil))))
  413. (setf *location-name->timezone* (make-hash-table :test 'equal))
  414. (setf *abbreviated-subzone-name->timezone-list* (make-hash-table :test 'equal))
  415. (cl-fad:walk-directory root-directory #'visitor :directories nil :follow-symlinks nil
  416. :test (lambda (file)
  417. (not (find "Etc" (pathname-directory file) :test #'string=))))
  418. (cl-fad:walk-directory (merge-pathnames "Etc/" root-directory) #'visitor :directories nil))))
  419. (let ((zonepath "/usr/share/zoneinfo/"))
  420. (when (directory zonepath)
  421. (local-time:reread-timezone-repository :timezone-repository zonepath)))