Skip to content

Instantly share code, notes, and snippets.

@gkbrk
Last active January 20, 2023 06:20
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save gkbrk/771852072ed5a6715882bfac734fdd36 to your computer and use it in GitHub Desktop.
Save gkbrk/771852072ed5a6715882bfac734fdd36 to your computer and use it in GitHub Desktop.
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)
@vindarel
Copy link

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 the str 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/

@PuercoPop
Copy link

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

(loop
  :for post :in (get-rss-feed)
  :repeat 5 :collect post)

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