浏览代码

[ledger] git journal support

Innocenty Enikeew 8 年之前
父节点
当前提交
dff8b2abe1
共有 1 个文件被更改,包括 133 次插入64 次删除
  1. 133 64
      plugins/ledger.lisp

+ 133 - 64
plugins/ledger.lisp

@@ -4,79 +4,152 @@
 (in-package :chatikbot.plugins.ledger)
 
 (eval-when (:compile-toplevel :load-toplevel :execute)
-  (ql:quickload :pta-ledger))
+  (ql:quickload '(:pta-ledger :legit)))
 
 (defsetting *ledger/default-timezone* -3 "Default timezone for time display. GMT+3")
 (defvar *ledger/chat-journals* (make-hash-table))
 
-(defun ledger/get-hook-url (chat-id url)
-  (declare (ignore url))
-  (get-webhook-url "ledger" chat-id (token-hmac (write-to-string chat-id))))
+(defvar *git-repo-locks* (make-hash-table :test #'equal))
+(defun git-get-repo-lock (repo)
+  (let ((key (legit:location repo)))
+    (setf key
+          (etypecase key
+            (string key)
+            (pathname (namestring key))))
+    (or (gethash key *git-repo-locks*)
+        (setf (gethash key *git-repo-locks*)
+              (bt:make-recursive-lock key)))))
 
-(defun ledger/parse-uri (chat-id uri)
-  (setf (gethash chat-id *ledger/chat-journals*)
-        (cons (pta-ledger:parse-journal (http-request uri))
-              (get-universal-time))))
+(defun git-get-repo (location remote)
+  (let ((repo (make-instance 'legit:repository :location location)))
+    (bt:with-recursive-lock-held ((git-get-repo-lock repo))
+      (legit:init repo :remote remote :if-does-not-exist :clone))
+    repo))
+
+(defsetting *git-repos-root* "/tmp/ledger-repos/")
+(defun git-get-chat-location (chat-id remote)
+  (merge-pathnames (format nil "~A-~A" chat-id (token-hmac remote))
+                   *git-repos-root*))
+
+(defun git-read-latest-file (repo path)
+  (bt:with-recursive-lock-held ((git-get-repo-lock repo))
+    (legit:fetch repo :branch "master" :remote "origin")
+    (legit:reset repo :hard t :to "origin/master")
+    (uiop:read-file-string (merge-pathnames path
+                                            (uiop:ensure-directory-pathname
+                                             (legit:location repo))))))
+
+(defun ledger/refresh-git (chat-id remote path)
+  (let* ((location (git-get-chat-location chat-id remote))
+         (repo (git-get-repo location remote))
+         (content (git-read-latest-file repo path))
+         (journal (pta-ledger:parse-journal content))
+         (updated (legit:current-age repo)))
+    (setf (gethash chat-id *ledger/chat-journals*)
+          (cons journal updated))))
+
+(defun git-append-latest-file (repo path text message)
+  (bt:with-recursive-lock-held ((git-get-repo-lock repo))
+    (let ((repo-path (merge-pathnames path (legit:location repo))))
+      (dotimes (tries 5)
+        (let ((current (or (ignore-errors (git-read-latest-file repo path)) "")))
+          (uiop/stream:with-output-file (s repo-path
+                                           :if-exists :supersede
+                                           :if-does-not-exist :create)
+            (format s "~A~A~%" current text)))
+        (legit:add repo repo-path)
+        (legit:commit repo message)
+        (handler-case
+            (progn
+              (legit:push repo)
+              (return-from git-append-latest-file path))
+          (legit:git-error ())))
+      (error "Tried 5 times to push ~A to ~A" path (legit:remote-url repo)))))
+
+(defun ledger/get-hook-url (chat-id)
+  (get-webhook-url "ledger" chat-id (token-hmac (write-to-string chat-id))))
 
 (defun ledger/format-uri (url)
   (let ((uri (quri:uri url)))
     (quri:render-uri (quri:make-uri :userinfo (when (quri:uri-userinfo uri) "*:*")
                                     :defaults url))))
 
-(defun ledger/handle-set-uri (chat-id uri)
+(defun ledger/format-time (universal-time)
+  (when universal-time
+    (multiple-value-bind (sec min hour day month year dow dst-p tz)
+        (decode-universal-time universal-time *ledger/default-timezone*)
+      (declare (ignore dow dst-p tz))
+      (format nil "~4,'0D-~2,'0D-~2,'0D ~2,'0D:~2,'0D:~2,'0D"
+              year month day hour min sec))))
+
+(defun ledger/refresh-uri (chat-id uri)
+  (setf (gethash chat-id *ledger/chat-journals*)
+        (cons (pta-ledger:parse-journal (http-request uri))
+              (get-universal-time))))
+
+(defun get-chat-journal-info (chat-id &optional info)
+  (labels ((process-info (info)
+             (cond
+               ((stringp info) (ledger/refresh-uri chat-id info))
+               ((consp info) (apply #'ledger/refresh-git chat-id info))
+               (:otherwise nil))))
+    (let ((journal-info (gethash chat-id *ledger/chat-journals*)))
+      (if journal-info journal-info
+          (if info (process-info info)
+              (with-secret (info (list :ledger chat-id))
+                (process-info info)))))))
+
+(Defun ledger/handle-info (chat-id)
+  (with-secret (info (list :ledger chat-id))
+    (if info
+        (destructuring-bind (journal . ut)
+            (get-chat-journal-info chat-id info)
+          (let ((uri (cond
+                       ((consp info) (car info))
+                       ((stringp info) info))))
+            (bot-send-message chat-id (format nil "Журнал с ~D записями, из ~A, обновлён ~A.~%Веб-хук: ~A"
+                                              (length journal)
+                                              (ledger/format-uri uri)
+                                              (ledger/format-time ut)
+                                              (ledger/get-hook-url chat-id))
+                              :disable-web-preview t)))
+        (send-response chat-id "Добавь журнал: uri - /ledger <url>; git - /ledger <remote> <path>"))))
+
+(defun ledger/handle-set-info (chat-id info)
+  (setf (gethash chat-id *ledger/chat-journals*) nil)
   (handler-case
       (destructuring-bind (journal . ut)
-          (ledger/parse-uri chat-id uri)
+          (get-chat-journal-info chat-id info)
         (declare (ignore ut))
-        (secret-set (list :ledger chat-id) uri)
+        (secret-set (list :ledger chat-id) info)
         (send-response chat-id (format nil "Добавил журнал с ~D записями. Веб-хук для обновления: ~A"
                                        (length journal)
-                                       (ledger/get-hook-url chat-id uri))))
+                                       (ledger/get-hook-url chat-id))))
     (pta-ledger:journal-failed (e)
       (send-response chat-id (format nil "Не смог спарсить: ~A" e)))
     (dex:http-request-failed (e)
       (send-response chat-id (format nil "Не смог в урл: ~A" (dex:response-body e))))))
 
-(defun ledger/format-time (universal-time)
-  (when universal-time
-    (multiple-value-bind (sec min hour day month year dow dst-p tz)
-        (decode-universal-time universal-time *ledger/default-timezone*)
-      (declare (ignore dow dst-p tz))
-      (format nil "~4,'0D-~2,'0D-~2,'0D ~2,'0D:~2,'0D:~2,'0D"
-              year month day hour min sec))))
-
-(defun ledger/handle-info (chat-id)
-  (with-secret (uri (list :ledger chat-id))
-    (if uri
-        (destructuring-bind (journal . ut)
-            (or (gethash chat-id *ledger/chat-journals*)
-                (ledger/parse-uri chat-id uri))
-          (bot-send-message chat-id (format nil "Журнал с ~D записями, из ~A, обновлён ~A.~%Веб-хук: ~A"
-                                            (length journal)
-                                            (ledger/format-uri uri)
-                                            (ledger/format-time ut)
-                                            (ledger/get-hook-url chat-id uri))
-                            :disable-web-preview t))
-        (send-response chat-id "Добавь урл журнала, /ledger <url>"))))
 
 (def-message-cmd-handler handler-ledger (:ledger)
   (cond
-    ((= 1 (length args)) (ledger/handle-set-uri chat-id (car args)))
+    ((<= 1 (length args) 2) (ledger/handle-set-info chat-id args))
     (:otherwise (ledger/handle-info chat-id))))
 
+(defmacro with-chat-journal ((chat-id journal updated) &body body)
+  (let ((info (gensym "info")))
+    `(let ((,info (get-chat-journal-info ,chat-id)))
+       (if ,info
+           (destructuring-bind (,journal . ,updated) ,info
+             ,@body)
+           (send-response ,chat-id "Добавь журнал: uri - /ledger <url>; git - /ledger <remote> <path>")))))
+
 (defun ledger/handle-balance (chat-id query)
-  (let ((pair (gethash chat-id *ledger/chat-journals*)))
-    (if pair
-        (destructuring-bind (journal . ut) pair
-          (bot-send-message chat-id (format nil "```~%~A~%```Обновлено: ~A"
-                                            (pta-ledger:journal-balance journal query)
-                                            (ledger/format-time ut))
-                            :parse-mode "markdown"))
-        (with-secret (uri (list :ledger chat-id))
-          (if uri
-              (progn (ledger/parse-uri chat-id uri)
-                     (ledger/handle-balance chat-id query))
-              (send-response chat-id "Добавь урл журнала, /ledger <url>"))))))
+  (with-chat-journal (chat-id journal updated)
+    (bot-send-message chat-id (format nil "```~%~A~%```Обновлено: ~A"
+                                      (pta-ledger:journal-balance journal query)
+                                      (ledger/format-time updated))
+                              :parse-mode "markdown")))
 
 (def-message-cmd-handler handler-balance (:balance :bal)
   (cond
@@ -84,21 +157,14 @@
     (:otherwise (ledger/handle-balance chat-id (spaced args)))))
 
 (defun ledger/handle-journal (chat-id query)
-  (let ((pair (gethash chat-id *ledger/chat-journals*)))
-    (if pair
-        (destructuring-bind (journal . ut) pair
-          (let* ((entries (pta-ledger:journal-print journal query))
-                 (len (length entries))
-                 (count (min len 20)))
-            (bot-send-message chat-id (format nil "```~%~{~A~^ ~%~%~}```Обновлено: ~A"
-                                              (subseq entries (- len count) len)
-                                              (ledger/format-time ut))
-                              :parse-mode "markdown")))
-        (with-secret (uri (list :ledger chat-id))
-          (if uri
-              (progn (ledger/parse-uri chat-id uri)
-                     (ledger/handle-balance chat-id query))
-              (send-response chat-id "Добавь урл журнала, /ledger <url>"))))))
+  (with-chat-journal (chat-id journal updated)
+    (let* ((entries (pta-ledger:journal-print journal query))
+           (len (length entries))
+           (count (min len 20)))
+      (bot-send-message chat-id (format nil "```~%~{~A~^ ~%~%~}```Обновлено: ~A"
+                                        (subseq entries (- len count) len)
+                                        (ledger/format-time updated))
+                        :parse-mode "markdown"))))
 
 (def-message-cmd-handler handler-journal (:journal)
   (cond
@@ -110,6 +176,9 @@
     (destructuring-bind (chat-id hmac) paths
       (let ((true-hmac (token-hmac chat-id)))
         (when (string= true-hmac hmac)
-          (with-secret (uri (list :ledger chat-id))
-            (when uri
-              (ledger/parse-uri (parse-integer chat-id) uri))))))))
+          (with-secret (info (list :ledger chat-id))
+            (when info
+              (let ((chat-id (parse-integer chat-id)))
+                (setf (gethash chat-id *ledger/chat-journals*) nil)
+                (get-chat-journal-info chat-id info)
+                "OK"))))))))