nalunch.lisp 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. (in-package :cl-user)
  2. (defpackage chatikbot.plugins.nalunch
  3. (:use :cl :chatikbot.common))
  4. (in-package :chatikbot.plugins.nalunch)
  5. (defparameter +api-uri+ "https://www.nalunch.ru/api/" "Nalunch API base url")
  6. (defvar *calend* nil "Working calendar exceptions")
  7. (defparameter +basicdata-calend-url+ "http://basicdata.ru/api/json/calend/")
  8. ;; poller methods
  9. (defmethod poller-request ((module (eql :nalunch)) method &rest params)
  10. (handler-case
  11. (json-request (concatenate 'string +api-uri+ method)
  12. :parameters (rest-parameters params)
  13. :headers (filled `(("X-AUTH-SIGN" . ,*poller-token*))))
  14. (dex:http-request-failed (e) e)))
  15. (defmethod poller-validate ((module (eql :nalunch)) response)
  16. (not (typep response 'dex:http-request-failed)))
  17. (defmethod poller-authenticate ((module (eql :nalunch)) secret)
  18. (destructuring-bind (username . password) secret
  19. (agets (poller-request :nalunch "auth" :username username :password password)
  20. "token")))
  21. ;; API
  22. (defun user-profile ()
  23. (poller-call :nalunch "user/profile"))
  24. (defun user-balance ()
  25. (poller-call :nalunch "user/balance"))
  26. (defun get-transactions (&optional month)
  27. (let ((month (or month (format-ts (local-time:now)))))
  28. (poller-call :nalunch "transactions/GetCHTransactions" :from month)))
  29. (defun get-catering-points (lat lon &key page size)
  30. (poller-call :nalunch "cateringpoint/getpointlist" :latitude lat :longitude lon :page page "pageSize" size))
  31. (defsetting *currency* "RUB")
  32. (defsetting *expense-account* "expenses:Food:Work")
  33. (defsetting *liabilities-account* "liabilities:nalunch")
  34. ;; Bot
  35. (defun get-calend (year)
  36. (setf year (princ-to-string year))
  37. (unless (aget year *calend*)
  38. (setf *calend* (aget "data" (json-request +basicdata-calend-url+))))
  39. (aget year *calend*))
  40. (defun get-working-days (year month)
  41. (let* ((exceptions (aget (princ-to-string month) (get-calend year)))
  42. (days-in-month (local-time:days-in-month month year)))
  43. (loop for day from 1 upto days-in-month
  44. for ts = (local-time:encode-timestamp 0 0 0 0 day month year)
  45. for dof = (local-time:timestamp-day-of-week ts)
  46. for exc = (aget "isWorking" (aget (princ-to-string day) exceptions))
  47. when (or (and (<= 1 dof 5)
  48. (not (equal 2 exc)))
  49. (and (or (= dof 0) (= dof 6))
  50. (or (equal 0 exc)
  51. (equal 3 exc))))
  52. collect day)))
  53. (defun format-balance-left (balance)
  54. (let* ((balance (aget "sum" balance))
  55. (now (local-time:now))
  56. (left-working-days (length (remove-if #'(lambda (d) (<= d (local-time:timestamp-day now)))
  57. (get-working-days (local-time:timestamp-year now)
  58. (local-time:timestamp-month now))))))
  59. (format nil "🍴 Баланс ~A руб~@[ на ~A дней, по ~$ руб~]."
  60. balance left-working-days (/ balance (max left-working-days 1)))))
  61. (defun format-entries (changes)
  62. (text-chunks (mapcar #'pta-ledger:render changes)))
  63. (defun flat-transactions (transactions)
  64. (loop for day in transactions
  65. appending (agets day "transactionList")))
  66. (defun transaction->entry (tr)
  67. (let* ((pta-ledger (find-package :pta-ledger))
  68. (make-entry (symbol-function (intern "MAKE-ENTRY" pta-ledger)))
  69. (make-posting (symbol-function (intern "MAKE-POSTING" pta-ledger)))
  70. (make-amount (symbol-function (intern "MAKE-AMOUNT" pta-ledger)))
  71. (date (local-time:timestamp-to-universal
  72. (local-time:parse-timestring (agets tr "time"))))
  73. (payee (agets tr "catPointName"))
  74. (amount (agets tr "sum")))
  75. (funcall make-entry
  76. :date date
  77. :description payee
  78. :postings (list
  79. (funcall make-posting
  80. :account *expense-account*
  81. :amount (funcall make-amount
  82. :quantity amount
  83. :commodity *currency*))
  84. (funcall make-posting
  85. :account *liabilities-account*
  86. :amount (funcall make-amount
  87. :quantity (* -1 amount)
  88. :commodity *currency*))))))
  89. (defun transactions->entries (transactions)
  90. (mapcar #'transaction->entry (flat-transactions transactions)))
  91. (defun process-new (diff)
  92. (let ((ledger-package (find-package :chatikbot.plugins.ledger)))
  93. (if ledger-package
  94. (let ((new-chat-entry (symbol-function
  95. (intern "LEDGER/NEW-CHAT-ENTRY" ledger-package))))
  96. (dolist (tr diff)
  97. (funcall new-chat-entry *chat-id* tr)))
  98. (bot-send-message (format-entries diff)))))
  99. ;; Cron
  100. (defcron process-nalunch (:minute '(member 0 10 20 30 40 50))
  101. (poller-poll-lists :nalunch
  102. #'(lambda () (transactions->entries (get-transactions)))
  103. #'(lambda (diff)
  104. (bot-send-message (format-balance-left (user-balance)))
  105. (process-new diff))
  106. :key #'pta-ledger:entry-date))
  107. ;; Hooks
  108. (defun handle-set-cron (enable)
  109. (lists-set-entry :nalunch *chat-id* enable)
  110. (bot-send-message (if enable
  111. "Включил рассылку. '/nalunch off' чтобы выключить, /nalunch - показать последние."
  112. "Без рассылки. '/nalunch on' - включить, /nalunch - последние.")))
  113. (defun handle-auth (login pass)
  114. (let ((secret (cons login pass)))
  115. (unless (poller-authenticate :nalunch secret)
  116. (bot-send-message "Чот не смог, пропробуй другие."))
  117. (secret-set `(:nalunch ,*chat-id*) secret)
  118. (handle-set-cron t)))
  119. (defun handle-recent (&optional month)
  120. (bot-send-message (format-balance-left (user-balance)))
  121. (bot-send-message
  122. (handler-case
  123. (let ((transactions (get-transactions month)))
  124. (format-entries (transactions->entries transactions)))
  125. (poller-no-secret () "Нужен логин-пароль. /nalunch <login> <pass>")
  126. (poller-cant-authenticate () "Не смог получить данные. Попробуй перелогинься. /nalunch <login> <pass>"))
  127. :parse-mode "markdown"))
  128. (def-message-cmd-handler handle-cmd-nalunch (:nalunch)
  129. (let ((a0 (car *args*)))
  130. (cond
  131. ((= 2 (length *args*)) (apply 'handle-auth *args*))
  132. ((and (= 1 (length *args*)) (or (equal "on" a0) (equal "off" a0)))
  133. (handle-set-cron (equal "on" a0)))
  134. ((equal a0 "bal") (bot-send-message (format-balance-left (user-balance))))
  135. (:otherwise (handle-recent (spaced *args*))))))