| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 |
- (uiop:define-package :enikesha-scripts/tinkoff
- (:use :cl)
- (:mix :uiop :drakma :cl-ppcre :yason :flexi-streams)
- (:export
- #:main))
- (in-package :enikesha-scripts/tinkoff)
- (defparameter +session-file+ ".tinkoff.sexp" "Session file pathspec")
- (defvar *api-base-url* "https://api.tinkoff.ru/v1/")
- (defvar *ua* "OnePlus ONE A2003/android: 6.0.1/TCSMB/3.4.2" "User agent")
- (defvar *pin-hash* nil "Pin hash (obtain from mitmproxy")
- (defvar *session-id* nil "Last session id")
- (defvar *origin* "mobile,ib5,loyalty,platform")
- (defvar *platform* "android")
- (defvar *device-id* "1df9bdeac787e08")
- (defvar *app-version* "3.4.2")
- (defun save-session ()
- (with-open-file (s +session-file+ :direction :output :if-exists :supersede :if-does-not-exist :create)
- (print `(setf *pin-hash* ,*pin-hash* *session-id* ,*session-id*) s)))
- (defun load-session ()
- (if (probe-file +session-file+)
- (load +session-file+)
- (error "No session file ~A" +session-file+)))
- ;; JSON processing
- (defun json-request (url &key (method :get) parameters form-data content content-type additional-headers (object-as :alist))
- (multiple-value-bind (stream status headers uri http-stream)
- (drakma:http-request url :method method
- :user-agent *ua*
- :parameters parameters
- :form-data form-data
- :content content :content-type content-type
- :additional-headers additional-headers
- :external-format-out :utf-8
- :force-binary t :want-stream t :decode-content t)
- (declare (ignore status headers))
- (unwind-protect
- (progn
- (setf (flex:flexi-stream-external-format stream) :utf-8)
- (values (yason:parse stream :object-as object-as) uri))
- (ignore-errors (close http-stream)))))
- (defun aget (alist &rest keys)
- (loop for key in keys
- for ret = (cdr (assoc key alist :test #'equal)) then (cdr (assoc key ret :test #'equal))
- finally (return ret)))
- (defun api-url (method)
- (concatenate 'string *api-base-url* method))
- (defun default-params ()
- `(("origin" . ,*origin*)
- ("platform" . ,*platform*)
- ("deviceId" . ,*device-id*)
- ("appVersion" . ,*app-version*)
- ("sessionid" . ,*session-id*)
- ("ccc" . "true")))
- (defun request (method &key params form-data content)
- (let* ((params (loop for (k . v) in (append (default-params) params) when v
- collect (cons (princ-to-string k) (princ-to-string v))))
- (r (json-request (api-url method) :method (if (or form-data content) :POST :GET)
- :parameters params
- :form-data form-data :content content)))
- (unless (string= "OK" (aget r "resultCode"))
- (error "Tinkoff API error: ~A" (aget r "errorMessage")))
- (aget r "payload")))
- (defun api/sign-up ()
- (let* ((old-session *session-id*)
- new-session)
- (let* ((*session-id* nil)
- (payload (request "sign_up" :params `(("pinHash" . ,*pin-hash*)
- ("oldSessionId" . ,old-session)
- ("auth_type" . "pin")
- ("auth_type_set_date" . "0")))))
- (setf new-session (aget payload "sessionId")))
- (setf *session-id* new-session)
- (prog1
- (request "level_up" :params `(("auth_type" . "pin") ("auth_type_set_date" . "0")))
- (save-session))))
- (defun api/accounts ()
- (request "accounts_flat"))
- (defun api/operations (&key account start end)
- (request "operations" :params `(("account" . ,account)
- ("start" . ,start)
- ("end" . ,end))))
- (defvar *unix-epoch-difference*
- (encode-universal-time 0 0 0 1 1 1970 0))
- (defun universal-to-unix-time (universal-time)
- (- universal-time *unix-epoch-difference*))
- (defun unix-to-universal-time (unix-time)
- (+ unix-time *unix-epoch-difference*))
- (defun get-unix-time ()
- (universal-to-unix-time (get-universal-time)))
- (defun short-date (ms)
- (if ms
- (multiple-value-bind (sec min hour day month year)
- (decode-universal-time (unix-to-universal-time (round ms 1000)))
- (declare (ignore sec min hour))
- (format nil "~4,'0D/~2,'0D/~2,'0D" year month day))
- "-"))
- (defun get-op-description (op)
- (let ((cat (parse-integer (aget op "category" "id") :junk-allowed t)))
- (cond
- ((equal cat 16) (if (> (aget op "accountAmount" "value")
- 1500)
- "project: Volvo" "project: Smart"))
- (:otherwise
- (or
- (aget op "payment" "fieldsValues" "comment")
- (aget op "brand" "name"))))))
- (defun get-op-account1 (op)
- (case (parse-integer (aget op "account"))
- (5001173482 "assets:Tinkoff:Debit")
- (1850735 "assets:Raiffeisen:Debit")
- (0047479860 "liabilities:Tinkoff:Credit:Platinum")
- (8102961813 "assets:Tinkoff:Savings:For credit")
- (t (concatenate 'string "assets:Tinkoff:" (aget op "account")))))
- (defun get-op-account2 (op)
- (let ((cat (parse-integer (aget op "category" "id") :junk-allowed t)))
- (case cat
- (60 "expenses:Food:Fast-food")
- (36 "expenses:Transport")
- (32 "expenses:Food:Restaurant")
- (20 "expenses:Life:Wear")
- (16 "expenses:Transport:Car:Gas")
- (10 "expenses:Food:Grocery")
- (t (concatenate 'string
- (if (equal "Credit" (aget op "type"))
- "income:" "expenses:")
- (aget op "category" "name"))))))
- (defun ops->journal (ops)
- (loop for op in ops
- for date = (short-date (aget op "operationTime" "milliseconds"))
- for payee = (aget op "description")
- for description = (get-op-description op)
- for account-amount = (aget op "accountAmount" "value")
- for account-currency = (aget op "accountAmount" "currency" "name")
- for amount = (aget op "amount" "value")
- for currency = (aget op "amount" "currency" "name")
- for account1 = (get-op-account1 op)
- for account2 = (get-op-account2 op)
- for expense = (equal "Debit" (aget op "type"))
- collect (format nil "~A ~A~@[ ; ~A~]~% ~38A ~A~,2F ~A~@[ @@ ~{~A~,2F ~A~}~]~% ~38A ~A~,2F ~A"
- date payee (unless (equal payee description) description)
- account2 (if expense " " "-") amount currency
- (unless (equal currency account-currency)
- (list (if expense "" "-") account-amount account-currency))
- account1 (if expense "-" " ") account-amount account-currency)))
- (defparameter +day+ (* 24 60 60) "Seconds in day")
- (defun main (argv)
- (load-session)
- (api/sign-up)
- (let* ((period (car argv))
- (end (get-unix-time))
- (start (- end
- (cond
- ((equal "day" period) +day+)
- ((equal "month" period) (* 30 +day+))
- (:otherwise (* 7 +day+)))))
- (ops (api/operations :start (* start 1000) :end (* end 1000))))
- (format t "~{~A~^~%~%~}~%" (ops->journal (nreverse ops))))
- (loop for a in (api/accounts)
- when (aget a "moneyAmount" "value")
- do (format t "; balance ~A ~A = ~A~A ~A~%"
- (short-date (aget a "lastPaymentDate" "milliseconds"))
- (get-op-account1 (push (cons "account" (aget a "id")) a))
- (if (equal "Credit" (aget a "accountType")) "-" "")
- (if (equal "Credit" (aget a "accountType"))
- (aget a "debtAmount" "value")
- (aget a "moneyAmount" "value"))
- (aget a "moneyAmount" "currency" "name"))))
|