utils.lisp 18 KB

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