| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- (in-package :cl-user)
- (defpackage chatikbot.plugins.shopper
- (:use :cl :chatikbot.common))
- (in-package :chatikbot.plugins.shopper)
- (defvar *cookie-jar* (cl-cookie:make-cookie-jar) "Currently active cookies")
- (defparameter +shops+
- `((:crc (:type :bike)
- (:form . (("http://www.chainreactioncycles.com/")
- "#localepickerpage > form"
- (("/atg/userprofiling/ProfileFormHandler.value.country" . "RU")
- ("/atg/userprofiling/ProfileFormHandler.value.currency" . "EUR")
- ("/atg/userprofiling/ProfileFormHandler.value.language" . "en"))))
- (:search (:request . ("http://www.chainreactioncycles.com/ru/en/s" :parameters (("q" . q))))
- (:items . "div.products_details > ul")
- (:info (:link . (".description > a" :href))
- (:title . (".description"))
- (:price . (".fromamt")))))
- (:b24 (:type . :bike)
- (:init . ("https://www.bike24.de/" :method :post
- :content (("country" . "23")
- ("lang" . "2")
- ("action" . "locale_select"))))
- (:search (:request . ("https://www.bike24.de/1.php"
- :parameters (("content" . "13") ("search" . q))))
- (:items . ".box-product-list-item-default")
- (:info (:link . ("a" :href))
- (:title . (".title"))
- (:price . (".box-price")))))
- (:bd (:type . :bike)
- (:init . ("https://www.bike-discount.de/index.php"
- :parameters (("lang" . "en")
- ("delivery_country" . "144")
- ("currency" . "1"))))
- (:search (:request . ("https://www.bike-discount.de/en/search"
- :parameters (("q" . q))))
- (:items . ".element_artikel_gallery")
- (:info (:link . ("a" :href))
- (:title . (".productimage img" "alt"))
- (:price . ("[itemprop='price']" "content")))))
- (:bc (:type . :bike)
- (:search (:request . ("https://www.bike-components.de/en/s/" :parameters (("keywords" . q))))
- (:items . ".site-product-items > .item")
- (:info (:link . ("a" :href))
- (:title . ("h3"))
- (:price . (".price")))))
- (:birota (:type :bike)
- (:search (:request . ("https://www.birota.ru/shop/search/index.php" :parameters (("q" . q))))
- (:items . "[itemtype='http://schema.org/Product']")
- (:info (:link . ("[itemprop='url']" :href))
- (:title . ("[itemprop='name']" "content"))
- (:price . (".price")))))))
- (defun apply-context (template context)
- (sublis context template))
- (defun do-request (template &optional context)
- (let ((args (apply-context template context)))
- (apply 'xml-request `(,@args :cookie-jar ,*cookie-jar*))))
- (defun node-full-url (node attr uri)
- (quri:render-uri (quri:merge-uris (quri:uri (plump:attribute node attr)) uri)))
- (defun do-form (form-info)
- (destructuring-bind (url form-selector context) form-info
- (multiple-value-bind (dom status headers uri)
- (do-request url)
- (declare (ignore status headers))
- (let ((form (clss:select form-selector dom)))
- (unless (zerop (length form))
- (let* ((form (elt form 0))
- (action (node-full-url form "action" uri))
- (method (or (keyify (plump:attribute form "method")) :get))
- (content (append context
- (loop for input across (clss:select "input,select" form)
- for name = (plump:attribute input "name")
- for value = (plump:attribute input "value")
- when name
- unless (agets context name)
- collect (cons name value)))))
- (do-request `(,action :method ,method :content ,content))))))))
- (defun extract-data (node template uri)
- (destructuring-bind (selector &optional post) template
- (let* ((nodes (clss:select selector node))
- (node (and (not (zerop (length nodes)))
- (elt nodes 0))))
- (when node
- (cond
- ((eq :href post) (node-full-url node "href" uri))
- ((stringp post) (plump:attribute node post))
- (t (unspacify (text-with-cdata node))))))))
- (defun parse-dom (uri dom items-selector info-templates &optional limit)
- (let ((nodes (clss:select items-selector dom)))
- (when limit
- (setf nodes (subseq nodes 0 (min limit (length nodes)))))
- (loop for node across nodes
- collect (loop for (key . template) in info-templates
- collect (cons key (extract-data node template uri))))))
- (defun have-cookies (request)
- (let ((uri (quri:uri (car request))))
- (cl-cookie:cookie-jar-host-cookies
- *cookie-jar* (quri:uri-host uri) (or (quri:uri-path uri) "/")
- :securep (string= (quri:uri-scheme uri) "https"))))
- (defun search-shop (query params &optional limit)
- (let ((init (agets params :init))
- (form (agets params :form))
- (search (agets params :search)))
- (unless (have-cookies (agets search :request))
- (when init (do-request init))
- (when form (do-form form)))
- (multiple-value-bind (dom status headers uri)
- (do-request (agets search :request) `((q . ,query)))
- (declare (ignore status headers))
- (parse-dom uri dom (agets search :items) (agets search :info) limit))))
- (defun search-shops (query shops &optional limit)
- (labels ((f (shop)
- (destructuring-bind (name . params) shop
- (ignore-errors
- (cons name (search-shop query params limit))))))
- (pmapcar #'f shops)))
- (defun merge-search-results (search-results)
- (let (merged)
- (dolist (shop search-results (nreverse merged))
- (dolist (res (cdr shop))
- (let* ((title (agets res :title))
- (pair (assoc title merged :test 'equal)))
- (unless pair
- (setf pair (car (push (list title) merged))))
- (push (cons (car shop) res) (cdr pair)))))))
- (defun format-merged (merged)
- (with-output-to-string (s)
- (loop for (title . shops) in merged
- do (format s "~A - ~{*~A*: [~A](~A)~^, ~}~%" title
- (loop for (name . result) in shops
- append (list (dekeyify name)
- (agets result :price)
- (agets result :link)))))))
- (defun handle-search (chat-id query)
- (telegram-send-chat-action chat-id "typing")
- (let ((results (merge-search-results
- (search-shops query +shops+ 3))))
- (bot-send-message chat-id (if results (format-merged results) "Ничего не нашлось")
- :parse-mode "markdown" :disable-web-preview "true")))
- (def-message-cmd-handler handler-cmd-wall (:bike)
- (cond
- ((null args) (bot-send-message chat-id "/bike <query>"))
- (:otherwise (handle-search chat-id (spaced args)))))
|