1
0

utils.lisp 18 KB

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