Innocenty Enikeev il y a 8 ans
Parent
commit
47dbb11233
3 fichiers modifiés avec 213 ajouts et 3 suppressions
  1. 10 0
      enikesha-scripts.asd
  2. 0 3
      raif.lisp
  3. 203 0
      tinkoff.lisp

+ 10 - 0
enikesha-scripts.asd

@@ -0,0 +1,10 @@
+#-asdf3.1 (error "ASDF 3.1 or bust!")
+
+(defsystem "enikesha-scripts"
+  :version "0" ;; not even released
+  :description "Various small programs that I write in CL in lieu of shell scripts"
+  :license "MIT" ;; also BSD or bugroff
+  :author "Innokentiy Enikeev"
+  :class :package-inferred-system
+  :depends-on ("enikesha-scripts/raif"
+               "enikesha-scripts/tinkoff"))

+ 0 - 3
raif.lisp

@@ -116,9 +116,6 @@
       (when (= code 200)
         (stp:filter-recursively (stp:of-name "return") info)))))
 
-"<startDate>2017-05-09T16:57:23.0</startDate><endDate>2017'
-     b'-05-16T16:57:23.0</endDate>"
-
 (defun tag->keyword (tag)
   (setf tag (cl-ppcre:regex-replace-all "([a-z])([A-Z])" tag '(0 "-" 1)))
   (setf tag (cl-ppcre:regex-replace-all "_" tag "-"))

+ 203 - 0
tinkoff.lisp

@@ -0,0 +1,203 @@
+(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))))
+
+(defun get-account-name (account)
+  (let ((num (aget account :number)))
+    (concatenate 'string "assets:Raiffeisen:"
+                 (cond
+                   ((string= num "40817810503000266700") "Debit")
+                   ((string= num "40817978803000110883") "Savings:EUR")
+                   ((string= num "40817810203001534278") "Savings:Renov")
+                   ((string= num "40817810103001667025") "Savings:For credit")
+                   (:overwise (concatenate 'string
+                                           (aget account :account-type)
+                                           ":"
+                                           num
+                                           ":"
+                                           (aget account :currency)))))))
+
+(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"))))
+    (cond
+      ((= 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"))))
+    (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~A ~A~%    ~38A  ~A~A ~A~[ @@ ~A~A ~A~]"
+                     date payee (unless (equal payee description) description)
+                     account1 (if expense "-" " ") account-amount account-currency
+                     account2 (if expense " " "-") account-amount account-currency
+                     (not (equal currency account-currency)) (if expense " " "-") amount 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)
+     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"))))