gazprom.lisp 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. (in-package :cl-user)
  2. (defpackage chatikbot.plugins.gazprom
  3. (:use :cl :chatikbot.common :alexandria))
  4. (in-package :chatikbot.plugins.gazprom)
  5. (defparameter +gpn-api-url+ "https://api.gpnbonus.ru/ios/v2/")
  6. (defvar *api-os* "android")
  7. (defvar *api-ver* "1.7.4")
  8. (defvar *session* nil "Currently active session")
  9. (defvar *credentials-provider* nil "Active credentials provider")
  10. (defvar *bonus-account* "assets:gazprom:bonus")
  11. (defvar *assets-account* "assets")
  12. (defvar *gas-expenses-account* "expenses:transport:car:gas")
  13. (defvar *other-expenses-account* "expenses:food:snacks")
  14. (defvar *expenses-currency* "RUB")
  15. (defvar *entry-description* "ГазпромНефть")
  16. (defun filled (alist)
  17. (remove nil alist :key #'cdr))
  18. (defun %api (method &optional params)
  19. (let* ((response
  20. (json-request (concatenate 'string +gpn-api-url+ method)
  21. :parameters (filled (append `(("session" . ,*session*)
  22. ("os" . ,*api-os*)
  23. ("ver" . ,*api-ver*))
  24. params))))
  25. (message (agets response "message")))
  26. (if (equal message "Необходимо авторизоваться")
  27. (if *credentials-provider*
  28. (progn
  29. (funcall *credentials-provider*
  30. (lambda (login password)
  31. (setf *session* (^auth login password))))
  32. (let (*credentials-provider*) ;; Retry call resetting *credentials-provider* to prevent loop
  33. (%api method params)))
  34. (error message))
  35. response)))
  36. (defun get-token (login pass)
  37. (let ((date (format nil "~{~4,'0D~2,'0D~2,'0D~}"
  38. (subseq (reverse (multiple-value-list
  39. (decode-universal-time
  40. (get-universal-time) 0)))
  41. 3 6))))
  42. (crypto:byte-array-to-hex-string
  43. (crypto:digest-sequence
  44. :md5 (crypto:ascii-string-to-byte-array
  45. (concatenate 'string login pass date *api-ver*))))))
  46. (defun ^auth (login pass)
  47. (let* ((resp
  48. (json-request (concatenate 'string +gpn-api-url+ "auth.php")
  49. :method :post
  50. :content `(("login" . ,login)
  51. ("passw" . ,pass)
  52. ("token" . ,(get-token login pass))
  53. ("os" . ,*api-os*)
  54. ("ver" . ,*api-ver*))))
  55. (status (agets resp "status")))
  56. (unless (= status 1)
  57. (error (agets resp "message")))
  58. (agets resp "session")))
  59. (defun ^get-card-info ()
  60. (%api "getCardInfo.php"))
  61. (defun ^get-order (&key (count 20) (offset 0))
  62. (%api "getOrder.php" `(("count" . ,count) ("offset" . ,offset))))
  63. ;; Formatting
  64. (defvar *account-fuel* "expenses:Transport:Car:Gas")
  65. (defvar *account-other* "expenses:Food:Snacks")
  66. (defvar *account-asset* "liabilities:Tinkoff:Credit:Platinum")
  67. (defvar *income-bonus* "income:bonus")
  68. (defvar *account-bonus* "assets:Gazprom:Bonus")
  69. (defvar *default-currency* "RUB")
  70. (defun is-fuel (name)
  71. (or (equal name "ДТ+")
  72. (equal name "Аи-92")
  73. (equal name "Аи-95")
  74. (equal name "Аи-98")))
  75. (defun get-currency (name)
  76. (cond
  77. ((or (equal name "Аи-92")
  78. (equal name "Аи-95")
  79. (equal name "Аи-98")) "BENZ")
  80. ((equal name "ДТ+") "DIZ")
  81. (t *default-currency*)))
  82. (defun get-account (name)
  83. (cond
  84. ((is-fuel name) *account-fuel*)
  85. (t *account-other*)))
  86. (defun get-expense-posting (order)
  87. (let* ((name (agets order "name"))
  88. (is-fuel (is-fuel name))
  89. (count (parse-float (agets order "count")))
  90. (sum (parse-float (agets order "sum")))
  91. (currency (get-currency name)))
  92. (pta-ledger:make-posting
  93. :account (get-account name)
  94. :comment name
  95. :amount (pta-ledger:make-amount
  96. :quantity (if is-fuel count sum)
  97. :commodity currency)
  98. :unit-price (when is-fuel
  99. (pta-ledger:make-amount
  100. :quantity (/ sum count)
  101. :commodity *default-currency*)))))
  102. (defun orders->entry (date orders)
  103. (pta-ledger:make-entry
  104. :date (local-time:timestamp-to-universal (local-time:unix-to-timestamp date))
  105. :description *entry-description*
  106. :postings (loop for (type . orders) in (group-by orders (agetter "type"))
  107. for total = 0 then 0
  108. for bonus = 0 then 0
  109. append (append
  110. (loop for order in orders
  111. do (incf total (parse-float (agets order "sum")))
  112. do (incf bonus (parse-float (agets order "bonus")))
  113. collect (get-expense-posting order))
  114. (list (pta-ledger:make-posting
  115. :account *account-bonus*
  116. :amount (pta-ledger:make-amount
  117. :quantity bonus :commodity *default-currency*)))
  118. (when (= type 1)
  119. (list (pta-ledger:make-posting
  120. :account *income-bonus*
  121. :amount (pta-ledger:make-amount
  122. :quantity (* -1 bonus) :commodity *default-currency*))
  123. (pta-ledger:make-posting
  124. :account *account-asset*
  125. :amount (pta-ledger:make-amount
  126. :quantity (* -1 total) :commodity *default-currency*))))))))
  127. (defun format-card (card)
  128. (format nil "Баланс: ~,2F баллов~%Статус: ~A~%Литров в месяце: ~D"
  129. (agets card "card_balance")
  130. (agets card "card_status")
  131. (agets card "amount_current_month_liter")))
  132. (defun format-entries (changes)
  133. (text-chunks (mapcar #'pta-ledger:render changes)))
  134. ;; Cron
  135. (defvar *last-entries* (make-hash-table) "Last per-chat entries")
  136. (defvar *sessions* (make-hash-table) "Per-chat sessions")
  137. (defmacro with-chat-credentials ((chat-id) &body body)
  138. `(let* ((*session* (gethash ,chat-id *sessions*))
  139. (*credentials-provider* (lambda (authenticator)
  140. (with-secret (login-pass (list :gazprom ,chat-id))
  141. (if login-pass
  142. (apply authenticator login-pass)
  143. (error "no gazprom credentials for ~A" ,chat-id))))))
  144. (prog1 (progn ,@body)
  145. (setf (gethash ,chat-id *sessions*) *session*))))
  146. (defun get-chat-card (chat-id)
  147. (with-chat-credentials (chat-id)
  148. (^get-card-info)))
  149. (defun get-chat-last-n-orders (chat-id &optional (count 10))
  150. (with-chat-credentials (chat-id)
  151. (^get-order :count count)))
  152. (defun prepare-entries (orders)
  153. (loop for (date . orders) in (group-by orders (agetter "date"))
  154. collect (orders->entry date orders)))
  155. (defcron process-gazprom (:minute '(member 0 5 10 15 20 25 30 35 40 45 50 55))
  156. (dolist (chat-id (lists-get :gazprom))
  157. (let ((old (gethash chat-id *last-entries*))
  158. (new (get-chat-last-n-orders chat-id 20))
  159. (ledger-package (find-package :chatikbot.plugins.ledger)))
  160. (when new
  161. (when old
  162. (when-let (changes (prepare-entries (set-difference new old :test #'equalp)))
  163. (log:info changes)
  164. (if ledger-package
  165. (let ((new-chat-entry (symbol-function
  166. (intern "LEDGER/NEW-CHAT-ENTRY" ledger-package))))
  167. (dolist (entry changes)
  168. (funcall new-chat-entry chat-id (pta-ledger:clone-entry entry))))
  169. (bot-send-message chat-id (format-entries changes) :parse-mode "markdown"))))
  170. (let ((merged (remove-duplicates
  171. (merge 'list old new #'< :key (agetter "date"))
  172. :test 'equalp)))
  173. (setf (gethash chat-id *last-entries*)
  174. (subseq merged (max 0 (- (length merged) 100)))))))))
  175. (def-message-cmd-handler handler-gazprom (:gpn :gazprom)
  176. (let ((arg (car args)))
  177. (if (string= arg "bal")
  178. (bot-send-message chat-id (format-card (get-chat-card chat-id)) :parse-mode "markdown")
  179. (let ((last (prepare-entries (get-chat-last-n-orders chat-id (if arg (parse-integer arg) 10)))))
  180. (bot-send-message chat-id (format-entries last) :parse-mode "markdown")))))