(in-package #:chatikbot) (defstruct feed id url title next-fetch (period 300)) (defstruct feed-item feed guid link title description published) (defparameter *rss-min-period* 60 "Min rss refresh period in seconds") (defparameter *rss-max-period* 1800 "Max rss refresh period in seconds") (defparameter *rss-change-rate* 0.1 "Refresh period adjustment rate") (defun find-rss-links (url) (handler-case (multiple-value-bind (root uri) (xml-request url) (loop for link in (get-by-tag root "link") when (string= "application/rss+xml" (plump:attribute link "type")) collect (list (plump:attribute link "title") (puri:render-uri (puri:merge-uris (puri:uri (plump:attribute link "href")) uri) nil)))) (error (e) (log:error e)))) (defun build-feed (url) (let ((root (xml-request url))) (alexandria:when-let (rss (car (get-by-tag root "rss"))) (make-feed :url url :title (child-text rss "title"))))) (defun adjust-period (period new-items) "Adjust the period of feed based on whenever there were new items. With clamping" (let ((diff (round (* period *rss-change-rate*)))) (min *rss-max-period* (max *rss-min-period* (- period (* diff (if (zerop new-items) -1 new-items))))))) (defun need-fetch-p (feed) (or (null (feed-next-fetch feed)) (local-time:timestamp> (local-time:now) (feed-next-fetch feed)))) (defun refresh-feed (feed &optional (skip-p #'not)) (let* ((items (loop for item in (ignore-errors (fetch-feed-items feed)) unless (funcall skip-p item) collect item)) (new-period (adjust-period (feed-period feed) (length items)))) (setf (feed-period feed) new-period (feed-next-fetch feed) (local-time:timestamp+ (local-time:now) new-period :sec)) items)) (defun trim-nil (text) (when text (let ((text (string-trim " " text))) (unless (zerop (length text)) text)))) (defun text-with-cdata (node) "Compiles all text nodes within the nesting-node into one string." (with-output-to-string (stream) (labels ((r (node) (loop for child across (plump:children node) do (typecase child (plump:text-node (write-string (plump:text child) stream)) (plump:cdata (write-string (plump:text child) stream)) (plump:nesting-node (r child)))))) (r node)))) (defun child-text (node tag) (alexandria:when-let (child (car (get-by-tag node tag))) (trim-nil (text-with-cdata child)))) (defun clean-text (text) (when text (trim-nil (plump:text (plump:parse text))))) (defun fetch-feed-items (feed) (let ((plump:*tag-dispatchers* plump:*xml-tags*)) (loop for item in (get-by-tag (xml-request (feed-url feed)) "item") collect (make-feed-item :feed feed :guid (or (child-text item "guid") (child-text item "link")) :link (child-text item "link") :title (clean-text (child-text item "title")) :description (clean-text (child-text item "description")) :published (ignore-errors (alexandria:when-let (pub (or (child-text item "pubDate") (child-text item "published"))) (local-time:universal-to-timestamp (date-time-parser:parse-date-time pub)))))))) (defun format-feed-item (item) (format nil "~A~@[ @ ~A~]~%~A~%~A" (feed-title (feed-item-feed item)) (alexandria:when-let (ts (feed-item-published item)) (local-time:format-timestring nil ts :format '((:year 2) "-" (:month 2) "-" (:day 2) " " (:hour 2) ":" (:min 2)))) (feed-item-title item) (feed-item-link item))) (defun feed-next-fetch-unix (feed) (alexandria:when-let (ts (feed-next-fetch feed)) (local-time:timestamp-to-unix ts))) (defun feed-item-published-unix (item) (alexandria:when-let (ts (feed-item-published item)) (local-time:timestamp-to-unix ts)))