1
0

vk.lisp 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. (in-package #:chatikbot)
  2. (defparameter +vk-api-url+ "https://api.vk.com/method/~A?v=5.40" "VK.com API endpoint")
  3. (defun %vk-api-call (method &optional args)
  4. (handler-case
  5. (bordeaux-threads:with-timeout (5)
  6. (let* ((params (loop for (k . v) in args
  7. when v
  8. collect (cons
  9. (princ-to-string k)
  10. (princ-to-string v))))
  11. (response (json-request (format nil +vk-api-url+ method)
  12. :method :post
  13. :parameters params)))
  14. (when (aget "error" response)
  15. (error (aget "error_msg" (aget "error" response))))
  16. (aget "response" response)))
  17. (bordeaux-threads:timeout (e)
  18. (declare (ignore e))
  19. (error "Timeout"))))
  20. (defun vk-wall-get (&key owner-id domain offset count filter extended fields)
  21. (%vk-api-call "wall.get"
  22. `(("owner_id" . ,owner-id)
  23. ("domain" . ,domain)
  24. ("offset" . ,offset)
  25. ("count" . ,count)
  26. ("filter" . ,filter)
  27. ("extended" . ,extended)
  28. ("fields" . ,fields))))
  29. (defun vk-get-user-name (id)
  30. (let ((r (first (%vk-api-call "users.get" `(("user_ids" . ,id))))))
  31. (format nil "~A ~A" (aget "first_name" r) (aget "last_name" r))))
  32. (defun vk-get-group-name (id)
  33. (aget "name" (first (%vk-api-call "groups.getById"
  34. `(("group_id" . ,(if (numberp id)
  35. (- id)
  36. id)))))))
  37. (defun vk-get-name (id)
  38. (if (and (numberp id) (> id 0))
  39. (vk-get-user-name id)
  40. (vk-get-group-name id)))
  41. ;; Cron
  42. (defun process-walls ()
  43. (handler-case
  44. (loop for (domain last-id next-fetch period) in (db-vk-get-active-walls)
  45. when (or (null next-fetch)
  46. (local-time:timestamp> (local-time:now) (local-time:unix-to-timestamp next-fetch)))
  47. do (progn
  48. (log:info "Fetching wall" domain)
  49. (handler-case
  50. (let ((new-posts
  51. (remove last-id (reverse (aget "items" (vk-wall-get :domain domain)))
  52. :test #'>= :key (lambda (p) (aget "id" p))))
  53. name)
  54. (setf period (adjust-period period (length new-posts)))
  55. (when new-posts
  56. (setf name (vk-get-name domain)))
  57. (dolist (post new-posts)
  58. (multiple-value-bind (text disable)
  59. (%format-wall-post domain name post)
  60. (dolist (chat-id (db-vk-get-domain-chats domain))
  61. (ignore-errors
  62. (telegram-send-message chat-id text
  63. :parse-mode "Markdown"
  64. :disable-web-preview disable))))
  65. (setf last-id (max last-id (aget "id" post)))))
  66. (error (e) (log:error "~A" e)))
  67. (db-vk-update-wall domain last-id
  68. (local-time:timestamp-to-unix
  69. (local-time:timestamp+ (local-time:now) period :sec))
  70. period))) ;; Update last-id, next-fetch and period
  71. (error (e) (log:error "~A" e))))
  72. ;; Hooks
  73. (defparameter +akb-vk-domain+ "baneks" "VK.com username of 'B-category anekdotes'")
  74. (defun format-akb (post)
  75. (let* ((id (aget "id" post))
  76. (url (format nil "https://vk.com/~A?w=wall~A_~A"
  77. +akb-vk-domain+ (aget "from_id" post) id)))
  78. (format nil "~A~%~A" (aget "text" post) url)))
  79. (def-message-cmd-handler handler-akb (:akb)
  80. (let ((total-aneks
  81. (aget "count" (vk-wall-get :domain +akb-vk-domain+ :count 1 :offset 10000000))))
  82. (dolist (post (aget "items" (vk-wall-get :domain +akb-vk-domain+
  83. :count (or (ignore-errors (parse-integer (car args))) 1)
  84. :offset (random total-aneks))))
  85. (bot-send-message chat-id (format-akb post) :disable-web-preview 1))))
  86. ;; VK walls
  87. (defun %send-domains (chat-id domains)
  88. (bot-send-message
  89. chat-id
  90. (if (null domains)
  91. "Пока ничего не постим"
  92. (format nil "Постим~%~{~A) https://vk.com/~A~^~%~}"
  93. (loop for d in domains for i from 1 append (list i d))))
  94. :disable-web-preview 1))
  95. (defun %find-vk-domain (url)
  96. (let ((path (puri:uri-path (puri:parse-uri url))))
  97. (if (equal #\/ (elt path 0))
  98. (subseq path 1)
  99. path)))
  100. (defun %ensure-domain (domain)
  101. (let* ((res (vk-wall-get :domain domain :count 1))
  102. (last-id (aget "id" (first (aget "items" res)))))
  103. (db-vk-ensure-domain domain last-id)
  104. domain))
  105. (defun %vk-find-best-photo (attach)
  106. (when attach
  107. (let* ((photo (aget "photo" attach))
  108. (sizes (loop for (k . v) in photo
  109. when (equal "photo_" (subseq k 0 (min 6 (length k))))
  110. collect (cons (parse-integer (subseq k 6)) v))))
  111. (cdr (assoc (apply #'max (mapcar #'car sizes)) sizes)))))
  112. (defun %vk-find-video (attach)
  113. (when attach
  114. (let ((video (aget "video" attach)))
  115. (format nil "https://vk.com/video~A_~A"
  116. (aget "owner_id" video)
  117. (aget "id" video)))))
  118. (defun %vk-find-preview (attachments)
  119. (labels ((find-type (type)
  120. (find type attachments :key #'(lambda (a) (aget "type" a)) :test #'equal)))
  121. (or
  122. (%vk-find-best-photo (find-type "photo"))
  123. (%vk-find-video (find-type "video")))))
  124. (defparameter +vk-link-scanner+ (cl-ppcre:create-scanner "\\[((id|club)\\d+)\\|([^\\]]*?)\\]") "vk linking regex")
  125. (defun %vk-post-text (post)
  126. (let* ((history (aget "copy_history" post))
  127. (reposts (loop for p in history
  128. collect (let* ((owner (aget "owner_id" p))
  129. (type (if (> owner 0) "id" "club"))
  130. (id (abs owner)))
  131. (format nil "[~A](https://vk.com/~A~A)"
  132. (vk-get-name owner) type id)))))
  133. (when history
  134. (setf post (car (last history))))
  135. (values
  136. (cl-ppcre:regex-replace-all +vk-link-scanner+
  137. (aget "text" post)
  138. "[\\3](https://vk.com/\\1)")
  139. (%vk-find-preview (aget "attachments" post))
  140. reposts)))
  141. (defun %format-wall-post (domain name post)
  142. (multiple-value-bind (text preview reposts) (%vk-post-text post)
  143. (values
  144. (format nil "~@[[✅](~A) ~][~A](https://vk.com/~A?w=wall~A_~A)~@[ ~{↩ ~A~}~]~@[ @ ~A~]~%~A"
  145. preview name domain (aget "from_id" post) (aget "id" post)
  146. reposts (format-ts (local-time:unix-to-timestamp (aget "date" post)))
  147. text)
  148. (if preview 0 1))))
  149. (def-message-cmd-handler handler-cmd-wall (:wall)
  150. (let ((domains (db-vk-get-chat-domains chat-id)))
  151. (if (null args)
  152. (%send-domains chat-id domains)
  153. (progn
  154. (dolist (url args)
  155. (handler-case
  156. (let ((idx (parse-integer url)))
  157. (db-vk-remove-chat-domain chat-id (nth (1- idx) domains)))
  158. (parse-error ()
  159. (let* ((domain (%ensure-domain (%find-vk-domain url)))
  160. (existing (find domain domains :test #'equal)))
  161. (if existing
  162. (db-vk-remove-chat-domain chat-id domain)
  163. (db-vk-add-chat-domain chat-id domain))))
  164. (error (e) (log:error "~A" e))))
  165. (%send-domains chat-id (db-vk-get-chat-domains chat-id))))))