nalunch.lisp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. (in-package :cl-user)
  2. (defpackage chatikbot.plugins.nalunch
  3. (:use :cl :chatikbot.common))
  4. (in-package :chatikbot.plugins.nalunch)
  5. (defvar *nalunch/calend* nil "Working calendar exceptions")
  6. (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"
  7. "Mobile UA")
  8. (defparameter +nalunch/mobile-uri+ "https://www.nalunch.ru/Mobile")
  9. (defparameter +nalunch/login-uri+ "https://www.nalunch.ru/Mobile/Account/Login")
  10. (defparameter +nalunch/basicdata-calend+ "http://basicdata.ru/api/json/calend/")
  11. (defun nalunch/auth (login pass cookies &optional dom)
  12. (let* ((dom (or dom
  13. (xml-request +nalunch/login-uri+ :cookie-jar cookies :user-agent +nalunch/mobile-ua+)))
  14. (form (plump:get-element-by-id dom "LoginForm"))
  15. (parameters
  16. (loop for input in (get-by-tag form "input")
  17. for name = (plump:get-attribute input "name")
  18. for value = (plump:get-attribute input "value")
  19. when (and name value) collect (cons name value)
  20. when (string= name "UserName") collect (cons name login)
  21. when (string= name "Password") collect (cons name pass))))
  22. (multiple-value-bind (response status response-headers)
  23. (http-request +nalunch/login-uri+
  24. :method :post
  25. :content parameters
  26. :cookie-jar cookies
  27. :user-agent +nalunch/mobile-ua+)
  28. (when (and (member status '(301 302 303 307) :test #'=)
  29. (gethash "location" response-headers))
  30. (setf response (http-request (quri:merge-uris
  31. (quri:uri (gethash "location" response-headers))
  32. (quri:uri +nalunch/login-uri+))
  33. :cookie-jar cookies
  34. :user-agent +nalunch/mobile-ua+)))
  35. (when (search "id=\"LoginForm\"" response)
  36. (error "Bad username or password"))
  37. (if (search "<title>Чек</title>" response) ;; Reload feed page on 'Cheque'
  38. (xml-request +nalunch/mobile-uri+ :cookie-jar cookies :user-agent +nalunch/mobile-ua+)
  39. (plump:parse response)))))
  40. (defun nalunch/recent (login pass &optional cookies)
  41. (let ((cookies (or cookies (cl-cookie:make-cookie-jar))))
  42. (multiple-value-bind (dom status headers uri)
  43. (xml-request +nalunch/mobile-uri+ :cookie-jar cookies :user-agent +nalunch/mobile-ua+)
  44. (declare (ignore status headers))
  45. (let* ((dom (if (quri:uri= uri (quri:uri +nalunch/mobile-uri+))
  46. dom
  47. (nalunch/auth login pass cookies dom)))
  48. (balance (parse-integer (plump:text (elt (clss:select ".newswire-header_balance" dom) 0))))
  49. (recent (loop for day across (clss:select ".day-feed" dom)
  50. append (loop for el across (clss:select ".media" day)
  51. for date = (select-text day ".day-feed_date")
  52. for time = (select-text el ".transaction_time")
  53. for price = (parse-float (select-text el ".transaction_price"))
  54. for place = (select-text el ".transaction-title")
  55. collect (list (cons :date date)
  56. (cons :time time)
  57. (cons :price price)
  58. (cons :place place))))))
  59. (list (cons :balance balance)
  60. (cons :recent recent))))))
  61. (defsetting *currency* "RUB")
  62. (defsetting *expense-account* "expenses:Food:Work")
  63. (defsetting *liabilities-account* "liabilities:nalunch")
  64. (defparameter +months+ '("" "января" "февраля" "марта" "апреля" "мая" "июня" "июля" "августа" "сентября" "октября" "ноября" "декабря"))
  65. (defun date-time->ut (date time)
  66. (let* ((decoded-now (multiple-value-list
  67. (decode-universal-time (get-universal-time) *chat-default-timezone*)))
  68. (year (nth 5 decoded-now))
  69. (hour (parse-integer time :start 0 :end 2))
  70. (minute (parse-integer time :start 3 :end 5))
  71. (day (if (string= date "Сегодня")
  72. (nth 3 decoded-now)
  73. (parse-integer date :start 0 :end 2)))
  74. (month (if (string= date "Сегодня")
  75. (nth 4 decoded-now)
  76. (position (subseq date 3) +months+ :test #'equal))))
  77. (encode-universal-time 0 minute hour day month year *chat-default-timezone*)))
  78. (defun recent->entry (recent)
  79. (let* ((pta-ledger (find-package :pta-ledger))
  80. (make-entry (symbol-function (intern "MAKE-ENTRY" pta-ledger)))
  81. (make-posting (symbol-function (intern "MAKE-POSTING" pta-ledger)))
  82. (make-amount (symbol-function (intern "MAKE-AMOUNT" pta-ledger)))
  83. (date (date-time->ut (agets recent :date) (agets recent :time)))
  84. (payee (agets recent :place))
  85. (amount (agets recent :price)))
  86. (funcall make-entry
  87. :date date
  88. :description payee
  89. :postings (list
  90. (funcall make-posting
  91. :account *expense-account*
  92. :amount (funcall make-amount
  93. :quantity amount
  94. :commodity *currency*))
  95. (funcall make-posting
  96. :account *liabilities-account*
  97. :amount (funcall make-amount
  98. :quantity (* -1 amount)
  99. :commodity *currency*))))))
  100. (defun %nalunch/get-calend (year)
  101. (setf year (princ-to-string year))
  102. (unless (aget year *nalunch/calend*)
  103. (setf *nalunch/calend* (aget "data" (json-request +nalunch/basicdata-calend+))))
  104. (aget year *nalunch/calend*))
  105. (defun %nalunch/get-working-days (year month)
  106. (let* ((exceptions (aget (princ-to-string month) (%nalunch/get-calend year)))
  107. (days-in-month (local-time:days-in-month month year)))
  108. (loop for day from 1 upto days-in-month
  109. for ts = (local-time:encode-timestamp 0 0 0 0 day month year)
  110. for dof = (local-time:timestamp-day-of-week ts)
  111. for exc = (aget "isWorking" (aget (princ-to-string day) exceptions))
  112. when (or (and (<= 1 dof 5)
  113. (not (equal 2 exc)))
  114. (and (or (= dof 0) (= dof 6))
  115. (or (equal 0 exc)
  116. (equal 3 exc))))
  117. collect day)))
  118. (defun %nalunch/format (result &optional last)
  119. (let* ((balance (aget :balance result))
  120. (all (aget :recent result))
  121. (recent (cons (car all) (unless last (cdr all))))
  122. (now (local-time:now))
  123. (left-working-days (length (remove-if #'(lambda (d) (<= d (local-time:timestamp-day now)))
  124. (%nalunch/get-working-days (local-time:timestamp-year now)
  125. (local-time:timestamp-month now))))))
  126. (format nil "🍴 Баланс ~A руб~@[ на ~A дней, по ~$ руб~].~{~&~A~}"
  127. balance left-working-days (/ balance (max left-working-days 1))
  128. (mapcar (lambda (meal) (format nil "~A ~A @ ~A — ~A руб."
  129. (aget :date meal) (aget :time meal)
  130. (aget :place meal) (aget :price meal)))
  131. recent))))
  132. ;; Cron
  133. (defvar *nalunch/last-results* (make-hash-table) "Last check results")
  134. (defvar *nalunch/jars* (make-hash-table) "Cookie jars")
  135. (defcron process-nalunch (:minute '(member 0 10 20 30 40 50))
  136. (dolist (chat-id (lists-get :nalunch))
  137. (with-secret (login-pass (list :nalunch chat-id))
  138. (if login-pass
  139. (let* ((cookie-jar (or (gethash chat-id *nalunch/jars*)
  140. (cl-cookie:make-cookie-jar)))
  141. (ledger-package (find-package :chatikbot.plugins.ledger))
  142. (old (gethash chat-id *nalunch/last-results*))
  143. (new (nalunch/recent (car login-pass) (cdr login-pass) cookie-jar)))
  144. (when new
  145. (when (and old (not (equal (aget :balance old)
  146. (aget :balance new))))
  147. (bot-send-message chat-id (%nalunch/format new t))
  148. (when ledger-package
  149. (let ((new-chat-entry (symbol-function
  150. (intern "LEDGER/NEW-CHAT-ENTRY" ledger-package))))
  151. (dolist (recent (set-difference (agets new :recent) (agets old :recent) :test #'equalp))
  152. (funcall new-chat-entry chat-id (recent->entry recent))))))
  153. (setf (gethash chat-id *nalunch/last-results*) new
  154. (gethash chat-id *nalunch/jars*) cookie-jar)))
  155. (progn
  156. (log:warn "nalunch no login/pass for" chat-id))))))
  157. ;; Hooks
  158. (defun nalunch/handle-set-cron (chat-id enable)
  159. (lists-set-entry :nalunch chat-id enable)
  160. (bot-send-message chat-id
  161. (if enable
  162. "Включил рассылку. '/nalunch off' чтобы выключить, /nalunch - показать последние."
  163. "Без рассылки. '/nalunch on' - включить, /nalunch - последние.")))
  164. (defun nalunch/handle-auth (chat-id login pass)
  165. (let ((cookies (cl-cookie:make-cookie-jar)))
  166. (handler-case
  167. (progn
  168. (nalunch/auth login pass cookies)
  169. (secret-set `(:nalunch ,chat-id) (cons login pass))
  170. (nalunch/handle-set-cron chat-id t))
  171. (error () (bot-send-message chat-id "Чот не смог, пропробуй другие.")))))
  172. (defun nalunch/handle-recent (chat-id)
  173. (with-secret (login-pass (list :nalunch chat-id))
  174. (bot-send-message chat-id
  175. (if login-pass
  176. (let* ((cookies (or (gethash chat-id *nalunch/jars*)
  177. (cl-cookie:make-cookie-jar)))
  178. (data (nalunch/recent (car login-pass) (cdr login-pass) cookies)))
  179. (if data
  180. (progn
  181. (setf (gethash chat-id *nalunch/jars*) cookies)
  182. (%nalunch/format data))
  183. "Не смог получить данные. Попробуй перелогинься. /nalunch <login> <pass>"))
  184. "Нужен логин-пароль. /nalunch <login> <pass>")
  185. :parse-mode "markdown")))
  186. (def-message-cmd-handler handle-cmd-nalunch (:nalunch)
  187. (cond
  188. ((= 1 (length args))
  189. (nalunch/handle-set-cron chat-id (equal "on" (car args))))
  190. ((= 2 (length args)) (apply 'nalunch/handle-auth chat-id args))
  191. (:otherwise (nalunch/handle-recent chat-id))))