(import-macros {: map : reduce : filter} :lib.macro) (tset package :path (.. package.path ";./vendor/lpeglj/?.lua")) (local math (require :math)) (local fennel (require :vendor.fennel)) (local html (require :vendor.html)) (local libhtml (require :lib.html)) (local array (require :lib.array)) (local str (require :lib.string)) (local texts (require :texts)) (local fs (require :lib.fs)) (local spellfix (require :lib.spellfix)) (local cache (require :lib.cache)) (local {: must} (require :lib.utils)) (when _G.unpack (tset table :unpack _G.unpack)) (set _G.reload (fn [module] (local old (require module)) (tset package :loaded module nil) (local (ok? new) (pcall require module)) (if (not ok?) (do (tset package :loaded module old) (error new)) (when (= (type new) :table) (do (each [k v (pairs new)] (tset old k v)) (each [k (pairs old)] (when (not (. new k)) (tset old k nil))) (tset package :loaded module old)))))) (local db (must (luna.db.open "file:var/db.sqlite?_journal=WAL&_sync=NORMAL&_txlock=immediate"))) (local query-synonyms { "шэн" "шен" "шен" "шэн" "дянь" "дань" "дань" "дянь" "чжень" "чжэнь" "чжэнь" "чжень" "хун" "цун" "цун" "хун" "доска" "чабань" "лунцзин" "лун цзин" "лун цзин" "лунцзин" "билочунь" "би ло чунь" "би ло чунь" "билочунь" "чахай" "сливник" "сливник" "чахай" "чабань" "доска" "термос" "бутылка" "бутылка" "термос"}) (fn sanitize-input [input] (if input (str.trim (input:gsub "[=()<>']" "") " ") nil)) (fn query-string [query key] (if (and query (. query key) (. query key 1) (< 0 (# (. query key 1)))) (sanitize-input (. query key 1)) nil)) (fn query-number [query key] (if (and query (. query key) (. query key 1) (< 0 (# (. query key 1)))) (tonumber (sanitize-input (. query key 1))) nil)) (fn serialize-query [query] (local flattened-object (collect [k v (pairs query)] (values k (array.join (map (fn [_ vv] (.. k "=" vv)) v) "&")))) (array.join (array.list flattened-object) "&")) (fn collect-form [params] {:query (or (query-string params "query") "") :page (or (query-number params "page") 1) :tags (map #(sanitize-input $2) (filter #(~= "" $2) (or params.tags []))) :site (or (query-string params "site") "") :sort (or (query-string params "sort") "") :min-price (query-number params "min-price") :max-price (query-number params "max-price") :min-volume (query-number params "min-volume") :max-volume (query-number params "max-volume") :price-per (= "on" (query-string params "price-per"))}) (fn form-empty? [form] (and (= "" form.query) (= (# form.tags) 0) (= "" form.site) (not form.min-price) (not form.max-price) (not form.min-volume) (not form.max-volume) ;; price-per and sort are intentionally left out since they must not ;; trigger search by itself )) (fn form->path [page form] (.. "?page=" (tostring page) (if (not (str.empty? form.query)) (.. "&query=" form.query) "") (if (< 0 (# form.tags)) (.. "&" (array.join (array.flatten (map (fn [_ tag] (.. "tags=" tag)) form.tags)) "&")) "") (if (not (str.empty? form.site)) (.. "&site=" form.site) "") (if (and form.min-price (< 0 form.min-price)) (.. "&min-price=" (tostring form.min-price)) "") (if (and form.max-price (< 0 form.max-price)) (.. "&max-price=" (tostring form.max-price)) "") (if form.price-per "&price-per=on" "") (if (and form.min-volume (< 0 form.min-volume)) (.. "&min-volume=" (tostring form.min-volume)) "") (if (and form.max-volume (< 0 form.max-volume)) (.. "&max-volume=" (tostring form.max-volume)) "") (if form.sort (.. "&sort=" form.sort) ""))) (fn teas-of-the-day [limit] (assert (< 0 limit) "limit must be > 0") (local the-number (* (tonumber (os.date "%Y%m%d")) limit)) (local total (. (must (luna.db.query db "SELECT count(ROWID) FROM ( SELECT products.ROWID FROM products INNER JOIN product_tags ON product_tags.product = products.url WHERE archived = false AND product_tags.tag IN ('Красный чай', 'Улун', 'Шен пуэр', 'Шу пуэр', 'Зеленый чай', 'Белый чай', 'Желтый чай') GROUP BY products.url)" [])) 1 1)) (must (luna.db.query-assoc db "SELECT products.site, products.title, products.description, products.image, products.url, products.price, products.weight, products.price_per AS \"price-per\" FROM products INNER JOIN product_tags ON product_tags.product = products.url WHERE products.archived = false AND product_tags.tag IN ('Красный чай', 'Улун', 'Шен пуэр', 'Шу пуэр', 'Зеленый чай', 'Белый чай', 'Желтый чай') GROUP BY products.url ORDER BY ROW_NUMBER() over (PARTITION BY site ORDER BY products.ROWID) LIMIT ? OFFSET ?" [limit (% the-number (- total limit))]))) (fn now [] (os.date "%Y-%m-%d %H:%M:%S")) (fn store-metric [headers url] (local fingerprint (must (luna.crypto.sha1 (.. (or headers.User-Agent "") ":" (or headers.X-Forwarded-For headers.Forwarded headers.X-Real-Ip ""))))) (must (luna.db.exec db "INSERT INTO metrics(fingerprint, url, creation_time) VALUES (?, ?, ?)" [fingerprint url (now)]))) (fn tracked-url [url] (.. "/track?url=" (must (luna.http.encode-url url)))) (fn all-tags [] (map (fn [_ v] (. v 1)) (must (luna.db.query db "SELECT DISTINCT tags.title FROM tags INNER JOIN product_tags ON product_tags.tag = tags.title ORDER BY creation_time" [])))) (fn shops-count [] (# (must (luna.db.query db "SELECT DISTINCT site FROM products WHERE archived = false" [])))) (fn products-count [] (. (must (luna.db.query db "SELECT DISTINCT COUNT(url) FROM products WHERE archived = false" [])) 1 1)) (fn latest-update-date [] (. (must (luna.db.query db "SELECT creation_time FROM products ORDER BY creation_TIME DESC LIMIT 1" [])) 1 1)) (fn query-products [{: query : tags : min-price : max-price : price-per : min-volume : max-volume : site : sort} page] (local tags (or tags [])) (local where-conds []) (local sort-criteria []) (local query-args []) ;; create WHERE clause from dynamic form parameters (when (< 0 (# tags)) (each [_ tag (pairs tags)] (table.insert where-conds "product_tags.tag = ?") (table.insert query-args tag))) (when (and query (< 0 (# query))) (table.insert where-conds "search.title MATCH ?") (table.insert query-args (array.join (map (fn [_ q] (local lower-q (must (luna.utf8.lower q))) (if (. query-synonyms lower-q) (.. "(" q "* OR " (. query-synonyms lower-q) "*)") (.. q "*"))) (str.split query " ")) " AND "))) (when (not (str.empty? site)) (table.insert where-conds "products.site = ?") (table.insert query-args site)) (when (and min-price (< 0 min-price)) (if price-per (table.insert where-conds "products.price_per >= ?") (table.insert where-conds "products.price >= ?")) (table.insert query-args min-price)) (when (and max-price (< 0 max-price)) (if price-per (table.insert where-conds "products.price_per <= ?") (table.insert where-conds "products.price <= ?")) (table.insert query-args max-price)) (when (and min-volume (< 0 min-volume)) (table.insert where-conds "products.volume >= ?") (table.insert query-args min-volume)) (when (and max-volume (< 0 max-volume)) (table.insert where-conds "(products.volume <= ? AND products.volume != 0)") (table.insert query-args max-volume)) (local where-sql (if (< 0 (# where-conds)) (.. "AND " (array.join where-conds "\nAND ")) "")) ;; create SORT clause from dynamic form parameters (when sort (if (= sort "cheap-first") (table.insert sort-criteria ;; multiple price by 100 if we only know a price for 1 gram "CASE WHEN products.weight = 1 THEN products.price * 100 ELSE products.price END ASC") (= sort "expensive-first") (table.insert sort-criteria ;; multiple price by 100 if we only know a price for 1 gram "CASE WHEN products.weight = 1 THEN products.price * 100 ELSE products.price END DESC"))) (local sort-sql (if (< 0 (# sort-criteria)) (.. (array.join sort-criteria ",\n") ",") "")) (local total (must (luna.db.query db (string.format "SELECT count(ROWID) FROM ( SELECT products.ROWID FROM search INNER JOIN products ON search.fid = products.url LEFT JOIN product_tags ON product_tags.product = search.fid WHERE search.`table` = 'products' AND products.archived = false %s GROUP BY products.url)" where-sql) query-args))) {:results (must (luna.db.query-assoc db (string.format "SELECT products.title, products.site, products.description, products.image, products.url, products.price, products.weight, products.price_per AS \"price-per\" FROM search INNER JOIN products ON search.fid = products.url LEFT JOIN product_tags ON product_tags.product = products.url WHERE search.`table` = 'products' AND products.archived = false %s GROUP BY products.url ORDER BY %s ROW_NUMBER() over (PARTITION BY products.site ORDER BY products.ROWID), rank LIMIT 24 OFFSET ?" where-sql sort-sql) (array.concat query-args [(* (- page 1) 24)]))) :total (if (< 0 (# total)) (. total 1 1) 0)}) (fn site-name-template [name] (local module (require (.. "parser." name))) [:a {:class "site-icon" :href (tracked-url (.. module.url "?from=everytea.ru"))} [:img {:src (.. "/static/" name ".png") :alt (.. "Логотип " module.title)}] module.title]) (fn item-template [product] (local link (tracked-url (.. product.url "?from=everytea.ru"))) [:div {:class "tile"} [:a {:href link :class "img-link" :rel "nofollow"} [:img {:class "img" :src product.image :title product.title :alt product.title} ""]] (site-name-template product.site) [:a {:href link :style "text-decoration: none;" :rel "nofollow"} [:NO-ESCAPE (.. "