utils.lisp 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. (in-package #:chatikbot)
  2. (defvar *backoff-start* 1 "Initial back-off")
  3. (defvar *backoff-max* 64 "Maximum back-off delay")
  4. (defun loop-with-error-backoff (func)
  5. (let ((backoff *backoff-start*))
  6. (loop
  7. do
  8. (handler-case
  9. (progn
  10. (funcall func)
  11. (setf backoff *backoff-start*))
  12. (error (e)
  13. (log:error e)
  14. (log:info "Backing off for" backoff)
  15. (sleep backoff)
  16. (setf backoff (min *backoff-max*
  17. (* 2 backoff))))
  18. (bordeaux-threads:timeout (e)
  19. (log:error e)
  20. (log:info "Backing off for" backoff)
  21. (sleep backoff)
  22. (setf backoff (min *backoff-max*
  23. (* 2 backoff))))))))
  24. (defun replace-all (string part replacement &key (test #'char=))
  25. "Returns a new string in which all the occurences of the part
  26. is replaced with replacement."
  27. (with-output-to-string (out)
  28. (loop with part-length = (length part)
  29. for old-pos = 0 then (+ pos part-length)
  30. for pos = (search part string
  31. :start2 old-pos
  32. :test test)
  33. do (write-string string out
  34. :start old-pos
  35. :end (or pos (length string)))
  36. when pos do (write-string replacement out)
  37. while pos)))
  38. (defmacro aget (key alist)
  39. `(cdr (assoc ,key ,alist :test #'equal)))
  40. (defun mappend (fn &rest lists)
  41. "Apply fn to each element of lists and append the results."
  42. (apply #'append (apply #'mapcar fn lists)))
  43. (defun random-elt (choices)
  44. "Choose an element from a list at random."
  45. (elt choices (random (length choices))))
  46. (defun flatten (the-list)
  47. "Append together elements (or lists) in the list."
  48. (mappend #'(lambda (x) (if (listp x) (flatten x) (list x))) the-list))
  49. ;; Circular lists
  50. (defun make-circular (items)
  51. "Make items list circular"
  52. (setf (cdr (last items)) items))
  53. (defmacro push-circular (obj circ)
  54. "Move circ list and set head to obj"
  55. `(progn
  56. (pop ,circ)
  57. (setf (car ,circ) ,obj)))
  58. (defmacro peek-circular (circ)
  59. "Get head of circular list"
  60. `(car ,circ))
  61. (defmacro pop-circular (circ)
  62. "Get head of circular list"
  63. `(pop ,circ))
  64. (defun flat-circular (circ)
  65. "Flattens circular list"
  66. (do ((cur (cdr circ) (cdr cur))
  67. (head circ)
  68. result)
  69. ((eq head cur)
  70. (nreverse (push (car cur) result)))
  71. (push (car cur) result)))
  72. (defun http-default (url)
  73. (let ((uri (puri:uri url)))
  74. (puri:render-uri
  75. (if (null (puri:uri-scheme uri))
  76. (puri:uri (format nil "http://~A" url))
  77. uri)
  78. nil)))
  79. ;; XML processing
  80. (defun xml-request (url &optional encoding)
  81. (multiple-value-bind (raw-body status headers uri http-stream)
  82. (drakma:http-request (http-default url)
  83. :force-binary t
  84. :decode-content t)
  85. (declare (ignore status http-stream))
  86. (let ((encoding
  87. (or
  88. ;; 1. Provided encoding
  89. encoding
  90. ;; 2. Content-type header
  91. (ignore-errors
  92. (let ((ct (aget :content-type headers)))
  93. (subseq ct (1+ (position #\= ct)))))
  94. ;; 3. Parse first 1000 bytes
  95. (ignore-errors
  96. (let ((dom (plump:parse (flex:octets-to-string (subseq raw-body 0 1000)))))
  97. (or
  98. ;; 3.1 Content-type from http-equiv
  99. (ignore-errors
  100. (let ((ct (loop for meta in (get-by-tag dom "meta")
  101. for http-equiv = (plump:get-attribute meta "http-equiv")
  102. for content = (plump:get-attribute meta "content")
  103. when (equal http-equiv "Content-Type")
  104. return content)))
  105. (subseq ct (1+ (position #\= ct)))))
  106. ;; 3.2 'content' xml node attribute
  107. (ignore-errors (plump:get-attribute (plump:first-child dom) "encoding")))))
  108. ;; 4. Default 'utf-8'
  109. "utf-8")))
  110. (values
  111. (handler-bind ((flex:external-format-encoding-error
  112. (lambda (c) (use-value #\? c))))
  113. (plump:parse
  114. (flex:octets-to-string raw-body :external-format (intern encoding 'keyword))))
  115. uri))))
  116. (defun get-by-tag (node tag)
  117. (nreverse (org.shirakumo.plump.dom::get-elements-by-tag-name node tag)))
  118. (defun select-text (selector node)
  119. (string-trim '(#\Newline #\Space #\Return) (plump:text (elt (clss:select selector node) 0))))
  120. ;; JSON processing
  121. (defun json-request (url &key (method :get) parameters (object-as :alist))
  122. (multiple-value-bind (stream status headers uri http-stream)
  123. (drakma:http-request (http-default url) :method method :parameters parameters
  124. :external-format-out :utf-8
  125. :force-binary t :want-stream t :decode-content t)
  126. (declare (ignore status headers))
  127. (unwind-protect
  128. (progn
  129. (setf (flex:flexi-stream-external-format stream) :utf-8)
  130. (values (yason:parse stream :object-as object-as) uri))
  131. (ignore-errors (close http-stream)))))
  132. (defun format-ts (ts)
  133. (local-time:format-timestring nil ts
  134. :format '(:year "-" (:month 2) "-" (:day 2) " "
  135. (:hour 2) ":" (:min 2) ":" (:sec 2))))
  136. (defun google-tts (text &key (lang "en"))
  137. (let ((path #P"google_tts.mp3"))
  138. (with-open-file (s path :direction :output
  139. :element-type '(unsigned-byte 8)
  140. :if-exists :supersede
  141. :if-does-not-exist :create)
  142. (write-sequence
  143. (drakma:http-request
  144. "http://translate.google.com/translate_tts"
  145. :parameters `(("ie" . "UTF-8")
  146. ("client" . "t")
  147. ("tl" . ,lang)
  148. ("q" . ,text))
  149. :user-agent "stagefright/1.2 (Linux;Android 5.0)"
  150. :additional-headers '((:referer . "http://translate.google.com/"))
  151. :external-format-out :utf-8
  152. :force-binary t)
  153. s)
  154. path)))
  155. (defun say-it (lang words)
  156. (cons :voice
  157. (google-tts (print-with-spaces words) :lang lang)))
  158. (defun yit-info ()
  159. (labels ((get-rows (url)
  160. (rest (get-by-tag (plump:get-element-by-id (xml-request url) "apartmentList") "tr")))
  161. (row-data (row)
  162. (mapcar (lambda (e) (string-trim '(#\Newline #\Space) (plump:text e)))
  163. (get-by-tag row "td")))
  164. (format-data (data)
  165. (format nil "~{~A~^ ~}" (mapcar (lambda (n) (nth n data)) '(1 2 3 4 7 6))))
  166. (get-intresting (rows)
  167. (loop for row in rows
  168. for data = (row-data row)
  169. for rooms = (parse-integer (nth 2 data))
  170. for area = (parse-float:parse-float (replace-all (nth 3 data) "," "."))
  171. when (= rooms 3)
  172. when (< 65 area 75)
  173. collect data))
  174. (format-apts (url)
  175. (let ((apts (get-intresting (get-rows url))))
  176. (format nil "~A~%~{~A~^~%~}~%~A/~A" url (mapcar #'format-data apts)
  177. (length (remove "забронировано" apts :test #'equal :key #'(lambda (r) (nth 7 r)) ))
  178. (length apts)))))
  179. (format nil "~{~A~^~%~%~}"
  180. (mapcar #'format-apts
  181. '("http://www.yitspb.ru/yit_spb/catalog/apartments/novoorlovskiy-korpus-1-1-1"
  182. "http://www.yitspb.ru/yit_spb/catalog/apartments/novoorlovskiy-korpus-1-1-2")))))
  183. ;; Fix bug in local-time (following symlinks in /usr/share/zoneinfo/
  184. ;; leads to bad cutoff)
  185. (in-package #:local-time)
  186. (defun reread-timezone-repository (&key (timezone-repository *default-timezone-repository-path*))
  187. (check-type timezone-repository (or pathname string))
  188. (multiple-value-bind (valid? error)
  189. (ignore-errors
  190. (truename timezone-repository)
  191. t)
  192. (unless valid?
  193. (error "REREAD-TIMEZONE-REPOSITORY was called with invalid PROJECT-DIRECTORY (~A). The error is ~A."
  194. timezone-repository error)))
  195. (let* ((root-directory timezone-repository)
  196. (cutoff-position (length (princ-to-string root-directory))))
  197. (flet ((visitor (file)
  198. (handler-case
  199. (let* ((full-name (subseq (princ-to-string file) cutoff-position))
  200. (name (pathname-name file))
  201. (timezone (%realize-timezone (make-timezone :path file :name name))))
  202. (setf (gethash full-name *location-name->timezone*) timezone)
  203. (map nil (lambda (subzone)
  204. (push timezone (gethash (subzone-abbrev subzone)
  205. *abbreviated-subzone-name->timezone-list*)))
  206. (timezone-subzones timezone)))
  207. (invalid-timezone-file () nil))))
  208. (setf *location-name->timezone* (make-hash-table :test 'equal))
  209. (setf *abbreviated-subzone-name->timezone-list* (make-hash-table :test 'equal))
  210. (cl-fad:walk-directory root-directory #'visitor :directories nil :follow-symlinks nil
  211. :test (lambda (file)
  212. (not (find "Etc" (pathname-directory file) :test #'string=))))
  213. (cl-fad:walk-directory (merge-pathnames "Etc/" root-directory) #'visitor :directories nil))))
  214. (let ((zonepath "/usr/share/zoneinfo/"))
  215. (when (directory zonepath)
  216. (local-time:reread-timezone-repository :timezone-repository zonepath)))