1
0

foursquare.lisp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. (in-package :cl-user)
  2. (defpackage chatikbot.plugins.foursquare
  3. (:use :cl :chatikbot.common :alexandria))
  4. (in-package :chatikbot.plugins.foursquare)
  5. (defparameter *fsq-checkins-url* "https://api.foursquare.com/v2/checkins/recent"
  6. "URL of recent checkins API")
  7. (defparameter *fsq-api-url* "https://api.foursquare.com/v2/~A"
  8. "Foursquare API URL")
  9. (defparameter +fsq-oauth-url+ "https://foursquare.com/oauth2/authenticate")
  10. (defparameter +fsq-token-url+ "https://foursquare.com/oauth2/access_token")
  11. (defsetting *client-id* nil "App Client ID")
  12. (defsetting *client-secret* nil "App Client Secret")
  13. (defun get-authorization-url (chat-id)
  14. (quri:render-uri
  15. (quri:merge-uris
  16. (quri:make-uri :query (quri:url-encode-params
  17. `(("response_type" . "code")
  18. ("client_id" . ,*client-id*)
  19. ("redirect_uri" . ,(get-oauth-url))
  20. ("state" . ,(encode-oauth-state :fsq chat-id)))))
  21. (quri:uri +fsq-oauth-url+))))
  22. (def-oauth-section-handler oauth-handler (:fsq)
  23. (if code
  24. (progn
  25. (log:info code state)
  26. (let* ((resp (json-request +fsq-token-url+ :method :post
  27. :content (list
  28. (cons "code" code)
  29. (cons "client_id" *client-id*)
  30. (cons "client_secret" *client-secret*)
  31. (cons "redirect_uri" (get-oauth-url))
  32. (cons "grant_type" "authorization_code"))))
  33. (access-token (aget "access_token" resp))
  34. (chat-id (parse-integer state)))
  35. (log:info chat-id access-token resp)
  36. (if access-token
  37. (let ((token-id (db/fsq-ensure-token access-token)))
  38. (db/fsq-set-chat-token-id chat-id token-id)
  39. (hunchentoot:redirect "/oauth/success"))
  40. (hunchentoot:redirect "/oauth/fail"))))
  41. (progn
  42. (log:info error)
  43. (hunchentoot:redirect "/oauth/fail"))))
  44. (defun %send-auth (chat-id)
  45. (bot-send-message chat-id "Нет токена"
  46. :reply-markup (telegram-inline-keyboard-markup
  47. (list (list (list :text "Авторизоваться!"
  48. :url (get-authorization-url chat-id)))))))
  49. (defun %fsq-api-call (access-token method &optional params)
  50. (let* ((resp
  51. (json-request (format nil *fsq-api-url* method)
  52. :parameters (list*
  53. (cons "oauth_token" access-token)
  54. (cons "v" "20150811")
  55. params)
  56. :timeout 5))
  57. (meta (aget "meta" resp)))
  58. (when (not (= 200 (aget "code" meta)))
  59. (error (format nil "Foursquare API error, code ~A, errorType '~A', errorDetail '~A'"
  60. (aget "code" meta) (aget "errorType" meta) (aget "errorDetail" meta))))
  61. (aget "response" resp)))
  62. (defun fsq-fetch-self (access-token)
  63. (aget "user" (%fsq-api-call access-token "users/self")))
  64. (defun fsq-fetch-checkins (access-token &optional after-timestamp limit)
  65. (aget "recent"
  66. (%fsq-api-call access-token "checkins/recent"
  67. (list (cons "afterTimestamp" (or after-timestamp "0"))
  68. (cons "limit" (or limit "20"))))))
  69. (defun fsq-fetch-friends (access-token &optional offset)
  70. (list*
  71. (fsq-fetch-self access-token)
  72. (aget "items"
  73. (aget "friends"
  74. (%fsq-api-call access-token "users/self/friends"
  75. (list (cons "offset" (or offset "0"))))))))
  76. ;; Formatting
  77. (defun fsq-user-name (user)
  78. (when user
  79. (format nil "~@[~A~]~@[ ~A~]"
  80. (aget "firstName" user)
  81. (aget "lastName" user))))
  82. (defun fsq-format-checkin (checkin &optional with-dates)
  83. (when checkin
  84. (let ((user (aget "user" checkin))
  85. (venue (aget "venue" checkin)))
  86. (format nil "📍 ~@[~A~]~@[ ~A~]~@[ в ~A~]~@[ (~A)~]~@[ 📢 ~A~]~:[~; ~A~]"
  87. (aget "firstName" user) (aget "lastName" user)
  88. (aget "name" venue) (first (aget "formattedAddress" (aget "location" venue)))
  89. (aget "shout" checkin)
  90. with-dates
  91. (local-time:format-timestring
  92. nil
  93. (local-time:unix-to-timestamp (aget "createdAt" checkin))
  94. :format '(:year "-" (:month 2) "-" (:day 2) " " (:hour 2) ":" (:min 2)))))))
  95. ;; Database
  96. (def-db-init
  97. (db-execute "create table if not exists fsq_tokens (id INTEGER PRIMARY KEY, token)")
  98. (db-execute "create table if not exists fsq_chat_tokens (token_id REFERENCES fsq_tokens, chat_id)")
  99. (db-execute "create unique index if not exists fsq_chat_tokens_idx on fsq_chat_tokens (token_id, chat_id)")
  100. (db-execute "create table if not exists fsq_chat_users (chat_id, user_id)")
  101. (db-execute "create index if not exists fsq_chat_users_chat_idx on fsq_chat_users (chat_id)")
  102. (db-execute "create index if not exists fsq_chat_users_user_idx on fsq_chat_users (user_id)")
  103. (db-execute "create table if not exists fsq_seen (token_id REFERENCES fsq_tokens, checkin_id, created_at)")
  104. (db-execute "create index if not exists fsq_seen_idx on fsq_seen (checkin_id)"))
  105. (defun db/fsq-ensure-token (token)
  106. (db-transaction
  107. (or (db-single "select id from fsq_tokens where token = ?" token)
  108. (db-execute "insert into fsq_tokens (token) values (?)" token))))
  109. (defun db/fsq-get-chat-token (chat-id)
  110. (db-single "select t.token from fsq_chat_tokens ct inner join fsq_tokens t on t.id=ct.token_id where chat_id = ?" chat-id))
  111. (defun db/fsq-set-chat-token-id (chat-id token-id)
  112. (db-execute "replace into fsq_chat_tokens (token_id, chat_id) values (?, ?)" token-id chat-id))
  113. (defun db/fsq-get-active-tokens ()
  114. (db-select "select id, token from fsq_tokens as t where exists (select 1 from fsq_chat_users cu inner join fsq_chat_tokens as ct on cu.chat_id=ct.chat_id and ct.token_id = t.id)"))
  115. (defun db/fsq-get-token-chats (token)
  116. (flatten (db-select "select chat_id from fsq_chat_tokens inner join fsq_tokens on token_id = id where token = ?" token)))
  117. (defun db/fsq-get-chat-users (chat-id)
  118. (flatten (db-select "select user_id from fsq_chat_users where chat_id = ?" chat-id)))
  119. (defun db/fsq-get-user-chats (user-id)
  120. (flatten (db-select "select chat_id from fsq_chat_users where user_id = ?" user-id)))
  121. (defun db/fsq-set-chat-users (chat-id users)
  122. (db-transaction
  123. (db-execute "delete from fsq_chat_users where chat_id = ?" chat-id)
  124. (dolist (user-id users)
  125. (db-execute "insert into fsq_chat_users (chat_id, user_id) values (?, ?)" chat-id user-id))))
  126. (defun db/fsq-add-seen (token-id checkin-id created-at)
  127. (db-execute "insert into fsq_seen (token_id, checkin_id, created_at) values (?, ?, ?)" token-id checkin-id created-at))
  128. (defun db/fsq-has-seen (checkin-id)
  129. (db-single "select checkin_id from fsq_seen where checkin_id = ? limit 1" checkin-id))
  130. (defun db/fsq-has-seen-via (token-id checkin-id)
  131. (db-single "select checkin_id from fsq_seen where token_id = ? and checkin_id = ? limit 1" token-id checkin-id))
  132. (defun db/fsq-last-created (token-id)
  133. (db-single "select created_at from fsq_seen where token_id = ? order by created_at desc limit 1" token-id))
  134. ;; Cron
  135. (defcron process-latest-checkins ()
  136. (let ((checkins (make-hash-table)))
  137. (loop
  138. for (token-id token) in (db/fsq-get-active-tokens)
  139. for ts = (princ-to-string (1+ (or (db/fsq-last-created token-id) -1)))
  140. do (dolist (checkin (fsq-fetch-checkins token ts))
  141. (let ((checkin-id (aget "id" checkin))
  142. (created-at (aget "createdAt" checkin))
  143. (user-id (aget "id" (aget "user" checkin))))
  144. (unless (db/fsq-has-seen-via token-id checkin-id)
  145. (unless (db/fsq-has-seen checkin-id)
  146. (dolist (chat-id (db/fsq-get-user-chats user-id))
  147. (push (fsq-format-checkin checkin)
  148. (gethash chat-id checkins))))
  149. (db/fsq-add-seen token-id checkin-id created-at)))))
  150. (loop for chat-id being the hash-keys in checkins using (hash-value texts)
  151. do (log:info "Sending checkins" chat-id texts)
  152. (telegram-send-message chat-id (format nil "~{~A~^~%~}" texts)))))
  153. ;; Hooks
  154. (defmacro with-fsq-token ((token chat-id) &body body)
  155. `(let ((,token (db/fsq-get-chat-token ,chat-id)))
  156. (if ,token
  157. (progn ,@body)
  158. (%send-auth chat-id))))
  159. (def-message-cmd-handler handle-cmd-post-checkins (:postcheckins)
  160. (with-fsq-token (token chat-id)
  161. (let ((users (db/fsq-get-chat-users chat-id)))
  162. (if (null args)
  163. (bot-send-message chat-id
  164. (if (null users)
  165. "Пока никого не палим"
  166. (format nil "Палим ~{~A~^, ~}"
  167. (loop for user in (fsq-fetch-friends token)
  168. when (member (aget "id" user)
  169. users :test #'equal)
  170. collect (fsq-user-name user)))))
  171. (let ((friends (fsq-fetch-friends token)))
  172. (dolist (user args)
  173. (let ((username (fsq-user-name
  174. (find user friends
  175. :test #'equal
  176. :key #'(lambda (f) (aget "id" f))))))
  177. (when username
  178. (if (member user users :test #'equal)
  179. (progn
  180. (setf users (remove user users :test #'equal))
  181. (bot-send-message chat-id
  182. (format nil "Больше не палим ~A" username)))
  183. (progn
  184. (push user users)
  185. (bot-send-message chat-id (format nil "Теперь палим ~A" username)))))))
  186. (db/fsq-set-chat-users chat-id users))))))
  187. (def-message-cmd-handler handle-cmd-friends (:friends)
  188. (with-fsq-token (token chat-id)
  189. (let ((users (db/fsq-get-chat-users chat-id))
  190. (friends (fsq-fetch-friends token)))
  191. (bot-send-message chat-id
  192. (text-chunks (loop for user in friends
  193. collect (format nil "~A: ~:[~;📍 ~]~A"
  194. (aget "id" user)
  195. (member (aget "id" user) users :test #'equal)
  196. (fsq-user-name user)))
  197. :text-sep "
  198. " :pre-pre "" :pre-post "")))))
  199. (def-message-cmd-handler handle-cmd-checkins (:checkins)
  200. (with-fsq-token (token chat-id)
  201. (let ((users (db/fsq-get-chat-users chat-id)))
  202. (when users
  203. (bot-send-message chat-id
  204. (format nil "~{~A~^~%~}"
  205. (or
  206. (loop for checkin in (fsq-fetch-checkins token)
  207. if (member (aget "id" (aget "user" checkin)) users :test #'equal)
  208. collect (fsq-format-checkin checkin t))
  209. '("Нету"))))))))