raiffeisen.lisp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. (in-package :cl-user)
  2. (defpackage chatikbot.plugins.raiffeisen
  3. (:use :cl :chatikbot.common))
  4. (in-package :chatikbot.plugins.raiffeisen)
  5. (eval-when (:compile-toplevel :load-toplevel :execute)
  6. (ql:quickload :cxml-stp))
  7. (defvar *raif-ws* "https://connect.raiffeisen.ru/Mobile-WS/services/" "Base WS URL")
  8. (defvar *ua* "Dalvik/2.1.0 (Linux; U; Android 6.0.1; ONE A2003 Build/MMB29M) Android/3.19.0(459)" "User agent")
  9. (defvar *soap-dom* (cxml:parse "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><soapenv:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://entry.rconnect/xsd\" xmlns:ser=\"http://service.rconnect\" xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:soapenc=\"http://schemas.xmlsoap.org/soap/encoding/\"><soapenv:Header /><soapenv:Body></soapenv:Body></soapenv:Envelope>" (stp:make-builder)) "XML SOAP raif service envelope")
  10. (defparameter +ns-soap-env+ "http://schemas.xmlsoap.org/soap/envelope/" "Soap envelope xmlns")
  11. (defparameter +ns-rconnect+ "http://service.rconnect" "Service r-connect xmlns")
  12. (defparameter +ct-xml+ "text/xml;charset=UTF-8" "XML Content-Type")
  13. (defvar *rc-auth-service* "RCAuthorizationService")
  14. (defvar *rc-account-service* "RCAccountService")
  15. (defvar *cookie-jar* nil "Active cookie jar")
  16. (defvar *credentials-provider* nil "Active credentials provider")
  17. (defun get-service-url (service-name)
  18. (concatenate 'string *raif-ws* service-name))
  19. (defun soap-request-serialize (body)
  20. (let* ((doc (stp:copy *soap-dom*))
  21. (n-body (stp:find-recursively-if (stp:of-name "Body" +ns-soap-env+) doc)))
  22. (stp:append-child n-body (stp:copy body))
  23. (stp:serialize doc (cxml:make-string-sink))))
  24. (define-condition soap-error (error)
  25. ((name :initarg :name :reader soap-error-name)
  26. (message :initarg :message :reader soap-error-message))
  27. (:report (lambda (condition stream)
  28. (with-slots (name message) condition
  29. (format stream "Soap error ~A: ~A" name message)))))
  30. (defun %rs/login (login password)
  31. (let ((n-method (stp:make-element "ser:login" +ns-rconnect+))
  32. (n-login (stp:make-element "login"))
  33. (n-password (stp:make-element "password")))
  34. (stp:append-child n-login (stp:make-text login))
  35. (stp:append-child n-password (stp:make-text password))
  36. (stp:append-child n-method n-login)
  37. (stp:append-child n-method n-password)
  38. n-method))
  39. (defun %r-call (service request)
  40. (let* ((body
  41. (handler-case
  42. (http-request (get-service-url service)
  43. :method :post
  44. :content (soap-request-serialize request)
  45. :cookie-jar *cookie-jar*
  46. :user-agent *ua*
  47. :headers `(("SOAPAction" . "")
  48. (:content-type . ,+ct-xml+)))
  49. (dex:http-request-internal-server-error (e)
  50. (dex:response-body e))))
  51. (x/body
  52. (stp:first-child
  53. (stp:find-recursively-if
  54. (stp:of-name "Body" +ns-soap-env+)
  55. (cxml:parse body (stp:make-builder))))))
  56. (if (string-equal (stp:local-name x/body) "Fault")
  57. (let ((name (stp:local-name (stp:first-child (stp:find-recursively-if (stp:of-name "detail") x/body))))
  58. (message (stp:data (stp:first-child (stp:find-recursively-if (stp:of-name "faultstring") x/body)))))
  59. (if (and *credentials-provider*
  60. (string-equal name "sessionExpiredFault"))
  61. (progn
  62. (funcall *credentials-provider*
  63. (lambda (login password)
  64. (%r-call *rc-auth-service* (%rs/login login password))))
  65. (%r-call service request))
  66. (error 'soap-error :name name :message message)))
  67. x/body)))
  68. (defun rc/get-batch-response ()
  69. (%r-call *rc-auth-service* (stp:make-element "ser:getBatchResponse" +ns-rconnect+)))
  70. (defun get-accounts (batch-response)
  71. (stp:filter-recursively (stp:of-name "accounts") batch-response))
  72. (defun %rs/get-last-account-movements (account count)
  73. (let ((n-method (stp:make-element "ser:GetLastAccountMovements" +ns-rconnect+))
  74. (n-account (stp:copy account))
  75. (n-count (stp:make-element "count")))
  76. (setf (stp:local-name n-account) "account")
  77. (stp:append-child n-count (stp:make-text (princ-to-string count)))
  78. (stp:append-child n-method n-account)
  79. (stp:append-child n-method n-count)
  80. n-method))
  81. (defun rc/get-last-account-movements (account count)
  82. (let ((info (%r-call *rc-account-service* (%rs/get-last-account-movements account count))))
  83. (stp:filter-recursively (stp:of-name "return") info)))
  84. (defun %rs/get-account-movements (account start-date end-date)
  85. (let ((n-method (stp:make-element "ser:GetAccountMovements" +ns-rconnect+))
  86. (n-account (stp:copy account))
  87. (n-start-date (stp:make-element "startDate"))
  88. (n-end-date (stp:make-element "endDate")))
  89. (setf (stp:local-name n-account) "account")
  90. (stp:append-child n-start-date (stp:make-text start-date))
  91. (stp:append-child n-end-date (stp:make-text end-date))
  92. (stp:append-child n-method n-account)
  93. (stp:append-child n-method n-start-date)
  94. (stp:append-child n-method n-end-date)
  95. n-method))
  96. (defun rc/get-account-movements (account start-ut end-ut)
  97. (labels ((format-ut (ut)
  98. (multiple-value-bind (sec min hour day month year)
  99. (decode-universal-time ut)
  100. (format nil "~4,'0D-~2,'0D-~2,'0DT~2,'0D:~2,'0D:~2,'0D.0"
  101. year month day hour min sec))))
  102. (let ((info (%r-call *rc-account-service*
  103. (%rs/get-account-movements account
  104. (format-ut start-ut)
  105. (format-ut end-ut)))))
  106. (stp:filter-recursively (stp:of-name "return") info))))
  107. (defun tag->keyword (tag)
  108. (setf tag (cl-ppcre:regex-replace-all "([a-z])([A-Z])" tag '(0 "-" 1)))
  109. (setf tag (cl-ppcre:regex-replace-all "_" tag "-"))
  110. (intern (string-upcase tag) :keyword))
  111. (defun node->alist (node &optional keep-id)
  112. (let (res)
  113. (stp:do-children (child node (nreverse res))
  114. (let ((name (stp:local-name child)))
  115. (when (or keep-id
  116. (not (string-equal "id" name)))
  117. (push (cons (tag->keyword name)
  118. (stp:string-value child))
  119. res))))))
  120. (defun get-account-currency (account)
  121. (let ((currency (agets account :currency)))
  122. (cond
  123. ((equal currency "RUR") "RUB")
  124. (t currency))))
  125. (defun get-account-name (account)
  126. (let ((num (agets account :number)))
  127. (concatenate 'string "assets:Raiffeisen:"
  128. (cond
  129. ((string= num "40817810503000266700") "Debit")
  130. ((string= num "40817978803000110883") "Savings:EUR")
  131. ((string= num "40817810203001534278") "Savings:Renov")
  132. ((string= num "40817810103001667025") "Savings:For credit")
  133. (:overwise (concatenate 'string
  134. (agets account :account-type)
  135. ":"
  136. num
  137. ":"
  138. (get-account-currency account)))))))
  139. (defun get-move-payee (move)
  140. (agets move :short-description))
  141. (defun get-move-description (move)
  142. (let ((full (agets move :full-description)))
  143. (unless (or (zerop (length full))
  144. (string= "CARD **" (subseq full 0 (min (length full) 7))))
  145. (cl-ppcre:regex-replace-all "\\s" full " "))))
  146. (defun get-move-account2 (move)
  147. (let ((desc (agets move :short-description)))
  148. (cond
  149. ((equal desc "P/O 001,40817810503000266700RC") "assets:Raiffeisen:Debit")
  150. ((equal desc "P/O 001,40817810203001534278RC") "assets:Raiffeisen:Savings:Renov")
  151. ((equal desc "TINKOFF BANK CARD2CARD MOSCOW") "assets:Tinkoff:Debit")
  152. ((equal (subseq desc 0 (min 7 (length desc))) "RBA ATM") "assets:Cash:RUB")
  153. (t (if (string= "1" (agets move :type)) "income" "expenses")))))
  154. (defun short-date (date)
  155. (cl-ppcre:regex-replace-all "-" (subseq date 0 10) "/"))
  156. (defun move->journal (move)
  157. (let* ((account (agets move :account))
  158. (account1 (get-account-name account))
  159. (account2 (get-move-account2 move))
  160. (currency (get-account-currency account))
  161. (date (short-date (agets move :commit-date)))
  162. (payee (get-move-payee move))
  163. (description (get-move-description move))
  164. (amount (parse-float (agets move :amount)))
  165. (expense (equal "0" (agets move :type))))
  166. (format nil "~A~@[ (~A)~] ~A~@[ ; ~A~]~% ~37A ~A~,2F ~A~% ~37A ~A~,2F ~A"
  167. date nil payee description
  168. account2 (if expense " " "-") amount currency
  169. account1 (if expense "-" " ") amount currency)))
  170. (defun move->balance (move)
  171. (let ((account (agets move :account)))
  172. (format nil "; balance ~A ~A = ~,2F ~A"
  173. (short-date (agets account :balance-date))
  174. (get-account-name account)
  175. (parse-float (agets account :balance))
  176. (get-account-currency account))))
  177. (defun get-last-n-movements (&optional (count 10))
  178. (loop for account in (get-accounts (rc/get-batch-response))
  179. append (loop for move in (nreverse (rc/get-last-account-movements account count))
  180. collect (cons (cons :account (node->alist account t))
  181. (node->alist move)))))
  182. (defun get-last-movements (begin-ut end-ut)
  183. (loop for account in (get-accounts (rc/get-batch-response))
  184. append (loop for move in (nreverse (rc/get-account-movements account begin-ut end-ut))
  185. collect (cons (cons :account (node->alist account t))
  186. (node->alist move)))))
  187. (defun format-changes (movements)
  188. (format nil "```~%~{~A~^~%~%~}```"
  189. (append (mapcar #'move->journal movements)
  190. (list (move->balance (car (last movements)))))))
  191. ;; Cron
  192. (defvar *last-movements* (make-hash-table) "Last per-chat movements")
  193. (defvar *cookie-jars* (make-hash-table) "Per-chat cookie jars")
  194. (defun get-chat-last-movements (chat-id &optional (offset +day+))
  195. (let* ((*cookie-jar* (or (gethash chat-id *cookie-jars*)
  196. (cl-cookie:make-cookie-jar)))
  197. (*credentials-provider* (lambda (authenticator)
  198. (with-secret (login-pass (list :raiffeisen chat-id))
  199. (if login-pass
  200. (apply authenticator login-pass)
  201. (error "no raiffeisen credentials for ~A" chat-id)))))
  202. (now (get-universal-time))
  203. (pre (- now offset))
  204. (new (get-last-movements pre now)))
  205. (when new
  206. (setf (gethash chat-id *cookie-jars*) *cookie-jar*))
  207. new))
  208. (defcron process-raiffeisen (:minute '(member 0 5 10 15 20 25 30 35 40 45 50 55))
  209. (dolist (chat-id (lists-get :raiffeisen))
  210. (let ((old (gethash chat-id *last-movements*))
  211. (new (get-chat-last-movements chat-id (* 7 +day+))))
  212. (when new
  213. (log:info "Got ~A raif events" (length new))
  214. (when old
  215. (alexandria:when-let (changes (set-difference new old :test #'equal))
  216. (bot-send-message chat-id (format-changes changes) :parse-mode "markdown")))
  217. (setf (gethash chat-id *last-movements*) new)))))
  218. (def-message-cmd-handler handler-raif (:raif)
  219. (let ((last (get-chat-last-movements chat-id (* (if args (parse-integer (car args)) 7) +day+))))
  220. (bot-send-message chat-id (format-changes last) :parse-mode "markdown")))