shopper.lisp 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. (in-package :cl-user)
  2. (defpackage chatikbot.plugins.shopper
  3. (:use :cl :chatikbot.common))
  4. (in-package :chatikbot.plugins.shopper)
  5. (defvar *cookie-jar* (cl-cookie:make-cookie-jar) "Currently active cookies")
  6. (defparameter +shops+
  7. `((:crc (:type :bike)
  8. (:form . (("http://www.chainreactioncycles.com/")
  9. "#localepickerpage > form"
  10. (("/atg/userprofiling/ProfileFormHandler.value.country" . "RU")
  11. ("/atg/userprofiling/ProfileFormHandler.value.currency" . "EUR")
  12. ("/atg/userprofiling/ProfileFormHandler.value.language" . "en"))))
  13. (:search (:request . ("http://www.chainreactioncycles.com/ru/en/s" :parameters (("q" . q))))
  14. (:items . "div.products_details > ul")
  15. (:info (:link . (".description > a" :href))
  16. (:title . (".description"))
  17. (:price . (".fromamt")))))
  18. (:b24 (:type . :bike)
  19. (:init . ("https://www.bike24.de/" :method :post
  20. :content (("country" . "23")
  21. ("lang" . "2")
  22. ("action" . "locale_select"))))
  23. (:search (:request . ("https://www.bike24.de/1.php"
  24. :parameters (("content" . "13") ("search" . q))))
  25. (:items . ".box-product-list-item-default")
  26. (:info (:link . ("a" :href))
  27. (:title . (".title"))
  28. (:price . (".box-price")))))
  29. (:bd (:type . :bike)
  30. (:init . ("https://www.bike-discount.de/index.php"
  31. :parameters (("lang" . "en")
  32. ("delivery_country" . "144")
  33. ("currency" . "1"))))
  34. (:search (:request . ("https://www.bike-discount.de/en/search"
  35. :parameters (("q" . q))))
  36. (:items . ".element_artikel_gallery")
  37. (:info (:link . ("a" :href))
  38. (:title . (".productimage img" "alt"))
  39. (:price . ("[itemprop='price']" "content")))))
  40. (:bc (:type . :bike)
  41. (:search (:request . ("https://www.bike-components.de/en/s/" :parameters (("keywords" . q))))
  42. (:items . ".site-product-items > .item")
  43. (:info (:link . ("a" :href))
  44. (:title . ("h3"))
  45. (:price . (".price")))))
  46. (:birota (:type :bike)
  47. (:search (:request . ("https://www.birota.ru/shop/search/index.php" :parameters (("q" . q))))
  48. (:items . "[itemtype='http://schema.org/Product']")
  49. (:info (:link . ("[itemprop='url']" :href))
  50. (:title . ("[itemprop='name']" "content"))
  51. (:price . (".price")))))))
  52. (defun apply-context (template context)
  53. (sublis context template))
  54. (defun do-request (template &optional context)
  55. (let ((args (apply-context template context)))
  56. (apply 'xml-request `(,@args :cookie-jar ,*cookie-jar*))))
  57. (defun node-full-url (node attr uri)
  58. (quri:render-uri (quri:merge-uris (quri:uri (plump:attribute node attr)) uri)))
  59. (defun do-form (form-info)
  60. (destructuring-bind (url form-selector context) form-info
  61. (multiple-value-bind (dom status headers uri)
  62. (do-request url)
  63. (declare (ignore status headers))
  64. (let ((form (clss:select form-selector dom)))
  65. (unless (zerop (length form))
  66. (let* ((form (elt form 0))
  67. (action (node-full-url form "action" uri))
  68. (method (or (keyify (plump:attribute form "method")) :get))
  69. (content (append context
  70. (loop for input across (clss:select "input,select" form)
  71. for name = (plump:attribute input "name")
  72. for value = (plump:attribute input "value")
  73. when name
  74. unless (agets context name)
  75. collect (cons name value)))))
  76. (do-request `(,action :method ,method :content ,content))))))))
  77. (defun extract-data (node template uri)
  78. (destructuring-bind (selector &optional post) template
  79. (let* ((nodes (clss:select selector node))
  80. (node (and (not (zerop (length nodes)))
  81. (elt nodes 0))))
  82. (when node
  83. (cond
  84. ((eq :href post) (node-full-url node "href" uri))
  85. ((stringp post) (plump:attribute node post))
  86. (t (unspacify (text-with-cdata node))))))))
  87. (defun parse-dom (uri dom items-selector info-templates &optional limit)
  88. (let ((nodes (clss:select items-selector dom)))
  89. (when limit
  90. (setf nodes (subseq nodes 0 (min limit (length nodes)))))
  91. (loop for node across nodes
  92. collect (loop for (key . template) in info-templates
  93. collect (cons key (extract-data node template uri))))))
  94. (defun have-cookies (request)
  95. (let ((uri (quri:uri (car request))))
  96. (cl-cookie:cookie-jar-host-cookies
  97. *cookie-jar* (quri:uri-host uri) (or (quri:uri-path uri) "/")
  98. :securep (string= (quri:uri-scheme uri) "https"))))
  99. (defun search-shop (query params &optional limit)
  100. (let ((init (agets params :init))
  101. (form (agets params :form))
  102. (search (agets params :search)))
  103. (unless (have-cookies (agets search :request))
  104. (when init (do-request init))
  105. (when form (do-form form)))
  106. (multiple-value-bind (dom status headers uri)
  107. (do-request (agets search :request) `((q . ,query)))
  108. (declare (ignore status headers))
  109. (parse-dom uri dom (agets search :items) (agets search :info) limit))))
  110. (defun search-shops (query shops &optional limit)
  111. (labels ((f (shop)
  112. (destructuring-bind (name . params) shop
  113. (ignore-errors
  114. (cons name (search-shop query params limit))))))
  115. (pmapcar #'f shops)))
  116. (defun merge-search-results (search-results)
  117. (let (merged)
  118. (dolist (shop search-results (nreverse merged))
  119. (dolist (res (cdr shop))
  120. (let* ((title (agets res :title))
  121. (pair (assoc title merged :test 'equal)))
  122. (unless pair
  123. (setf pair (car (push (list title) merged))))
  124. (push (cons (car shop) res) (cdr pair)))))))
  125. (defun format-merged (merged)
  126. (with-output-to-string (s)
  127. (loop for (title . shops) in merged
  128. do (format s "~A - ~{*~A*: [~A](~A)~^, ~}~%" title
  129. (loop for (name . result) in shops
  130. append (list (dekeyify name)
  131. (agets result :price)
  132. (agets result :link)))))))
  133. (defun handle-search (chat-id query)
  134. (let ((results (merge-search-results
  135. (search-shops query +shops+ 3))))
  136. (bot-send-message chat-id (if results (format-merged results) "Ничего не нашлось")
  137. :parse-mode "markdown" :disable-web-preview "true")))
  138. (def-message-cmd-handler handler-cmd-wall (:bike)
  139. (cond
  140. ((null args) (bot-send-message chat-id "/bike <query>"))
  141. (:otherwise (handle-search chat-id (spaced args)))))