nalunch.lisp 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. (in-package #:chatikbot)
  2. (defvar *nalunch/calend* nil "Working calendar exceptions")
  3. (defparameter +nalunch/mobile-ua+ "Mozilla/5.0 (Linux; Android 4.4.4; Nexus 5 Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.114 Mobile Safari/537.36"
  4. "Mobile UA")
  5. (defparameter +nalunch/mobile-uri+ "https://www.nalunch.ru/Mobile")
  6. (defparameter +nalunch/login-uri+ "https://www.nalunch.ru/Mobile/Account/Login")
  7. (defparameter +nalunch/basicdata-calend+ "http://basicdata.ru/api/json/calend/")
  8. (defun nalunch/auth (login pass cookies &optional body)
  9. (let* ((body (or body
  10. (drakma:http-request +nalunch/login-uri+ :cookie-jar cookies :user-agent +nalunch/mobile-ua+)))
  11. (dom (plump:parse body))
  12. (form (plump:get-element-by-id dom "LoginForm"))
  13. (parameters
  14. (loop for input in (get-by-tag form "input")
  15. for name = (plump:get-attribute input "name")
  16. for value = (plump:get-attribute input "value")
  17. when (and name value) collect (cons name value)
  18. when (string= name "UserName") collect (cons name login)
  19. when (string= name "Password") collect (cons name pass)))
  20. (response (drakma:http-request +nalunch/login-uri+
  21. :method :post
  22. :parameters parameters
  23. :cookie-jar cookies
  24. :user-agent +nalunch/mobile-ua+)))
  25. (when (search "id=\"LoginForm\"" response)
  26. (error "Bad username or password"))
  27. (if (search "<title>Чек</title>" response) ;; Reload feed page on 'Cheque'
  28. (drakma:http-request +nalunch/mobile-uri+ :cookie-jar cookies :user-agent +nalunch/mobile-ua+)
  29. response)))
  30. (defun nalunch/recent (login pass &optional cookies)
  31. (let ((cookies (or cookies (make-instance 'drakma:cookie-jar))))
  32. (multiple-value-bind (body status headers uri)
  33. (drakma:http-request +nalunch/mobile-uri+ :cookie-jar cookies :user-agent +nalunch/mobile-ua+)
  34. (declare (ignore status headers))
  35. (let* ((body (if (puri:uri= uri (puri:uri +nalunch/mobile-uri+))
  36. body
  37. (nalunch/auth login pass cookies body)))
  38. (dom (plump:parse body))
  39. (balance (parse-integer (plump:text (elt (clss:select ".newswire-header_balance" dom) 0))))
  40. (recent (loop for day across (clss:select ".day-feed" dom)
  41. append (loop for el across (clss:select ".media" day)
  42. for date = (select-text ".day-feed_date" day)
  43. for time = (select-text ".transaction_time" el)
  44. for price = (parse-integer (select-text ".transaction_price" el))
  45. for place = (select-text ".transaction-title" el)
  46. collect (list (cons :time (format nil "~A ~A" date time))
  47. (cons :price price)
  48. (cons :place place))))))
  49. (list (cons :balance balance)
  50. (cons :recent recent))))))
  51. (defun %nalunch/get-calend (year)
  52. (setf year (princ-to-string year))
  53. (unless (aget year *nalunch/calend*)
  54. (setf *nalunch/calend* (aget "data" (json-request +nalunch/basicdata-calend+))))
  55. (aget year *nalunch/calend*))
  56. (defun %nalunch/get-working-days (year month)
  57. (let* ((exceptions (aget (princ-to-string month) (%nalunch/get-calend year)))
  58. (days-in-month (local-time:days-in-month month year)))
  59. (loop for day from 1 upto days-in-month
  60. for ts = (local-time:encode-timestamp 0 0 0 0 day month year)
  61. for dof = (local-time:timestamp-day-of-week ts)
  62. for exc = (aget "isWorking" (aget (princ-to-string day) exceptions))
  63. when (or (and (<= 1 dof 5)
  64. (not (equal 2 exc)))
  65. (and (or (= dof 0) (= dof 6))
  66. (or (equal 0 exc)
  67. (equal 3 exc))))
  68. collect day)))
  69. (defun %nalunch/format (result &optional last)
  70. (let* ((balance (aget :balance result))
  71. (all (aget :recent result))
  72. (recent (cons (car all) (unless last (cdr all))))
  73. (now (local-time:now))
  74. (left-working-days (length (remove-if #'(lambda (d) (<= d (local-time:timestamp-day now)))
  75. (%nalunch/get-working-days (local-time:timestamp-year now)
  76. (local-time:timestamp-month now))))))
  77. (format nil "🍴 Баланс ~A руб~@[ на ~A дней, по ~$ руб~].~{~&~A~}"
  78. balance left-working-days (/ balance (max left-working-days 1))
  79. (mapcar (lambda (meal) (format nil "~A @ ~A — ~A руб."
  80. (aget :time meal) (aget :place meal) (aget :price meal)))
  81. recent))))
  82. ;; Cron
  83. (defvar *nalunch/last-results* (make-hash-table) "Last check results")
  84. (defvar *nalunch/jars* (make-hash-table) "Cookie jars")
  85. (defcron process-nalunch (:minute '(member 0 10 20 30 40 50))
  86. (dolist (chat-id (lists-get :nalunch))
  87. (secret/with (login-pass (list :nalunch chat-id))
  88. (if login-pass
  89. (let* ((cookie-jar (or (gethash chat-id *nalunch/jars*)
  90. (make-instance 'drakma:cookie-jar)))
  91. (old (gethash chat-id *nalunch/last-results*))
  92. (new (nalunch/recent (car login-pass) (cdr login-pass) cookie-jar)))
  93. (when new
  94. (when (and old (not (equal old new)))
  95. (send-response chat-id (%nalunch/format new t)))
  96. (setf (gethash chat-id *nalunch/last-results*) new
  97. (gethash chat-id *nalunch/jars*) cookie-jar)))
  98. (progn
  99. (log:warn "nalunch no login/pass for" chat-id)
  100. ;; (lists-set-entry :nalunch chat-id nil) ;; Comment out for now
  101. )))))
  102. ;; Hooks
  103. (def-message-cmd-handler handle-cmd-nalunch (:nalunch)
  104. (if (member chat-id *admins*)
  105. (send-response chat-id (nalunch-format
  106. (or *nalunch-last-result*
  107. (setf *nalunch-last-result*
  108. (nalunch-recent)))))
  109. (send-dont-understand chat-id)))
  110. (defun nalunch/handle-set-cron (chat-id enable)
  111. (lists-set-entry :nalunch chat-id enable)
  112. (bot-send-message chat-id
  113. (if enable
  114. "Включил рассылку. '/nalunch off' чтобы выключить, /nalunch - показать последние."
  115. "Без рассылки. '/nalunch on' - включить, /nalunch - последние.")))
  116. (defun nalunch/handle-auth (chat-id login pass)
  117. (let ((cookies (make-instance 'drakma:cookie-jar)))
  118. (handler-case
  119. (progn
  120. (nalunch/auth login pass cookies)
  121. (secret/set `(:nalunch ,chat-id) (cons login pass))
  122. (nalunch/handle-set-cron chat-id t))
  123. (error () (bot-send-message chat-id "Чот не смог, пропробуй другие.")))))
  124. (defun nalunch/handle-recent (chat-id)
  125. (secret/with (login-pass (list :nalunch chat-id))
  126. (bot-send-message chat-id
  127. (if login-pass
  128. (let* ((cookies (or (gethash chat-id *nalunch/jars*)
  129. (make-instance 'drakma:cookie-jar)))
  130. (data (nalunch/recent (car login-pass) (cdr login-pass) cookies)))
  131. (if data
  132. (progn
  133. (setf (gethash chat-id *nalunch/jars*) cookies)
  134. (%nalunch/format data))
  135. "Не смог получить данные. Попробуй перелогинься. /nalunch <login> <pass>"))
  136. "Нужен логин-пароль. /nalunch <login> <pass>")
  137. :parse-mode "markdown")))
  138. (def-message-cmd-handler handle-cmd-nalunch (:nalunch)
  139. (cond
  140. ((= 1 (length args))
  141. (nalunch/handle-set-cron chat-id (equal "on" (car args))))
  142. ((= 2 (length args)) (apply 'nalunch/handle-auth chat-id args))
  143. (:otherwise (nalunch/handle-recent chat-id))))