1
0

3 Коммитууд 642a9812ce ... b93c7510ad

Эзэн SHA1 Мессеж Огноо
  Innokentii Enikeev b93c7510ad [revolut] Initial version 3 жил өмнө
  Innokentii Enikeev 873b53a280 [poller] Reset stored token on error 3 жил өмнө
  Innokentii Enikeev a9320c6462 [music] Use dockerized URIs 3 жил өмнө
3 өөрчлөгдсөн 373 нэмэгдсэн , 5 устгасан
  1. 4 4
      plugins/music.lisp
  2. 368 0
      plugins/revolut.lisp
  3. 1 1
      poller.lisp

+ 4 - 4
plugins/music.lisp

@@ -4,11 +4,11 @@
 (in-package :chatikbot.plugins.music)
 
 (defvar *cookies* (cl-cookie:make-cookie-jar))
-(defvar *deluge-api* "http://localhost:8112/json")
+(defvar *deluge-api* "http://deluge:8112/json")
 (defvar *deluge-password* "chads")
 (defvar *deluge-request-id* 1)
-(defvar *chad-music-stats-url* "http://localhost:5000/api/stats")
-(defvar *chad-music-rescan-url* "http://localhost:5000/api/rescan")
+(defvar *chad-music-stats-url* "http://music-back:5000/api/stats")
+(defvar *chad-music-rescan-url* "http://music-back:5000/api/rescan")
 (defvar *slskd-api* "http://localhost:5015/api/v0")
 (defvar *slskd-downloads-dir* "/data/upload/batch2/")
 
@@ -355,7 +355,7 @@
 (defun web-action-hmac (action &optional (chat-id *chat-id*))
   (token-hmac (format nil "~a-~a" chat-id action)))
 
