Common lisp Mastodon bot
(ql:quickload :drakma) | |
(ql:quickload :cl-json) | |
(ql:quickload :plump) | |
(ql:quickload :babel) | |
(ql:quickload :tooter) | |
(ql:quickload :split-sequence) | |
(defvar *feed-path* "https://lobste.rs/rss") | |
(setf drakma:*drakma-default-external-format* :UTF-8) | |
(defstruct lobsters-post | |
title | |
url | |
guid | |
) | |
(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* ((post (make-lobsters-post)) | |
) | |
(setf (lobsters-post-title post) (get-first-text "title" item)) | |
(setf (lobsters-post-url post) (get-first-text "link" item)) | |
(setf (lobsters-post-guid post) (get-first-text "guid" item)) | |
post | |
)) | |
(defun get-rss-feed () | |
"Gets rss feed of Lobste.rs" | |
(let* ((xml-text (babel: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")) | |
) | |
(reverse (map 'list #'parse-rss-item items)) | |
)) | |
(defun get-mastodon-client () | |
(make-instance 'tooter:client | |
:base "https://botsin.space" | |
:name "lobsterbot" | |
:key "secret" | |
:secret "secret" | |
:access-token "secret") | |
) | |
(defun send-toot (item) | |
"Takes a lobsters-post and posts it on Mastodon" | |
(tooter:make-status (get-mastodon-client) (format nil "~a - ~a ~a" | |
(lobsters-post-title item) | |
(lobsters-post-guid item) | |
(lobsters-post-url item))) | |
) | |
(defun is-link-seen (item) | |
"Returns if we have processed a link before" | |
(with-open-file (stream "links.txt" | |
:if-does-not-exist :create) | |
(loop for line = (read-line stream nil) | |
while line | |
when (string= line (lobsters-post-guid item)) return t)) | |
) | |
(defun record-link-seen (item) | |
"Writes a link to the links file to keep track of it" | |
(with-open-file (stream "links.txt" | |
:direction :output | |
:if-exists :append | |
:if-does-not-exist :create) | |
(format stream "~a~%" (lobsters-post-guid item))) | |
) | |
(defun run-mastodon-bot () | |
(let* ((first-ten (subseq (get-rss-feed) 0 10)) | |
(new-links (remove-if #'is-link-seen first-ten)) | |
) | |
(loop for item in new-links do | |
(send-toot item) | |
(record-link-seen item)) | |
)) | |
(run-mastodon-bot) |
This comment has been minimized.
This comment has been minimized.
PuercoPop
commented
Aug 21, 2018
•
@vindarel in this case isn't the subseq a list of posts? If so one can get up to 10 items from a list with
But in this case because they want to filter the stories it would make sense to move everything to the loop (defun run-mastodon-bot ()
(loop
:with count := 0
:for rss-entry :in (get-rss-feed)
:while (< count 10)
:unless (link-seen? rss-entry)
:do
(send-toot rss-entry)
(record-link-seen rss-entry)
(incf count))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
vindarel commentedAug 21, 2018
Hey, nice post :)
my lil' review:
subseq
fails if the end limit is superior to the sequence's length. I fix this with(str:substr 0 3 '(:foo))
(function in thestr
library, which happens to work for sequences).To read lines from a file, I'd use
uiop:read-file-lines
.(also there is no error handling).
for cron-like jobs, I once used the nice Clerk library.
https://www.reddit.com/r/lisp/comments/991678/mastodon_bot_in_common_lisp_gokberk_yaltirakli/