git.fiddlerwoaroof.com
Raw Blame History
(in-package :whitespace.rss)
(cl-annot.syntax:enable-annot-syntax)


@export
(defmacro default-when (default test &body body)
  (once-only (default)
    `(or (when ,test
           ,@body)
         ,default)))

@export-class
(defclass rss-feed ()
  ((feed :accessor rss-feed-feed :initarg :feed)
   (channel :accessor rss-feed-channel :initarg :channel)
   (title :accessor rss-feed-title :initarg :title)
   (link :accessor rss-feed-link :initarg :link)
   (description :accessor rss-feed-description :initarg :description)
   (items :accessor rss-feed-items :initarg :items)))

@export-class
(defclass rss-item ()
  ((item :accessor rss-item-item  :initarg :item)
   (title :accessor rss-item-title :initarg :title)
   (link :accessor rss-item-link :initarg :link)
   (description-raw :accessor rss-item-description-raw :initarg :description-raw)
   (description :accessor rss-item-description :initarg :description)
   (category :accessor rss-item-category :initarg :category)
   (comments :accessor rss-item-comments :initarg :comments)
   (enclosure :accessor rss-item-enclosure :initarg :enclosure)
   (guid :accessor rss-item-guid :initarg :guid)
   (pub-date :accessor rss-item-pub-date :initarg :pub-date)
   (source :accessor rss-item-source  :initarg :source)))

(load "tables.lisp")

(setf (symbol-function 'rss-item-encoder)
      (jonathan.helper:compile-encoder () (title link description category comments enclosure guid
                                                 pub-date source)
        (list :|title| title :|link| link :|description| description
              :|category| category :|comments| comments :|enclosure| enclosure
              :|guid| guid :|pub-date| pub-date :|source| source)))

(setf (symbol-function 'rss-feed-encoder)
      (jonathan.helper:compile-encoder () (title link description items)
        (list :|title| title :|link| link :|description| description :|items| items)))

(defmethod jonathan:%to-json ((obj rss-feed))
  (jonathan:with-object
    (jonathan:write-key-value "title" (coerce (rss-feed-title obj) 'simple-string))
    (jonathan:write-key-value "link" (coerce (rss-feed-link obj) 'simple-string))
    (jonathan:write-key-value "description" (coerce (rss-feed-description obj) 'simple-string))
    (jonathan:write-key-value "items" (rss-feed-items obj))))

(defmethod jonathan:%to-json ((obj rss-item))
  (jonathan:with-object
    (jonathan:write-key-value "title" (coerce (rss-item-title obj) 'simple-string))
    (jonathan:write-key-value "link" (coerce (rss-item-link obj) 'simple-string))
    (jonathan:write-key-value "description" (coerce (rss-item-description-raw obj) 'simple-string))
    ;(jonathan:write-key-value "category" (rss-item-category obj))
    (jonathan:write-key-value "comments" (coerce (rss-item-comments obj) 'simple-string))
    (jonathan:write-key-value "enclosure" "rss-item-enclosure obj")
    (jonathan:write-key-value "guid" (coerce (rss-item-guid obj) 'simple-string))
    (jonathan:write-key-value "date" (coerce (rss-item-pub-date obj) 'simple-string))
    (jonathan:write-key-value "source" (coerce (rss-item-source obj) 'simple-string))))

@export
(defgeneric serialize (cls &rest links)
  (:method ((obj list) &rest ignored)
   (declare (ignore ignored))
   (loop for item in obj
         collect (serialize item)))

  (:method ((obj vector) &rest ignored)
   (declare (ignore ignored))
   (loop for item across obj
         collect (serialize item)))

  (:method ((obj rss-feed) &rest ignored)
   (declare (ignore ignored))
   (let ((feed (postmodern:make-dao
                 'rss_feed_store
                 :title (rss-feed-title obj) :link (rss-feed-link obj) :description (rss-feed-description obj))))
     (format t "~a~%" (rfs-link feed))
     (loop for item in (rss-feed-items obj)
           collect (serialize item (rfs-id feed)))
     feed))

  (:method ((obj rss-item) &rest links)
   (let ((feed (car links)))
     (format t "~a~%" feed)
     (postmodern:make-dao 'rss_item_store
                          :title (rss-item-title obj)
                          :link (rss-item-link obj)
                          :description (rss-item-description-raw obj)
                          :guid (rss-item-guid obj) :pub-date (rss-item-pub-date obj)
                          :source (rss-item-source obj)
                          :feed feed))))

@export
(defmacro copy-slots (slots from-v to-v)
   (with-gensyms (from to)
     `(let ((,from ,from-v) (,to ,to-v))
        ,@(loop for (fro-slot to-slot)
                in (mapcar (lambda (x) (if (symbolp x) (list x x) x)) slots)
                collect `(setf (slot-value ,to ',to-slot) (slot-value ,from ',fro-slot))))))


@export
(defun deserialize-item (item)
  (let ((result (make-instance 'rss-item)))
    (copy-slots (title link (description description-raw) comments enclosure guid pub-date source)
                item
                result)
    result))

@export
(defun deserialize-items (feed-id)
  (let ((items (postmodern:query-dao 'rss_item_store
                                     (:select :* :from 'rss_item_store :where (:= :feed feed-id)))))
    (loop for item in items collect (deserialize-item item))))

@export
(defun deserialize-feed (feed)
  (let ((result (make-instance 'rss-feed)))
    (copy-slots (title link description) feed result)
    (setf (rss-feed-items result) (deserialize-items (rfs-id feed)))
    result))

@export
(defun deserialize (&optional user-info)
  (default-when #() (not (emptyp user-info))
    (let ((feeds
            (postmodern:query-dao 'rss_feed_store
                                  (:select 'rssfeed.*
                                   :from 'rssfeed
                                   :inner-join  'subscriptions :on (:= 'rssfeed.id  'subscriptions.feedid)
                                   :inner-join  'reader_user :on (:= 'reader_user.id  'subscriptions.uid)
                                   :where (:= 'reader_user.foreign_id (user-foreign-id (car user-info)))))))
      (apply #'vector (loop for feed in feeds collect (deserialize-feed feed))))))


@export
(defmacro get-elements (feed &optional (filter nil))
  (let ((feed-sym (gensym))
        (filter-lis `(lambda (x) (and (plump-dom:element-p x) ,@(loop for x in filter
                                                                      collect `(funcall ,x x))))))
    `(let ((,feed-sym ,feed))
       (remove-if-not ,filter-lis (plump:children ,feed-sym)))))

@export
(defmacro get-elements-by-tagname (feed tagname)
  `(get-elements ,feed ((lambda (x) (string= ,tagname (plump:tag-name x))))))

@export
(defmacro extract-text (selector &optional (default ""))
  `(or (lquery:$ ,selector (text) (node)) ,default))

@export
(defun normalize-html (html)
  (let ((plump-parser:*tag-dispatchers* plump:*html-tags*))
    (with-output-to-string (ss)
      (map 'vector
           (lambda (x) (plump:serialize (plump:parse (plump:text x)) ss))
           html)
      ss)))

@export
(defmacro xml-text-bind (syms &body body)
  "Bind the symbols passed in the second arg to the text of the matching
   elements in the document lquery has been initialized with and then run the
   body in the resulting lexical scope.  This assumes that lquery:initialize
   has already been passed the proper xml document"
  `(let* ,(loop for sym in syms
           collect `(,sym (or (lquery:$ ,(symbol-name sym) (text) (node)) "")))
     ,@body))

@export
(defun make-rss-item (item)
  (lquery:initialize item)
  (flet ((dehtml (h) (plump:text (plump:parse h)))
         (get-category-names (it) ;;; TODO: simplify this---Ask Shinmera on IRC
           (if (not (equalp #() it))
             (map 'vector
                  (lambda (x) (plump:text (elt (plump:children x) 0)))
                  it)
             #())))
    (let* ((content-encoded (lquery:$ (children) (tag-name "content:encoded")))

           (description-element (default-when content-encoded (emptyp content-encoded)
                                  (lquery:$ (children "description"))))

           (description-raw (normalize-html
                              (default-when description-element (emptyp description-element)
                                (extract-text "description"))))

           (description-munged (dehtml description-raw))
           (category (get-category-names (lquery:$ "category"))))
           ;(enclosure) --- TODO: implement comment / enclosure handling
           
      (xml-text-bind (title link guid pub-date source comments)
        (make-instance 'rss-item :item item
                       :title title :link link :description-raw description-raw :description description-munged
                       :category category :guid guid :pub-date pub-date :source source :comments comments)))))
      ;(setf (rss-item-enclosure result) enclosure)      -- TODO: comment/enclosure . . .

@export
(defun make-rss-feed (feed)
  (lquery:initialize feed)
  (let* ((channel (lquery:$ "channel" (node)))
         (link (extract-text "link"))
         (link (if (string= link "") (lquery:$ "channel" (children) (tag-name "atom:link") ()) link))
         (items (lquery:$ "item")))
    (xml-text-bind (title description)
      (make-instance 'rss-feed :feed feed
                     :title title :link link :description description
                     :channel channel :items (loop for it across items
                                                   collect (make-rss-item it))))))
@export
(defun store-feed (doc uid)
  (postmodern:with-transaction ()
    (let* ((rss-feed- (make-rss-feed doc))
           (feedid (anaphora:aif (postmodern:query (:select 'id :from 'rssfeed
                                                    :where (:= 'link (rss-feed-link rss-feed-))))
                     (caar anaphora:it) ;; The postmodern query returns a nested list
                     (slot-value (serialize rss-feed-) 'id))))
      (postmodern:query
        (:insert-into 'subscriptions :set 'uid uid 'feedid feedid)))))