1
0

vk.lisp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. (in-package :cl-user)
  2. (defpackage chatikbot.plugins.vk
  3. (:use :cl :chatikbot.common :alexandria))
  4. (in-package :chatikbot.plugins.vk)
  5. (defparameter +vk-api-ver+ "5.53" "vk api version to use")
  6. (defparameter +vk-api-url+ "https://api.vk.com/method/~A?v=~A" "vk.com API endpoint")
  7. (defparameter +vk-oauth-authorize+ "https://oauth.vk.com/authorize" "vk.com OAuth authrization endpoint")
  8. (defsetting *vk-app-client-id* nil "vk app to authenticate against")
  9. (defsetting *vk-app-client-secret* nil "vk app secret")
  10. (defparameter +vk-scope-mapping+
  11. '((:notify . 1) (:friends . 2) (:photos . 4) (:audio . 8) (:video . 16) (:docs . 131072) (:notes . 2048)
  12. (:pages . 128) (:pages-left . 256) (:status . 1024) (:offers . 32) (:questions . 64) (:wall . 8192)
  13. (:groups . 262144) (:messages . 4096) (:email . 4194304) (:notifications . 524288) (:stats . 1048576)
  14. (:ads . 32768) (:market . 134217728) (:offline . 65536)))
  15. (defun vk-get-authorization-url (&optional state &rest scopes)
  16. (let ((scope (apply #'+ (mapcar #'(lambda (k) (cdr (assoc k +vk-scope-mapping+))) scopes))))
  17. (format nil "~A?v=~A&client_id=~A&redirect_uri=~A~@[&scope=~A~]~@[&state=~A~]"
  18. +vk-oauth-authorize+ +vk-api-ver+ *vk-app-client-id* (get-oauth-url) (unless (zerop scope) scope) state)))
  19. (defun %vk-api-call (method &optional args)
  20. (let* ((params (loop for (k . v) in args
  21. when v
  22. collect (cons
  23. (princ-to-string k)
  24. (princ-to-string v))))
  25. (response (json-request (format nil +vk-api-url+ method +vk-api-ver+)
  26. :method :post
  27. :content params
  28. :timeout 5)))
  29. (when (aget "error" response)
  30. (error (aget "error_msg" (aget "error" response))))
  31. (aget "response" response)))
  32. ;; Database
  33. (def-db-init
  34. (db-execute "create table if not exists vk_walls (domain, last_id, next_fetch, period)")
  35. (db-execute "create unique index if not exists vk_walls_domain_idx on vk_walls (domain)")
  36. (db-execute "create table if not exists vk_chat_walls (chat_id, domain)")
  37. (db-execute "create index if not exists vk_chat_walls_chat_idx on vk_chat_walls (chat_id)")
  38. (db-execute "create index if not exists vk_chat_walls_domain_idx on vk_chat_walls (domain)"))
  39. (defun db/vk-ensure-domain (domain last-id)
  40. (db-transaction
  41. (unless (db-single "select domain from vk_walls where domain = ?" domain)
  42. (db-execute "insert into vk_walls (domain, last_id, period) values (?, ?, 300)" domain last-id))))
  43. (defun db/vk-get-domain-chats (domain)
  44. (flatten (db-select "select chat_id from vk_chat_walls where domain = ?" domain)))
  45. (defun db/vk-get-chat-domains (chat-id)
  46. (flatten (db-select "select domain from vk_chat_walls where chat_id = ?" chat-id)))
  47. (defun db/vk-add-chat-domain (chat-id domain)
  48. (db-execute "insert into vk_chat_walls (chat_id, domain) values (?, ?)" chat-id domain))
  49. (defun db/vk-remove-chat-domain (chat-id domain)
  50. (db-execute "delete from vk_chat_walls where chat_id = ? and domain = ?" chat-id domain))
  51. (defun db/vk-get-active-walls ()
  52. (db-select "select domain, last_id, next_fetch, period from vk_walls w where exists (select 1 from vk_chat_walls where domain=w.domain)"))
  53. (defun db/vk-update-wall (domain last-id next-fetch period)
  54. (db-execute "update vk_walls set last_id = ?, next_fetch = ?, period = ? where domain = ?" last-id next-fetch period domain))
  55. (defun vk-wall-get (&key owner-id domain offset count filter extended fields)
  56. (%vk-api-call "wall.get"
  57. `(("owner_id" . ,owner-id)
  58. ("domain" . ,domain)
  59. ("offset" . ,offset)
  60. ("count" . ,count)
  61. ("filter" . ,filter)
  62. ("extended" . ,extended)
  63. ("fields" . ,fields))))
  64. (defun vk-wall-get-by-id (post-ids &key extended fields copy-history-depth)
  65. (%vk-api-call "wall.getById"
  66. `(("posts" . ,(if (consp post-ids)
  67. (format nil "~{~A~^,~}" post-ids) post-ids))
  68. ("extended" . ,extended)
  69. ("copy_history_depth" . ,copy-history-depth)
  70. ("fields" . ,fields))))
  71. (defun vk-get-user-name (id)
  72. (let ((r (first (%vk-api-call "users.get" `(("user_ids" . ,id))))))
  73. (format nil "~A ~A" (aget "first_name" r) (aget "last_name" r))))
  74. (defun vk-get-group-name (id)
  75. (aget "name" (first (%vk-api-call "groups.getById"
  76. `(("group_id" . ,(if (numberp id)
  77. (- id)
  78. id)))))))
  79. (defun vk-get-name (id)
  80. (if (and (numberp id) (> id 0))
  81. (vk-get-user-name id)
  82. (vk-get-group-name id)))
  83. ;; VK walls
  84. (defun %send-domains (chat-id domains)
  85. (bot-send-message
  86. chat-id
  87. (if (null domains)
  88. "Пока ничего не постим"
  89. (format nil "Постим~%~{~A) https://vk.com/~A~^~%~}"
  90. (loop for d in domains for i from 1 append (list i d))))
  91. :disable-web-preview 1))
  92. (defun %find-vk-domain (url)
  93. (let ((path (quri:uri-path (quri:uri url))))
  94. (if (equal #\/ (elt path 0))
  95. (subseq path 1)
  96. path)))
  97. (defun %ensure-domain (domain)
  98. (let* ((res (vk-wall-get :domain domain :count 1))
  99. (last-id (aget "id" (first (aget "items" res)))))
  100. (db/vk-ensure-domain domain last-id)
  101. domain))
  102. (defun %vk-find-best-photo (attach)
  103. (when attach
  104. (let* ((photo (aget "photo" attach))
  105. (sizes (loop for (k . v) in photo
  106. when (equal "photo_" (subseq k 0 (min 6 (length k))))
  107. collect (cons (parse-integer (subseq k 6)) v))))
  108. (cdr (assoc (apply #'max (mapcar #'car sizes)) sizes)))))
  109. (defun %vk-find-video (attach)
  110. (when attach
  111. (let ((video (aget "video" attach)))
  112. (format nil "https://vk.com/video~A_~A"
  113. (aget "owner_id" video)
  114. (aget "id" video)))))
  115. (defun %vk-find-preview (attachments)
  116. (labels ((find-type (type)
  117. (find type attachments :key #'(lambda (a) (aget "type" a)) :test #'equal)))
  118. (or
  119. (%vk-find-best-photo (find-type "photo"))
  120. (%vk-find-video (find-type "video")))))
  121. (defparameter +vk-link-scanner+ (cl-ppcre:create-scanner "\\[((id|club)\\d+)\\|([^\\]]*?)\\]") "vk linking regex")
  122. (defun %vk-post-text (post)
  123. (let* ((history (aget "copy_history" post))
  124. (reposts (loop for p in history
  125. collect (let* ((owner (aget "owner_id" p))
  126. (type (if (> owner 0) "id" "club"))
  127. (id (abs owner)))
  128. (format nil "[~A](https://vk.com/~A~A)"
  129. (vk-get-name owner) type id)))))
  130. (when history
  131. (setf post (car (last history))))
  132. (values
  133. (cl-ppcre:regex-replace-all +vk-link-scanner+
  134. (aget "text" post)
  135. "[\\3](https://vk.com/\\1)")
  136. (%vk-find-preview (aget "attachments" post))
  137. reposts)))
  138. (defun %format-wall-post (domain name post)
  139. (multiple-value-bind (text preview reposts) (%vk-post-text post)
  140. (values
  141. (format nil "~@[[✅](~A) ~][~A](https://vk.com/~A?w=wall~A_~A)~@[ ~{↩ ~A~}~]~@[ @ ~A~]~%~A"
  142. preview name domain (aget "from_id" post) (aget "id" post)
  143. reposts (format-ts (local-time:unix-to-timestamp (aget "date" post)))
  144. text)
  145. (if preview 0 1))))
  146. ;; Cron
  147. (defcron process-walls ()
  148. (loop for (domain last-id next-fetch period) in (db/vk-get-active-walls)
  149. when (or (null next-fetch)
  150. (local-time:timestamp> (local-time:now) (local-time:unix-to-timestamp next-fetch)))
  151. do (progn
  152. (log:info "Fetching wall" domain)
  153. (handler-case
  154. (let ((new-posts
  155. (remove last-id (reverse (aget "items" (vk-wall-get :domain domain)))
  156. :test #'>= :key (lambda (p) (aget "id" p))))
  157. name)
  158. (setf period (chatikbot.plugins.rss::adjust-period period (length new-posts)))
  159. (when new-posts
  160. (setf name (vk-get-name domain)))
  161. (dolist (post new-posts)
  162. (multiple-value-bind (text disable)
  163. (%format-wall-post domain name post)
  164. (dolist (chat-id (db/vk-get-domain-chats domain))
  165. (ignore-errors
  166. (telegram-send-message chat-id text
  167. :parse-mode "Markdown"
  168. :disable-web-preview disable))))
  169. (setf last-id (max last-id (aget "id" post)))))
  170. (error (e) (log:error "~A" e)))
  171. (db/vk-update-wall domain last-id
  172. (local-time:timestamp-to-unix
  173. (local-time:timestamp+ (local-time:now) period :sec))
  174. period))) ;; Update last-id, next-fetch and period
  175. )
  176. ;; Hooks
  177. (defparameter +akb-vk-domain+ "baneks" "VK.com username of 'B-category anekdotes'")
  178. (defvar *akb-max-posts* 10 "Maximum number of AKB posts to send at once")
  179. (defun format-akb (post)
  180. (let* ((id (aget "id" post))
  181. (url (format nil "https://vk.com/~A?w=wall~A_~A"
  182. +akb-vk-domain+ (aget "from_id" post) id)))
  183. (format nil "~A~%~A" (aget "text" post) url)))
  184. (def-message-cmd-handler handler-akb (:akb)
  185. (let ((total-aneks
  186. (aget "count" (vk-wall-get :domain +akb-vk-domain+ :count 1 :offset 10000000))))
  187. (dolist (post (aget "items" (vk-wall-get :domain +akb-vk-domain+
  188. :count (min *akb-max-posts*
  189. (or (ignore-errors (parse-integer (car args)))
  190. 1))
  191. :offset (random total-aneks))))
  192. (bot-send-message chat-id (format-akb post) :disable-web-preview 1))))
  193. (def-message-cmd-handler handler-cmd-wall (:wall)
  194. (let ((domains (db/vk-get-chat-domains chat-id)))
  195. (if (null args)
  196. (%send-domains chat-id domains)
  197. (progn
  198. (dolist (url args)
  199. (handler-case
  200. (let ((idx (parse-integer url)))
  201. (db/vk-remove-chat-domain chat-id (nth (1- idx) domains)))
  202. (parse-error ()
  203. (let* ((domain (%ensure-domain (%find-vk-domain url)))
  204. (existing (find domain domains :test #'equal)))
  205. (if existing
  206. (db/vk-remove-chat-domain chat-id domain)
  207. (db/vk-add-chat-domain chat-id domain))))
  208. (error (e) (log:error "~A" e))))
  209. (%send-domains chat-id (db/vk-get-chat-domains chat-id))))))