transmission.lisp 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. (in-package #:chatikbot)
  2. (defsetting *transmission-settings* nil "ALIST of (chat-id . url)")
  3. (defsetting *transmission-locations* nil "ALIST of (name . location)")
  4. (defsetting *transmission-monitor* nil "LIST of chat-ids where to send status updates")
  5. (defvar *transmission-sessions* nil "ALIST of (url . x-transmission-session-id)")
  6. (defun transmission-get-session (url)
  7. (multiple-value-bind (body status headers)
  8. (handler-bind ((dex:http-request-conflict #'dex:ignore-and-continue))
  9. (http-request url))
  10. (declare (ignore body status))
  11. (gethash "x-transmission-session-id" headers)))
  12. (defun transmission-set-session (url session-id)
  13. (setf *transmission-sessions*
  14. (cons (cons url session-id)
  15. (remove url *transmission-sessions* :test #'equal :key #'car))))
  16. (defun transmission-request (url method &rest arguments)
  17. (let ((retries (getf arguments :retries 0)))
  18. (when (> retries 5)
  19. (error "Too many retries"))
  20. (remf arguments :retries)
  21. (let ((session-id (aget url *transmission-sessions*))
  22. (content
  23. (with-output-to-string (stream)
  24. (yason:encode (alexandria:plist-hash-table
  25. (list "method" (dekeyify method t)
  26. "arguments" (alexandria:plist-hash-table
  27. (loop for (key value) on arguments by #'cddr
  28. when value
  29. appending (list (dekeyify key) value)))))
  30. stream))))
  31. (handler-case
  32. (let* ((response (json-request url :method :post :content content
  33. :headers (list (cons :x-transmission-session-id session-id))))
  34. (result (aget "result" response)))
  35. (unless (equal "success" result)
  36. (error result))
  37. (aget "arguments" response))
  38. (dex:http-request-conflict (e)
  39. (transmission-set-session url (gethash "x-transmission-session-id" (dex:response-headers e)))
  40. (apply #'transmission-request url method :retries (1+ retries) arguments))))))
  41. (defun transmission-get-torrents (url &optional ids (fields '("id" "name" "status" "percentDone" "eta" "totalSize")))
  42. (aget "torrents" (transmission-request url :torrent-get :ids ids :fields fields)))
  43. (defun transmission-add-torrent (url &key filename metainfo)
  44. (let ((torrent-added (transmission-request url :torrent-add :filename filename :metainfo metainfo)))
  45. (car (transmission-get-torrents url (list (aget "id" (or (aget "torrent-added" torrent-added)
  46. (aget "torrent-duplicate" torrent-added))))))))
  47. (defun %format-torrent-status (status)
  48. (case status
  49. (0 "⏹") ;; Stopped
  50. (1 "🔂") ;; Check wait
  51. (2 "🔁") ;; Check
  52. (3 "⏸") ;; Queued to download
  53. (4 "▶️") ;; Downloading
  54. (5 "⏸") ;; Queued to seed
  55. (6 "⏺") ;; Seeding
  56. ))
  57. (defun %format-torrent (torrent)
  58. (let ((eta (aget "eta" torrent)))
  59. (format nil "~A ~A ~A (~A)~:[~; (~A%~@[, eta ~A~])~]"
  60. (aget "id" torrent)
  61. (%format-torrent-status (aget "status" torrent))
  62. (aget "name" torrent)
  63. (format-size (aget "totalSize" torrent))
  64. (eq 4 (aget "status" torrent))
  65. (smart-f (* 100 (aget "percentDone" torrent)) 1)
  66. (when (not (= -1 eta)) (format-interval eta)))))
  67. ;; Command handlers
  68. (def-message-cmd-handler handle-torrents (:torrents)
  69. (let ((url (aget chat-id *transmission-settings*)))
  70. (if url
  71. (let ((torrents (transmission-get-torrents url)))
  72. (bot-send-message chat-id
  73. (if torrents (format nil "~{~A~^~%~}" (mapcar #'%format-torrent torrents))
  74. "Пустой список!")))
  75. (bot-send-message chat-id "Бота настрой!"))))
  76. (defun %torrent-add-and-respond (chat-id &key filename metainfo)
  77. (alexandria:when-let (url (aget chat-id *transmission-settings*))
  78. (let* ((new-torrent (transmission-add-torrent url :filename filename :metainfo metainfo))
  79. (markup (loop for (name . location) in *transmission-locations*
  80. collect (list :text (format nil "💾 ~A" name)
  81. :callback-data (encode-callback-data
  82. chat-id :tm
  83. (format nil "l-~A-~A" name
  84. (aget "id" new-torrent))
  85. 86400)))))
  86. (bot-send-message chat-id (%format-torrent new-torrent)
  87. :reply-markup (and markup (telegram-inline-keyboard-markup (list markup)))))))
  88. (defparameter +magnet-regex+ (ppcre:create-scanner "magnet:\\?\\S+"))
  89. (def-message-handler handle-magnet (message)
  90. (alexandria:when-let (magnet (ppcre:scan-to-strings +magnet-regex+ text))
  91. (%torrent-add-and-respond chat-id :filename magnet)))
  92. (def-message-handler handle-torrent (message)
  93. (alexandria:when-let* ((url (aget chat-id *transmission-settings*))
  94. (doc (aget "document" message))
  95. (file-name (aget "file_name" doc))
  96. (file-id (aget "file_id" doc)))
  97. (when (and (equal "torrent" (pathname-type (pathname file-name)))
  98. (< (aget "file_size" doc) (* 512 1024)))
  99. (%torrent-add-and-respond chat-id :metainfo (cl-base64:usb8-array-to-base64-string
  100. (telegram-file-contents file-id))))))
  101. ;; Callback handlers
  102. (defun %handle-torrent-move (query-id chat-id torrent-id name)
  103. (alexandria:when-let* ((url (aget chat-id *transmission-settings*))
  104. (location (aget name *transmission-locations*)))
  105. (transmission-request url :torrent-set-location
  106. :ids torrent-id
  107. :location location
  108. :move t)
  109. (telegram-answer-callback-query query-id :text (format nil "Moved to '~A'" location))))
  110. (def-callback-section-handler cb-handle-tm (:tm)
  111. (destructuring-bind (type val id)
  112. (split-sequence:split-sequence #\- data :count 3)
  113. (case (intern (string-upcase type) "KEYWORD")
  114. (:l (%handle-torrent-move query-id chat-id (parse-integer id) val)))))
  115. ;; Cron
  116. (defvar *transmission-last-results* (make-hash-table) "Last check results for each chat-id")
  117. (defcron process-transmission ()
  118. (dolist (chat-id *transmission-monitor*)
  119. (alexandria:when-let (url (aget chat-id *transmission-settings*))
  120. (let ((old-result (gethash chat-id *transmission-last-results*))
  121. (new-result (loop for torrent in (transmission-get-torrents url nil '("id" "status"))
  122. collect (cons (aget "id" torrent) (aget "status" torrent)))))
  123. (when old-result
  124. (alexandria:when-let (updated (mapcar #'car (set-difference new-result old-result :test #'equal)))
  125. (bot-send-message chat-id (format nil "~{~A~^~%~}"
  126. (mapcar #'%format-torrent
  127. (transmission-get-torrents url updated))))))
  128. (setf (gethash chat-id *transmission-last-results*) new-result)))))