raif.lisp 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. (uiop:define-package :enikesha-scripts/raif
  2. (:use :cl)
  3. (:mix :uiop :drakma :cl-ppcre :cxml-stp)
  4. (:export
  5. #:main))
  6. (in-package :enikesha-scripts/raif)
  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 *cookie-jar* (make-instance 'drakma:cookie-jar) "Default cookie jar")
  10. (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")
  11. (defparameter +ns-soap-env+ "http://schemas.xmlsoap.org/soap/envelope/" "Soap envelope xmlns")
  12. (defparameter +ns-rconnect+ "http://service.rconnect" "Service r-connect xmlns")
  13. (defparameter +ct-xml+ "text/xml;charset=UTF-8" "XML Content-Type")
  14. (defvar *rc-auth-service* "RCAuthorizationService")
  15. (defvar *rc-account-service* "RCAccountService")
  16. (defun reset-cookie ()
  17. (setf *cookie-jar* (make-instance 'drakma:cookie-jar)))
  18. (defun get-service-url (service-name)
  19. (concatenate 'string *raif-ws* service-name))
  20. (defun soap-request-serialize (body)
  21. (let* ((doc (stp:copy *soap-dom*))
  22. (n-body (stp:find-recursively-if (stp:of-name "Body" +ns-soap-env+) doc)))
  23. (stp:append-child n-body body)
  24. (stp:serialize doc (cxml:make-string-sink))))
  25. (defun %rs/login (login password)
  26. (let ((n-method (stp:make-element "ser:login" +ns-rconnect+))
  27. (n-login (stp:make-element "login"))
  28. (n-password (stp:make-element "password")))
  29. (stp:append-child n-login (stp:make-text login))
  30. (stp:append-child n-password (stp:make-text password))
  31. (stp:append-child n-method n-login)
  32. (stp:append-child n-method n-password)
  33. n-method))
  34. (defun %r-call (service request)
  35. (multiple-value-bind (body code)
  36. (drakma:http-request (get-service-url service)
  37. :method :post
  38. :content (soap-request-serialize request)
  39. :content-type +ct-xml+
  40. :cookie-jar *cookie-jar*
  41. :user-agent *ua*
  42. :external-format-in :utf-8
  43. :external-format-out :utf-8
  44. :additional-headers `(("SOAPAction" . "")))
  45. (values (stp:first-child
  46. (stp:find-recursively-if
  47. (stp:of-name "Body" +ns-soap-env+)
  48. (cxml:parse body (stp:make-builder))))
  49. code)))
  50. (defun rc/perform-login (login password)
  51. (reset-cookie)
  52. (multiple-value-bind (info code)
  53. (%r-call *rc-auth-service* (%rs/login login password))
  54. (if (= code 200)
  55. info
  56. (error "Login error"))))
  57. (defun rc/get-batch-response ()
  58. (multiple-value-bind (res code)
  59. (%r-call *rc-auth-service* (make-element "ser:getBatchResponse" +ns-rconnect+))
  60. (when (= code 200)
  61. res)))
  62. (defun get-accounts (batch-response)
  63. (stp:filter-recursively (stp:of-name "accounts") batch-response))
  64. (defun %rs/get-last-account-movements (account count)
  65. (let ((n-method (stp:make-element "ser:GetLastAccountMovements" +ns-rconnect+))
  66. (n-account (stp:copy account))
  67. (n-count (stp:make-element "count")))
  68. (setf (stp:local-name n-account) "account")
  69. (stp:append-child n-count (stp:make-text (princ-to-string count)))
  70. (stp:append-child n-method n-account)
  71. (stp:append-child n-method n-count)
  72. n-method))
  73. (defun rc/get-last-account-movements (account count)
  74. (multiple-value-bind (info code)
  75. (%r-call *rc-account-service* (%rs/get-last-account-movements account count))
  76. (when (= code 200)
  77. (stp:filter-recursively (stp:of-name "return") info))))
  78. (defun %rs/get-account-movements (account start-date end-date)
  79. (let ((n-method (stp:make-element "ser:GetAccountMovements" +ns-rconnect+))
  80. (n-account (stp:copy account))
  81. (n-start-date (stp:make-element "startDate"))
  82. (n-end-date (stp:make-element "endDate")))
  83. (setf (stp:local-name n-account) "account")
  84. (stp:append-child n-start-date (stp:make-text start-date))
  85. (stp:append-child n-end-date (stp:make-text end-date))
  86. (stp:append-child n-method n-account)
  87. (stp:append-child n-method n-start-date)
  88. (stp:append-child n-method n-end-date)
  89. n-method))
  90. (defun rc/get-account-movements (account start-ut end-ut)
  91. (labels ((format-ut (ut)
  92. (multiple-value-bind (sec min hour day month year)
  93. (decode-universal-time ut)
  94. (format nil "~4,'0D-~2,'0D-~2,'0DT~2,'0D:~2,'0D:~2,'0D.0"
  95. year month day hour min sec))))
  96. (multiple-value-bind (info code)
  97. (%r-call *rc-account-service*
  98. (%rs/get-account-movements account
  99. (format-ut start-ut)
  100. (format-ut end-ut)))
  101. (when (= code 200)
  102. (stp:filter-recursively (stp:of-name "return") info)))))
  103. "<startDate>2017-05-09T16:57:23.0</startDate><endDate>2017'
  104. b'-05-16T16:57:23.0</endDate>"
  105. (defun tag->keyword (tag)
  106. (setf tag (cl-ppcre:regex-replace-all "([a-z])([A-Z])" tag '(0 "-" 1)))
  107. (setf tag (cl-ppcre:regex-replace-all "_" tag "-"))
  108. (intern (string-upcase tag) :keyword))
  109. (defun node->alist (node)
  110. (let (res)
  111. (stp:do-children (child node (nreverse res))
  112. (push (cons (tag->keyword (stp:local-name child))
  113. (stp:string-value child))
  114. res))))
  115. (defun aget (alist &rest keys)
  116. (loop for key in keys
  117. for ret = (cdr (assoc key alist :test #'equal)) then (cdr (assoc key ret :test #'equal))
  118. finally (return ret)))
  119. (defun get-account-name (account)
  120. (let ((num (aget account :number)))
  121. (concatenate 'string "assets:Raiffeisen:"
  122. (cond
  123. ((string= num "40817810503000266700") "Debit")
  124. ((string= num "40817978803000110883") "Savings:EUR")
  125. ((string= num "40817810203001534278") "Savings:Renov")
  126. ((string= num "40817810103001667025") "Savings:For credit")
  127. (:overwise (concatenate 'string
  128. (aget account :account-type)
  129. ":"
  130. num
  131. ":"
  132. (aget account :currency)))))))
  133. (defun get-move-payee (move)
  134. (aget move :short-description))
  135. (defun get-move-description (move)
  136. (let ((full (aget move :full-description)))
  137. (unless (or (zerop (length full))
  138. (string= "CARD **" (subseq full 0 (min (length full) 7))))
  139. (cl-ppcre:regex-replace-all "\\s" full " "))))
  140. (defun get-move-account2 (move)
  141. (if (string= "1" (aget move :type))
  142. "income" "expenses"))
  143. (defun short-date (date)
  144. (cl-ppcre:regex-replace-all "-" (subseq date 0 10) "/"))
  145. (defun moves->journal (account moves)
  146. (let ((account1 (get-account-name account))
  147. (currency (aget account :currency))
  148. entries)
  149. (loop for move in moves
  150. for date = (short-date (aget move :commit-date))
  151. for payee = (get-move-payee move)
  152. for description = (get-move-description move)
  153. for id = (aget move :id)
  154. for amount = (aget move :amount)
  155. for account2 = (get-move-account2 move)
  156. for expense = (equal "0" (aget move :type))
  157. do (push (format nil "~A~@[ (~A)~] ~A~@[ ; ~A~]~% ~37A ~A~A ~A~% ~37A ~A~A ~A"
  158. date nil payee description
  159. account1 (if expense "-" " ") amount currency
  160. account2 (if expense " " "-") amount currency)
  161. entries))
  162. ;; Total account balance
  163. (push (format nil "; balance ~A ~A = ~A"
  164. (short-date (aget account :balance-date))
  165. account1
  166. (aget account :balance))
  167. entries)
  168. (nreverse entries)))
  169. (defun main (argv)
  170. (destructuring-bind (type login pass) argv
  171. (rc/perform-login login pass)
  172. (let* ((accounts (get-accounts (rc/get-batch-response)))
  173. (now (get-universal-time))
  174. (week-ago (- now (* 60 60 24 7)))
  175. (moves (mapcar (lambda (a) (cons (node->alist a)
  176. (mapcar #'node->alist
  177. (if (string= type "last")
  178. (nreverse (rc/get-last-account-movements a 10))
  179. (rc/get-account-movements a week-ago now)))))
  180. accounts)))
  181. (format t "~{~A~^~%~%~}~%" (loop for (a . m) in moves append (moves->journal a m))))))