-(def-webhook-handler ledger/handle-webhook ("music")
+(def-webhook-handler music/handle-webhook ("music")
   (destructuring-bind (chat-id hmac action) *paths*
     (let ((*chat-id* (parse-integer chat-id)))
       (when (and (string= (web-action-hmac action) hmac)

+ 368 - 0
plugins/revolut.lisp

@@ -0,0 +1,368 @@
+(in-package :cl-user)
+(defpackage chatikbot.plugins.revolut
+  (:use :cl :chatikbot.common :alexandria))
+(in-package :chatikbot.plugins.revolut)
+
+(defparameter +api-domain+ "app.revolut.com")
+(defparameter +api-uri+ (format nil "https://~a/api/retail/" +api-domain+))
+(defparameter +device-id+ "cb483962-b75c-4e4d-b4da-e5afee9a664c")
+(defparameter +user-agent+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:100.0) Gecko/20100101 Firefox/100.0")
+
+(defvar *accounts* nil "alist of account aliases, by id")
+(defvar *merchant-expenses*
+  '(("78c23116-39ff-4ebf-a311-344dbc9d507d" . "expenses:transport:taxi") ; Bolt
+    ("239ce793-91a7-47ba-8085-9de84c02b1c6" . "expenses:travel:hotel") ; booking.com
+    ("5a3451cf-dc08-4d94-9969-a4d080b7402c" . "expenses:food:fast-food") ; McDonalds
+    ))
+(defvar *category-expenses*
+  '(("56bdc570-99c6-420d-a855-d09dc5eaabc3" . "expenses:food:work")))
+(defvar *tag-expenses*
+      '(("groceries" . "expenses:food:groceries")
+        ("restaurants" . "expenses:food:restaurants")
+        ("health" . "expenses:life:health")))
+
+(defvar *topup-accounts*
+      '(("Top-Up by *1508" . "assets:rbcz:czk")))
+
+(defun is-content (rest-method)
+  (case rest-method
+    ((:post :put) t)))
+
+(defun make-cookie-jar (token)
+  (when token         
+    (cl-cookie:make-cookie-jar
+     :cookies
+     (list                             
+      (cl-cookie:make-cookie
+       :name "credentials"
+       :value (base64:string-to-base64-string
+               (format nil "~a:~a" (agets token "user" "id") (agets token "accessToken")))
+       :domain +api-domain+)))))
+
+(defmethod poller-request ((module (eql :revolut)) method &rest params)
+  (handler-case
+      (let* ((uri (if (listp method) (car method) method))             
+             (rest-method (if (listp method) (cdr method) :get))
+             (is-content (is-content rest-method))
+             (res
+               (json-request (concatenate 'string +api-uri+ uri)
+                             :headers `((:x-device-id . ,+device-id+)
+                                        (:x-browser-application . "WEB_CLIENT"))
+                             :user-agent +user-agent+
+                             :cookie-jar (make-cookie-jar *poller-token*)
+                             :method rest-method
+                             :json-content is-content
+                             (if is-content :content :parameters)
+                             (rest-parameters params t))))
+        res)
+    (dex:http-request-failed (e) e)))
+
+(defmethod poller-validate ((module (eql :revolut)) response)
+  (not (typep response 'dex:http-request-unauthorized)))
+
+(defun signin (phone passcode)
+  (let ((sign-res (poller-request :revolut '("signin" . :post)
+                                  :|phone| phone
+                                  :|channel| "APP"
+                                  :|password| passcode)))
+    (when (listp sign-res)
+      (loop with start = (get-universal-time)
+            for res = (poller-request :revolut '("token" . :post)
+                                      :|phone| phone
+                                      :|password| passcode
+                                      :|tokenId| (agets sign-res "tokenId"))
+            when (listp res) do (return res)
+            unless (= (dex:response-status res) 422) do (return)
+            when (> (- (get-universal-time) start) 600) do (return)
+            do (sleep 2)))))
+
+(defun refresh-token (user-id refresh-code)
+  (let* ((res (poller-request :revolut '("token" . :put)
+                              :|userId| user-id
+                              :|refreshCode| refresh-code)))
+    (when (listp res) res)))
+ 
+(defmethod poller-get-token ((module (eql :revolut)) secret)
+  (let ((user-id (agets *poller-token* "user" "id"))
+        (refresh-code (agets *poller-token* "refreshCode")))
+    ;; Try to refresh old token
+    (when (and user-id refresh-code)
+      (when-let (token (refresh-token user-id refresh-code))
+        (return-from poller-get-token
+          (append token `(("user" . (("id" . ,user-id))))))))
+    ;; Signin for a new token
+    (destructuring-bind (phone . passcode) secret
+      (signin phone passcode))))
+
+(defun get-user ()
+  (poller-call :revolut "user/current"))
+
+(defun get-user-portfolio ()
+  (poller-call :revolut "user/current/portfolio"))
+
+(defun get-user-features ()
+  (poller-call :revolut "user/current/features"))
+
+(defun get-user-wallet ()
+  (poller-call :revolut "user/current/wallet"))
+
+(defun get-user-money-boxes ()
+  (poller-call :revolut "user/current/money-boxes"))
+
+(defun get-my-card-all ()
+  (poller-call :revolut "my-card/all"))
+
+(defun get-currencies (&optional (type "fiat"))
+  "Type could be 'fiat', 'crypto' or 'commodity'"
+  (poller-call :revolut "currencies" :|type| type))
+
+(defun get-cashback ()
+  (poller-call :revolut "cashback"))
+
+(defun get-quote (symbol)
+  (poller-call :revolut "quote" :|symbol| symbol))
+
+(defun get-transactions-last (&key (count 10) pocket to)
+  (poller-call :revolut "user/current/transactions/last"
+               :|count| count
+               :|to| to
+               :|internalPocketId| pocket))
+
+(defun get-transactions-vault (&key id)
+  (poller-call :revolut "user/current/transactions/vault" :|id| id))
+
+(defun get-transaction (id)
+  (poller-call :revolut (format nil "transaction/~a" id)))
+
+(defun get-recurring-payments ()
+  (poller-call :revolut "recurring-payments"))
+
+(defparameter +unix-epoch-difference+
+  (encode-universal-time 0 0 0 1 1 1970 0))
+
+(defun unix-to-universal-time (unix-time)
+  (+ unix-time +unix-epoch-difference+))
+
+(defun universal-to-unix-time (universal-time)
+  (- universal-time +unix-epoch-difference+))
+
+(defun get-date (ms)
+  (unix-to-universal-time (round ms 1000)))
+
+(defun format-short-date (ms)
+  (if ms
+   (multiple-value-bind (sec min hour day month year)
+       (decode-universal-time (get-date ms))
+     (declare (ignore sec min hour))
+     (format nil "~4,'0D/~2,'0D/~2,'0D" year month day))
+   "-"))
+
+(defun get-amount (amount)
+  (/ amount 100))
+
+(defun format-pocket-account (p)
+  (when p         
+    (let ((cur (string-downcase (agets p "currency"))))
+      (concatenate 'string
+                   "assets:revolut:"
+                   (or (agets *accounts* (agets p "id"))
+                       (when (agets p "name") (string-downcase (agets p "name")))
+                       (when (equal "SAVINGS" (agets p "type"))
+                         (concatenate 'string "savings" cur))
+                       cur)))))
+
+(defun format-pocket-balance (p)
+  (format nil "; balance ~A ~A = ~,2F ~A"
+          (format-short-date (* 1000 (universal-to-unix-time (get-universal-time))))
+          (format-pocket-account p)
+          (get-amount (agets p "balance"))
+          (agets p "currency")))
+
+(defun format-entries (changes)
+  (text-chunks (mapcar #'pta-ledger:render (remove nil changes))))
+
+(defun find-pocket (id pockets)
+  (find id pockets :key (agetter "id") :test #'equal))
+
+(defun get-expense-account (tr)
+  (or (agets *merchant-expenses* (agets tr "merchant" "merchantId"))
+      (agets *category-expenses* (agets tr "category"))
+      (agets *tag-expenses* (agets tr "tag"))
+      (concatenate 'string "expenses:" (agets tr "tag"))))
+
+(defun params-exchange (tr pockets)
+  (when (equal "sell" (agets tr "direction"))
+    (let ((description (agets tr "description"))
+          (rcv-account (format-pocket-account (find-pocket
+                                               (agets tr "counterpart" "account" "id")
+                                               pockets)))
+          (rcv-amount (get-amount (agets tr "counterpart" "amount")))
+          (rcv-currency (agets tr "counterpart" "currency"))
+          (snd-account (format-pocket-account (find-pocket
+                                               (agets tr "account" "id") pockets)))
+          (snd-amount (get-amount (agets tr "amount")))
+          (snd-currency (agets tr "currency")))         
+      (values description nil
+              rcv-account rcv-amount rcv-currency
+              snd-account snd-amount snd-currency))))
+
+(defun params-topup (tr pockets)
+  (let* ((description (agets tr "description"))
+         (rcv-account (format-pocket-account (find-pocket
+                                              (agets tr "account" "id") pockets)))
+         (rcv-amount (get-amount (agets tr "amount")))
+         (rcv-currency (agets tr "currency"))
+         (snd-account (agets *topup-accounts* description))
+         (snd-amount (* -1 rcv-amount))
+         (snd-currency rcv-currency))         
+    (values description nil
+            rcv-account rcv-amount rcv-currency
+            snd-account snd-amount snd-currency)))
+
+(defun params-transfer (tr pockets)
+  (when (equal "CURRENT" (agets tr "account" "type"))
+    (let* ((description (agets tr "description"))
+           (snd-account (format-pocket-account (find-pocket (agets tr "account" "id")
+                                                            pockets)))
+           (snd-amount (get-amount (agets tr "amount")))
+           (snd-currency (agets tr "currency"))
+           (rcv-account (or (format-pocket-account (find-pocket
+                                                    (or
+                                                     (agets tr "recipient" "account" "id")
+                                                     (agets tr "sender" "account" "id"))
+                                                    pockets))
+                            (get-expense-account tr)))
+           (rcv-amount (* -1 snd-amount))
+           (rcv-currency snd-currency))
+      (values description nil
+              rcv-account rcv-amount rcv-currency
+              snd-account snd-amount snd-currency))))
+
+(defun params-atm (tr pockets)
+  (let* ((description (agets tr "description"))
+         (snd-account (format-pocket-account (find-pocket (agets tr "account" "id")
+                                                          pockets)))
+         (snd-amount (get-amount (agets tr "amount")))
+         (snd-currency (agets tr "currency"))
+         (rcv-currency (agets tr "counterpart" "currency"))
+         (rcv-amount (* -1 (get-amount (agets tr "counterpart" "amount"))))
+         (rcv-account (format nil "assets:cash:~(~a~)" rcv-currency)))         
+    (values description nil
+            rcv-account rcv-amount rcv-currency
+            snd-account snd-amount snd-currency)))
+
+(defun params-fee (tr pockets)
+  (let* ((description (agets tr "description"))
+         (rcv-account "expenses:banking:fee")
+         (rcv-amount (* -1 (get-amount (agets tr "amount"))))
+         (rcv-currency (agets tr "currency"))
+         (snd-account (format-pocket-account (find-pocket (agets tr "account" "id")
+                                                          pockets)))
+         (snd-amount (* -1 rcv-amount))
+         (snd-currency rcv-currency))         
+    (values description nil
+            rcv-account rcv-amount rcv-currency
+            snd-account snd-amount snd-currency)))
+
+(defun params-card-payment (tr pockets)
+  (let* ((merchant-name (agets tr "merchant" "name"))
+         (tr-description (agets tr "description"))
+         (description (or merchant-name tr-description))
+         (comment (unless (equal description tr-description) tr-description))
+         (rcv-account (get-expense-account tr))
+         (rcv-amount (get-amount (* -1 (agets tr "counterpart" "amount"))))
+         (rcv-currency (agets tr "counterpart" "currency"))
+         (snd-account (format-pocket-account (find-pocket (agets tr "account" "id")
+                                                          pockets)))
+         (snd-amount (get-amount (agets tr "amount")))
+         (snd-currency (agets tr "currency")))    
+    (values description comment
+            rcv-account rcv-amount rcv-currency
+            snd-account snd-amount snd-currency)))
+
+(defun transaction->entry (tr pockets)
+  (let* ((date (get-date (agets tr "startedDate")))
+         (type (keyify (agets tr "type"))))
+    (multiple-value-bind (description comment
+                          rcv-account rcv-amount rcv-currency
+                          snd-account snd-amount snd-currency)
+        (case type
+          (:exchange (params-exchange tr pockets))
+          (:topup (params-topup tr pockets))
+          (:atm (params-atm tr pockets))
+          (:transfer (params-transfer tr pockets))
+          (:fee (params-fee tr pockets))
+          (:card-payment (params-card-payment tr pockets)))
+      (when snd-account
+        (pta-ledger:make-entry
+         :date date
+         :description description
+         :comment comment
+         :postings (list
+                    (pta-ledger:make-posting
+                     :account rcv-account
+                     :amount (pta-ledger:make-amount
+                              :quantity rcv-amount
+                              :commodity rcv-currency)
+                     :total-price (unless (equal rcv-currency snd-currency)
+                                    (pta-ledger:make-amount
+                                     :quantity (abs snd-amount)
+                                     :commodity snd-currency)))
+                    (pta-ledger:make-posting
+                     :account snd-account
+                     :amount (pta-ledger:make-amount
+                              :quantity snd-amount
+                              :commodity snd-currency))))))))
+
+(defun handle-auth (login pass)
+  (handler-case
+      (progn
+        (poller-authenticate :revolut (cons login pass))
+        (handle-balance))
+    (poller-cant-authenticate ()
+      (bot-send-message "Чот не смог, пропробуй другие."))))
+
+(defun handle-balance ()
+  (bot-send-message
+   (handler-case
+       (let ((entries (mapcar 'format-pocket-balance (agets (get-user-wallet) "pockets"))))
+         (if entries (text-chunks entries) "Не нашлось"))
+     (poller-no-secret () "Нужен логин-пароль. /revolut <phone> <pin>")
+     (poller-cant-get-token () "Не смог получить данные. Попробуй перелогинься. /revolut <phone> <pin>"))
+   :parse-mode "markdown"))
+
+(defun prepare-entries (transactions)
+  (let ((pockets (agets (get-user-wallet) "pockets")))
+    (delete nil (mapcar (lambda (tr) (transaction->entry tr pockets)) transactions))))
+
+(defun handle-recent (&optional (count 10))
+  (bot-send-message
+   (handler-case
+       (format-entries (prepare-entries (get-transactions-last :count count)))
+     (poller-no-secret () "Нужен логин-пароль. /revolut <phone> <pin>")
+     (poller-cant-get-token () "Не смог получить данные. Попробуй перелогинься. /revolut <phone> <pin>"))
+   :parse-mode "markdown"))
+
+(def-message-cmd-handler handle-cmd-revolut (:revolut :revo)
+  (let ((a0 (car *args*)))
+    (cond
+      ((= 2 (length *args*)) (apply 'handle-auth *args*))
+      ((or (null *args*) (equal a0 "bal")) (handle-balance))
+      (:otherwise (handle-recent (parse-integer a0 :junk-allowed t))))))
+
+
+(defun process-new (changes)
+  (let ((ledger-package (find-package :chatikbot.plugins.ledger))
+        (transactions (prepare-entries changes)))
+    (if ledger-package
+        (let ((new-chat-entry (symbol-function
+                               (intern "LEDGER/NEW-CHAT-ENTRY" ledger-package))))
+          (dolist (entry transactions)
+            (funcall new-chat-entry *chat-id* (pta-ledger:clone-entry entry))))
+        (bot-send-message (format-entries transactions) :parse-mode "markdown"))))
+
+(defcron process-revolut ()
+  (poller-poll-lists :revolut
+                     #'get-transactions-last
+                     #'process-new
+                     :key (agetter "startedDate")))

+ 1 - 1
poller.lisp

@@ -61,8 +61,8 @@
         (with-secret (secret (list module chat-id))
           (unless secret (error 'poller-no-secret))
           (let ((*poller-token* (poller-get-token module secret)))
-            (unless *poller-token* (error 'poller-cant-get-token))
             (set-data *tokens* chat-id *poller-token* module)
+            (unless *poller-token* (error 'poller-cant-get-token))
             (values (apply 'poller-request module method params)))))))
 
 (defun poller-authenticate (module secret)