|
|
@@ -0,0 +1,57 @@
|
|
|
+(in-package :cl-user)
|
|
|
+(defpackage #:timeliner.twitter
|
|
|
+ (:use :cl #:timeliner.utils :cl-mongo)
|
|
|
+ (:export
|
|
|
+ #:*access-token*
|
|
|
+ #:on-cron))
|
|
|
+(in-package #:timeliner.twitter)
|
|
|
+
|
|
|
+(defvar *access-token*)
|
|
|
+
|
|
|
+(defun make-twitter-doc (twit)
|
|
|
+ (let* ((ts (local-time:universal-to-timestamp
|
|
|
+ (cl-date-time-parser:parse-date-time
|
|
|
+ (aget "created_at" twit)))))
|
|
|
+ (kv
|
|
|
+ (kv :ts ts)
|
|
|
+ (kv :type "twitter")
|
|
|
+ (kv :title (aget "text" twit))
|
|
|
+ (kv :language (aget "lang" twit))
|
|
|
+ (kv :twitter (kv
|
|
|
+ (kv "id" (aget "id" twit))
|
|
|
+ (kv "reply_to" (aget "in_reply_to_status_id" twit))
|
|
|
+ (kv "retweeted" (aget "retweeted" twit))))
|
|
|
+ (kv :loc (timeliner.locations:point->doc
|
|
|
+ (timeliner.locations:find-location-at (ts->ms ts)))))))
|
|
|
+
|
|
|
+(defparameter *timeline-url* "https://api.twitter.com/1.1/statuses/user_timeline.json")
|
|
|
+
|
|
|
+(defun load-twits (since-id)
|
|
|
+ (loop
|
|
|
+ for max-id = nil then (1- (aget "id" (car (last data))))
|
|
|
+ for data = (yason:parse
|
|
|
+ (flexi-streams:octets-to-string
|
|
|
+ (cl-oauth:access-protected-resource
|
|
|
+ (format nil "~A?~A"
|
|
|
+ *timeline-url*
|
|
|
+ (cl-oauth::alist->query-string
|
|
|
+ (remove-if
|
|
|
+ (complement #'cdr)
|
|
|
+ (list
|
|
|
+ (cons "count" 200)
|
|
|
+ (cons "trim_user" 1)
|
|
|
+ (cons "since_id" since-id)
|
|
|
+ (cons "max_id" max-id)))
|
|
|
+ :include-leading-ampersand nil))
|
|
|
+ *access-token*))
|
|
|
+ :object-as :alist)
|
|
|
+ while data
|
|
|
+ append (mapcar #'make-twitter-doc data)))
|
|
|
+
|
|
|
+(defun on-cron ()
|
|
|
+ (let* ((last-twit (first (docs (db.sort "events" ($ "type" "twitter") :field "ts"
|
|
|
+ :asc nil :limit 1))))
|
|
|
+ (since-id (if last-twit (get-element "twitter.id" last-twit) 1))
|
|
|
+ (twits (load-twits since-id)))
|
|
|
+ (format t "Got ~A twits from ~A~%" (length twits) since-id)
|
|
|
+ (save-events twits)))
|