utils.lisp 17 KB

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