The code has been written presumably by something learning Common Lisp, so with that in mind I’ve taken the liberty written down some feedback and some style adjustments.
#|
To make sure you've loaded the dependencies in your image run
(ql:quickload '("drakma" "plump" "babel" "tooter"))
One can evaluate comments from SLY/SLIME so its better to leave the form
commented out so one can compile file in one go without invoking QUICKLOAD.
The only real problem with the code is that it opens a file to see if a link
has been processed. With judicious use of global variables we can avoid this,
by keeping the list of processed links in memory. We still update it as we go.
In the main entry-point if we want to filter an process instead of
REMOVE-IF-NOT + LOOP we can use LOOP's WHEN clause instead.
It is better to use the most specific construct when possible, MAPCAR instead
of (MAP 'list ...). I would assume a vector if MAP was used instead of MAPCAR.
You could use the structure constructor in PARSE-RSS-ITEM to make the code less
'side-effect'ful (still imperative though nothing wrong with
that). PARSE-RSS-ITEM would look like
(defun parse-rss-item (item)
"Parse an RSS item into a lobsters-post"
(let ((guid (get-first-text "guid" item))
(title (get-first-text "title" item))
(link (get-first-text "link" item)))
(make-lobsters-post :guid guid
:title title
:url link)))
Note that for the cases where we want to create a resource, modify it and then
return it, the SERAPEUM library has RLET[0].
If we are going to depart from the archaic -p convention for predicates lets at
least do so for a better one, which is to end the predicate's name with ?
instead of is-foo.
As a matter of style, please don't leave dangling parens :pray:. And use
uppercase letters for the format directives.
A couple of 'dependencies' where unused and can be safely removed. Note that
TOOTER is already using YASON library so it would make sense to use that JSON
library instead of CL-JSON?
[0]: https://github.com/ruricolist/serapeum/blob/master/REFERENCE.md#lret-rest-bindings-body-body
|#
(defpackage "MASTODON-LOBSTERS"
(:use "CL"
"BABEL"))
(in-package "MASTODON-LOBSTERS")
(defparameter +feed-path+ "https://lobste.rs/rss")
(setf drakma:*drakma-default-external-format* :utf8)
(defparameter +mastodon-client+
(make-instance 'tooter:client
:base "https://botsin.space"
:name "lobsterbot"
:key "secret"
:secret "secret"
:access-token "secret"))
(defparameter +link-db-path+ #P"/Users/puercopop/links.txt")
(defparameter +processed-links+
(with-open-file (stream +link-db-path+
:if-does-not-exist :create)
(loop
:for line := (read-line stream nil)
:while line
:collect line))
"A collection of the previously seen links.")
(defun processed? (post)
"Have we already processed the POST?"
(find (post-guid post) +processed-links+ :key 'post-guid :test #'string=))
(defclass lobsters-post ()
((guid :initarg :guid :reader post-guid)
(title :initarg :title :reader post-title)
(url :initarg :url :reader post-url)))
(defmethod print-object ((post lobsters-post) stream)
(print-unreadable-object (post stream :type t)
(format stream "~A - ~A ~A"
(post-guid post)
(post-title post)
(post-url post))))
(defun get-first-text (tag node)
"Search the XML node for the given tag name and return the text of the first one"
(plump:render-text (car (plump:get-elements-by-tag-name node tag))))
(defun parse-rss-item (item)
"Parse an RSS item into a lobsters-post"
(let ((guid (get-first-text "guid" item))
(title (get-first-text "title" item))
(link (get-first-text "link" item)))
(make-instance 'lobsters-post :guid guid
:title title
:url link)))
(defun get-rss-feed ()
"Gets rss feed of Lobste.rs"
(let* ((xml-text (octets-to-string (drakma:http-request +feed-path+)))
(plump:*tag-dispatchers* plump:*xml-tags*)
(xml-tree (plump:parse xml-text))
(items (plump:get-elements-by-tag-name xml-tree "item")))
(loop :for entry :in items
:collect (parse-rss-item entry))))
(defun send-toot (post)
"Takes a lobsters-post and posts it on Mastodon"
(tooter:make-status +mastodon-client+
(format nil "~A - ~A ~A"
(post-title post)
(post-guid post)
(post-url post))))
(defun record-post (post)
"Notes that the POST has been processed by us."
(push setf +processed-links+
(cons (post-guid post) +processed-links+))
(with-open-file (out +link-db-path+
:direction :output
:if-exists :append
:if-does-not-exist :create)
(format out "~A~%" (post-guid post))))
(defun main ()
(loop
:with count := 0
:for rss-entry :in (get-rss-feed)
:while (> 10 count)
:unless (processed? rss-entry)
:do
(send-toot rss-entry)
(record-link rss-entry)
(incf count)))
#+entry-point
(main)
The code has been written presumably by something learning Common Lisp, so with that in mind I’ve taken the liberty written down some feedback and some style adjustments.