utils.lisp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. (in-package #:chatikbot)
  2. (defvar *hooks* (make-hash-table) "Hooks storage")
  3. (defun run-hooks (event &rest arguments)
  4. (let ((hooks (gethash event *hooks*)))
  5. (labels ((try-handle (func)
  6. (apply func arguments)))
  7. (unless (some #'try-handle hooks)
  8. (log:info "unhandled" event arguments)))))
  9. (defun add-hook (event hook &optional append)
  10. (let ((existing (gethash event *hooks*)))
  11. (unless (member hook existing)
  12. (setf (gethash event *hooks*)
  13. (if append (append existing (list hook))
  14. (cons hook existing))))))
  15. (defun remove-hook (event hook)
  16. (setf (gethash event *hooks*)
  17. (remove hook (gethash event *hooks*))))
  18. (defun string-to-event (key)
  19. (intern (string-upcase (substitute #\- #\_ key)) :keyword))
  20. (defmacro def-message-handler (name (message) &body body)
  21. `(progn
  22. (defun ,name (,message)
  23. (let ((message-id (aget "message_id" ,message))
  24. (from-id (aget "id" (aget "from" ,message)))
  25. (chat-id (aget "id" (aget "chat" ,message)))
  26. (text (aget "text" ,message)))
  27. (declare (ignorable message-id from-id chat-id text))
  28. (handler-case (progn ,@body)
  29. (error (e)
  30. (log:error "~A" e)
  31. (bot-send-message chat-id
  32. (format nil "Ошибочка вышла~@[: ~A~]"
  33. (when (member chat-id *admins*) e)))))))
  34. (add-hook :update-message ',name)))
  35. (defmacro def-message-cmd-handler (name (&rest commands) &body body)
  36. `(def-message-handler ,name (message)
  37. (when (and text (equal #\/ (char text 0)))
  38. (multiple-value-bind (cmd args) (parse-cmd text)
  39. (when (member cmd (list ,@commands))
  40. (log:info cmd message-id chat-id from-id args)
  41. ,@body
  42. t)))))
  43. (defmacro def-message-admin-cmd-handler (name (&rest commands) &body body)
  44. `(def-message-handler ,name (message)
  45. (when (and (member chat-id *admins*)
  46. text (equal #\/ (char text 0)))
  47. (multiple-value-bind (cmd args) (parse-cmd text)
  48. (when (member cmd (list ,@commands))
  49. (log:info cmd message-id chat-id from-id args)
  50. ,@body
  51. t)))))
  52. ;; Schedule
  53. (defmacro defcron (name (&rest schedule) &body body)
  54. (let ((schedule (or schedule '(:minute '* :hour '*))))
  55. `(progn
  56. (defun ,name ()
  57. (handler-case (progn ,@body)
  58. (error (e) (log:error e))))
  59. (add-hook :starting #'(lambda ()
  60. (clon:schedule-function
  61. ',name (clon:make-scheduler
  62. (clon:make-typed-cron-schedule
  63. ,@schedule)
  64. :allow-now-p t)
  65. :name ',name :thread t)
  66. (values))))))
  67. ;; Settings
  68. (defvar *settings* nil "List of plugin's settings symbols")
  69. (defmacro defsetting (var &optional val doc)
  70. `(progn (defvar ,var ,val ,doc)
  71. (push ',var *settings*)))
  72. (defun load-settings ()
  73. (loop for (var val) in (db-select "select var, val from settings")
  74. do (setf (symbol-value (intern var))
  75. (handler-case (read-from-string val)
  76. (error (e) (log:error e))))))
  77. (defun set-setting (symbol value)
  78. (handler-case
  79. (progn
  80. (db-execute "replace into settings (var, val) values (?, ?)"
  81. (symbol-name symbol)
  82. (write-to-string value))
  83. (setf (symbol-value symbol) value))
  84. (error (e) (log:error e))))
  85. (def-message-admin-cmd-handler handle-admin-settings (:settings)
  86. (send-response chat-id
  87. (format nil "~{~{~A~@[ (~A)~]: ~A~}~^~%~}"
  88. (loop for symbol in *settings*
  89. collect (list symbol (documentation symbol 'variable) (symbol-value symbol))))))
  90. (defvar *backoff-start* 1 "Initial back-off")
  91. (defvar *backoff-max* 64 "Maximum back-off delay")
  92. (defun loop-with-error-backoff (func)
  93. (let ((backoff *backoff-start*))
  94. (loop
  95. do
  96. (handler-case
  97. (progn
  98. (funcall func)
  99. (setf backoff *backoff-start*))
  100. (error (e)
  101. (log:error e)
  102. (log:info "Backing off for" backoff)
  103. (sleep backoff)
  104. (setf backoff (min *backoff-max*
  105. (* 2 backoff))))
  106. (bordeaux-threads:timeout (e)
  107. (log:error e)
  108. (log:info "Backing off for" backoff)
  109. (sleep backoff)
  110. (setf backoff (min *backoff-max*
  111. (* 2 backoff))))))))
  112. (defun replace-all (string part replacement &key (test #'char=))
  113. "Returns a new string in which all the occurences of the part
  114. is replaced with replacement."
  115. (with-output-to-string (out)
  116. (loop with part-length = (length part)
  117. for old-pos = 0 then (+ pos part-length)
  118. for pos = (search part string
  119. :start2 old-pos
  120. :test test)
  121. do (write-string string out
  122. :start old-pos
  123. :end (or pos (length string)))
  124. when pos do (write-string replacement out)
  125. while pos)))
  126. (defmacro aget (key alist)
  127. `(cdr (assoc ,key ,alist :test #'equal)))
  128. (defun mappend (fn &rest lists)
  129. "Apply fn to each element of lists and append the results."
  130. (apply #'append (apply #'mapcar fn lists)))
  131. (defun random-elt (choices)
  132. "Choose an element from a list at random."
  133. (elt choices (random (length choices))))
  134. (defun flatten (the-list)
  135. "Append together elements (or lists) in the list."
  136. (mappend #'(lambda (x) (if (listp x) (flatten x) (list x))) the-list))
  137. ;; Circular lists
  138. (defun make-circular (items)
  139. "Make items list circular"
  140. (setf (cdr (last items)) items))
  141. (defmacro push-circular (obj circ)
  142. "Move circ list and set head to obj"
  143. `(progn
  144. (pop ,circ)
  145. (setf (car ,circ) ,obj)))
  146. (defmacro peek-circular (circ)
  147. "Get head of circular list"
  148. `(car ,circ))
  149. (defmacro pop-circular (circ)
  150. "Get head of circular list"
  151. `(pop ,circ))
  152. (defun flat-circular (circ)
  153. "Flattens circular list"
  154. (do ((cur (cdr circ) (cdr cur))
  155. (head circ)
  156. result)
  157. ((eq head cur)
  158. (nreverse (push (car cur) result)))
  159. (push (car cur) result)))
  160. (defun preprocess-input (text)
  161. (when text
  162. (let ((first-word (subseq text 0 (position #\Space text))))
  163. (if (equal first-word "@chatikbot")
  164. (preprocess-input (subseq text 11))
  165. (replace-all text "@chatikbot" "ты")))))
  166. (defun parse-cmd (text)
  167. (let* ((args (split-sequence:split-sequence #\Space (subseq text 1) :remove-empty-subseqs t))
  168. (cmd (subseq (car args) 0 (position #\@ (car args)))))
  169. (values (intern (string-upcase cmd) "KEYWORD") (rest args))))
  170. (defun http-default (url)
  171. (let ((uri (puri:uri url)))
  172. (puri:render-uri
  173. (if (null (puri:uri-scheme uri))
  174. (puri:uri (format nil "http://~A" url))
  175. uri)
  176. nil)))
  177. ;; XML processing
  178. (defun xml-request (url &key encoding parameters)
  179. (multiple-value-bind (raw-body status headers uri http-stream)
  180. (drakma:http-request (http-default url)
  181. :parameters parameters
  182. :external-format-out :utf-8
  183. :force-binary t
  184. :decode-content t)
  185. (declare (ignore status http-stream))
  186. (let ((encoding
  187. (or
  188. ;; 1. Provided encoding
  189. encoding
  190. ;; 2. Content-type header
  191. (ignore-errors
  192. (let ((ct (aget :content-type headers)))
  193. (subseq ct (1+ (position #\= ct)))))
  194. ;; 3. Parse first 1000 bytes
  195. (ignore-errors
  196. (let ((dom (plump:parse (flex:octets-to-string (subseq raw-body 0 1000)))))
  197. (or
  198. ;; 3.1 Content-type from http-equiv
  199. (ignore-errors
  200. (let ((ct (loop for meta in (get-by-tag dom "meta")
  201. for http-equiv = (plump:get-attribute meta "http-equiv")
  202. for content = (plump:get-attribute meta "content")
  203. when (equal http-equiv "Content-Type")
  204. return content)))
  205. (subseq ct (1+ (position #\= ct)))))
  206. ;; 3.2 'content' xml node attribute
  207. (ignore-errors (plump:get-attribute (plump:first-child dom) "encoding")))))
  208. ;; 4. Default 'utf-8'
  209. "utf-8")))
  210. (values
  211. (handler-bind ((flex:external-format-encoding-error
  212. (lambda (c) (use-value #\? c))))
  213. (plump:parse
  214. (flex:octets-to-string raw-body :external-format (intern encoding 'keyword))))
  215. uri))))
  216. (defun get-by-tag (node tag)
  217. (nreverse (org.shirakumo.plump.dom::get-elements-by-tag-name node tag)))
  218. (defun select-text (selector node)
  219. (ignore-errors
  220. (string-trim '(#\Newline #\Space #\Return) (plump:text (elt (clss:select selector node) 0)))))
  221. ;; JSON processing
  222. (defun json-request (url &key (method :get) parameters (object-as :alist))
  223. (multiple-value-bind (stream status headers uri http-stream)
  224. (drakma:http-request (http-default url) :method method :parameters parameters
  225. :external-format-out :utf-8
  226. :force-binary t :want-stream t :decode-content t)
  227. (declare (ignore status headers))
  228. (unwind-protect
  229. (progn
  230. (setf (flex:flexi-stream-external-format stream) :utf-8)
  231. (values (yason:parse stream :object-as object-as) uri))
  232. (ignore-errors (close http-stream)))))
  233. (defun format-ts (ts)
  234. (local-time:format-timestring nil ts
  235. :format '(:year "-" (:month 2) "-" (:day 2) " "
  236. (:hour 2) ":" (:min 2) ":" (:sec 2))))
  237. (defun google-tts (text &key (lang "en"))
  238. (let ((path #P"google_tts.mp3"))
  239. (with-open-file (s path :direction :output
  240. :element-type '(unsigned-byte 8)
  241. :if-exists :supersede
  242. :if-does-not-exist :create)
  243. (write-sequence
  244. (drakma:http-request
  245. "http://translate.google.com/translate_tts"
  246. :parameters `(("ie" . "UTF-8")
  247. ("client" . "t")
  248. ("tl" . ,lang)
  249. ("q" . ,text))
  250. :user-agent "stagefright/1.2 (Linux;Android 5.0)"
  251. :additional-headers '((:referer . "http://translate.google.com/"))
  252. :external-format-out :utf-8
  253. :force-binary t)
  254. s)
  255. path)))
  256. (defun say-it (lang words)
  257. (cons :voice
  258. (google-tts (print-with-spaces words) :lang lang)))
  259. (defun yit-info ()
  260. (labels ((get-rows (url)
  261. (rest (get-by-tag (plump:get-element-by-id (xml-request url) "apartmentList") "tr")))
  262. (row-data (row)
  263. (mapcar (lambda (e) (string-trim '(#\Newline #\Space) (plump:text e)))
  264. (get-by-tag row "td")))
  265. (format-data (data)
  266. (format nil "~{~A~^ ~}" (mapcar (lambda (n) (nth n data)) '(1 2 3 4 7 6))))
  267. (get-intresting (rows)
  268. (loop for row in rows
  269. for data = (row-data row)
  270. for rooms = (parse-integer (nth 2 data))
  271. for area = (parse-float:parse-float (replace-all (nth 3 data) "," "."))
  272. when (= rooms 3)
  273. when (< 65 area 75)
  274. collect data))
  275. (format-apts (url)
  276. (let ((apts (get-intresting (get-rows url))))
  277. (format nil "~A~%~{~A~^~%~}~%~A/~A" url (mapcar #'format-data apts)
  278. (length (remove "забронировано" apts :test #'equal :key #'(lambda (r) (nth 7 r)) ))
  279. (length apts)))))
  280. (format nil "~{~A~^~%~%~}"
  281. (mapcar #'format-apts
  282. '("http://www.yitspb.ru/yit_spb/catalog/apartments/novoorlovskiy-korpus-1-1-1"
  283. "http://www.yitspb.ru/yit_spb/catalog/apartments/novoorlovskiy-korpus-1-1-2")))))
  284. ;; Fix bug in local-time (following symlinks in /usr/share/zoneinfo/
  285. ;; leads to bad cutoff)
  286. (in-package #:local-time)
  287. (defun reread-timezone-repository (&key (timezone-repository *default-timezone-repository-path*))
  288. (check-type timezone-repository (or pathname string))
  289. (multiple-value-bind (valid? error)
  290. (ignore-errors
  291. (truename timezone-repository)
  292. t)
  293. (unless valid?
  294. (error "REREAD-TIMEZONE-REPOSITORY was called with invalid PROJECT-DIRECTORY (~A). The error is ~A."
  295. timezone-repository error)))
  296. (let* ((root-directory timezone-repository)
  297. (cutoff-position (length (princ-to-string root-directory))))
  298. (flet ((visitor (file)
  299. (handler-case
  300. (let* ((full-name (subseq (princ-to-string file) cutoff-position))
  301. (name (pathname-name file))
  302. (timezone (%realize-timezone (make-timezone :path file :name name))))
  303. (setf (gethash full-name *location-name->timezone*) timezone)
  304. (map nil (lambda (subzone)
  305. (push timezone (gethash (subzone-abbrev subzone)
  306. *abbreviated-subzone-name->timezone-list*)))
  307. (timezone-subzones timezone)))
  308. (invalid-timezone-file () nil))))
  309. (setf *location-name->timezone* (make-hash-table :test 'equal))
  310. (setf *abbreviated-subzone-name->timezone-list* (make-hash-table :test 'equal))
  311. (cl-fad:walk-directory root-directory #'visitor :directories nil :follow-symlinks nil
  312. :test (lambda (file)
  313. (not (find "Etc" (pathname-directory file) :test #'string=))))
  314. (cl-fad:walk-directory (merge-pathnames "Etc/" root-directory) #'visitor :directories nil))))
  315. (let ((zonepath "/usr/share/zoneinfo/"))
  316. (when (directory zonepath)
  317. (local-time:reread-timezone-repository :timezone-repository zonepath)))