zhanna.lisp 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. (in-package :cl-user)
  2. (defpackage chatikbot.plugins.zhanna
  3. (:use :cl :chatikbot.common :chatikbot.gsheets))
  4. (in-package :chatikbot.plugins.zhanna)
  5. (defsetting *zhanna-token-id* nil)
  6. (defsetting *zhanna-sheet-id* "1kLBodFUwcfbpdqe_d2d01MHij95NAVcKrmpjotYsUQk")
  7. (defun %zh-parse-values (rows)
  8. (labels ((non-empty (s)
  9. (unless (equal s "") s))
  10. (get-el (row idx)
  11. (when (> (length row) idx)
  12. (non-empty (elt row idx))))
  13. (get-item (row start)
  14. (cons (get-el row start) (get-el row (1+ start)))))
  15. (loop for start to 4 by 2
  16. append (loop for row in rows
  17. for y from 0
  18. for item = (get-item row start)
  19. when (car item)
  20. collect (list (subseq (car item) 0 5) (cdr item) (cons start y))))))
  21. (defparameter +zhanna-dows+ '("вс" "пн" "вт" "ср" "чт" "пт" "сб"))
  22. (defun zhanna-get-schedule (&rest days)
  23. (labels ((dow-range (dow)
  24. (format nil "'~A'!A3:F30" (elt +zhanna-dows+ dow))))
  25. (let* ((ranges (mapcar #'dow-range days))
  26. (resp (gsheets-get-sheet-values *zhanna-token-id* *zhanna-sheet-id* ranges)))
  27. (loop for vr in (aget "valueRanges" resp)
  28. collect (cons (subseq (aget "range" vr) 1 3)
  29. (%zh-parse-values (aget "values" vr)))))))
  30. (defun zhanna-set-schedule (token-id name dow time &optional (count 1))
  31. (let* ((schedule (cdar (zhanna-get-schedule dow)))
  32. (start (member time schedule :key #'car :test #'equal))
  33. (positions (loop for (time name pos) in start
  34. while (null name)
  35. collect pos)))
  36. (cond
  37. ((null schedule) "Плохой день")
  38. ((null start) "Плохое время")
  39. ((< (length positions) count) (format nil "~[В ~A занято~:;Есть только ~:*~A с ~A~]"
  40. (length positions) time))
  41. (:otherwise
  42. (labels ((range-value (pos)
  43. (cons
  44. (format nil "'~A'!~A~A"
  45. (elt +zhanna-dows+ dow)
  46. (code-char (+ (car pos)
  47. (char-code #\B)))
  48. (+ 3 (cdr pos)))
  49. (list (list name)))))
  50. (let* ((resp (gsheets-put-sheet-values token-id *zhanna-sheet-id*
  51. (mapcar #'range-value (subseq positions 0 count))))
  52. (err (aget "error" resp)))
  53. (when (equal 403 (aget "code" err))
  54. "Нет сил писать")))))))
  55. (defun %zh-diff-schedule (old-s new-s)
  56. (let ((times (mapcar #'car (union old-s new-s :key #'car :test #'equal))))
  57. (loop for time in times
  58. for new = (car (aget time new-s))
  59. for old = (car (aget time old-s))
  60. unless (equal new old)
  61. collect (list (cons new old) time))))
  62. (defun %zh-format-diff (diff)
  63. (labels ((join (joined value)
  64. (destructuring-bind (change time) value
  65. (let ((existing (aget change joined)))
  66. (if existing
  67. (progn
  68. (setf (cdr existing) (append (cdr existing) (list time)))
  69. joined)
  70. (append joined (list value))))))
  71. (text-change (change)
  72. (destructuring-bind (new . old) change
  73. (cond
  74. ((null old) (format nil "🏄 ~A" new))
  75. ((null new) (format nil "💔 ~A" old))
  76. (:otherwise (format nil "~A 🔁 ~A" old new))))))
  77. (let ((joined (reduce #'join diff :initial-value (list))))
  78. (format nil "~{~A~^~%~}" (loop for (change . times) in joined
  79. collect (format nil "~A: ~{~A~^, ~}" (text-change change) times))))))
  80. (defsetting *zhanna-subscriptions* nil "chat-ids of schedule changes receivers")
  81. (defvar *zhanna-last-schedule* nil "Schedule from last cron run")
  82. (defcron process-zhanna ()
  83. (alexandria:when-let ((schedule (zhanna-get-schedule 1 2 3 4 5 6 0)))
  84. (when *zhanna-last-schedule*
  85. (handler-case
  86. (let* ((day-changes (loop for dow in +zhanna-dows+
  87. for new-s = (aget dow schedule)
  88. for old-s = (aget dow *zhanna-last-schedule*)
  89. for diff = (%zh-diff-schedule old-s new-s)
  90. when diff
  91. collect (format nil "*~A:*~%~A" dow (%zh-format-diff diff))))
  92. (msg (format nil "У Жанны сменились планы~%~{~A~^~%~}" day-changes)))
  93. (when day-changes
  94. (log:info msg)
  95. (dolist (chat-id *zhanna-subscriptions*)
  96. (bot-send-message chat-id msg :parse-mode "markdown"))))
  97. (error (e) (log:error e))))
  98. (setf *zhanna-last-schedule* schedule)
  99. (values)))
  100. (def-message-cmd-handler zhanna-free-handler (:free :свободна :свободно)
  101. (let* ((dows (if args (remove nil (mapcar
  102. #'(lambda (a) (position a +zhanna-dows+ :test #'equal))
  103. args))
  104. (list (mod (1+ (local-time:timestamp-day-of-week (local-time:now))) 7))))
  105. (schedule (apply #'zhanna-get-schedule dows))
  106. (texts (loop for (dow . day-schedule) in schedule
  107. collect (format nil "*~A*~%~{~A~^, ~}" dow
  108. (mapcar #'car (remove nil day-schedule :key #'second :test-not #'eql))))))
  109. (bot-send-message chat-id (format nil "Жанна свободна:~%~{~A~^~%~}" texts) :parse-mode "markdown")))
  110. (def-message-cmd-handler zhanna-set-handler (:set :записать)
  111. (if (null (gsheets-get-tokens from-id))
  112. (%gsheets-send-auth chat-id from-id)
  113. (handler-case
  114. (destructuring-bind (name day time &optional (sets "1")) args
  115. (let ((dow (position day +zhanna-dows+ :test #'equal))
  116. (count (parse-integer sets :junk-allowed t)))
  117. (cond
  118. ((null dow) (bot-send-message chat-id "День надо кратко: 'пн', 'вт', 'ср' и т.д"))
  119. ((null count) (bot-send-message chat-id "Число сетов - числом!"))
  120. (:otherwise
  121. (let ((err (zhanna-set-schedule from-id name dow time count)))
  122. (bot-send-message chat-id (or err "Записал!")))))))
  123. (error (e)
  124. (log:info e)
  125. (bot-send-message chat-id "Надо: /set <имя> <день> <время> [кол-во]")))))