123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- ;; RSS bot for Pleroma
- ;;
- ;; Publishes Stallman's political notes.
- ;; On the first run will ask for authorization and will spam with posts.
- ;; Run it with crontab every now and then.
- ;; The "read" RSS items are stored in the local file "_read_items".
- ;; The client is serialized to the local file "_client". If there is
- ;; an error you can regenerate the authorization data by removing the
- ;; "_client" file.
- ;; NOTE: if running inside Emacs using geiser: C-c C-e C-l to add "." to the load path.
- (use-modules
- (ice-9 rdelim)
- (ice-9 iconv)
- (ice-9 format)
- (ice-9 match)
- (ice-9 textual-ports)
- (rnrs bytevectors)
- (srfi srfi-1)
- (srfi srfi-9)
- (srfi srfi-11)
- (web uri)
- (web response)
- (web client)
- (sxml simple)
- (sxml match)
- ((sxml xpath) #:select (sxpath))
- ;; local
- (tapris client))
- (define instance (string->uri "https://satania.space"))
- (define rss-uri (string->uri "https://stallman.org/rss/rss.xml"))
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;; AUTHORIZATION
- (define (ask-for-code client)
- (let ((u (build-authorize-url client)))
- (format #t "Please visit the following URL to obtain the \
- authorization code: ~a\nPlease input the code: " (uri->string u))))
- (define (client->list client)
- "Serialize a <client> into a list."
- (match client
- (($ <client> instance id secret token)
- (list (uri->string instance) id secret token))))
- (define (list->client ls)
- "Deserialize a <client> from a list."
- (match ls
- ((instance id secret token)
- (make-client (string->uri instance) id secret token))))
- (define (new-client instance)
- "Request new authorization data and save it to `_client'."
- (let ((client (register-app instance)))
- (ask-for-code client)
- (let* ((auth-code (read-line))
- (token (get-token client auth-code)))
- (set-client-token! client token)
- (format #t "Trying to verify the token ~a.\n" auth-code)
- (catch 'pleroma (lambda () (verify-credentials client))
- (lambda (keys . args)
- (format #t "Error!\n")
- (for-each (lambda (a) (format #t "-- ~a\n" a)) args)
- (exit 1)))
- (format #t "Verifcation succeeded! Saving the client data locally.\n")
- (let ((port (open-output-file "_client")))
- (write (client->list client) port)
- (close-port port)
- client))))
- (define (obtain-client)
- "Try to either obtain existing client authorization data from
- _client, or request new authorization data."
- (define (try-read)
- (let* ((port (open-input-file "_client"))
- (client (list->client (read port))))
- (close-port port)
- (catch 'pleroma (lambda () (verify-credentials client))
- (lambda (keys . args)
- (format #t "The existing credentials are not valid! Trying to request new ones..\n")
- (new-client instance)))
- client))
- (catch 'system-error try-read
- (lambda (key . args)
- (match args
- (("open-file" fmt . rest)
- ;; Need to get a new token.
- (new-client instance))
- (_ (apply throw (cons 'system-error args)))))))
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;; RSS
- (define-record-type <rss-item>
- (make-rss-item guid title link description date)
- rss-item?
- (guid rss-item-guid)
- (title rss-item-title)
- (link rss-item-link)
- (description rss-item-description)
- (date rss-item-date))
- (define (download-sxml uri)
- "Download an XML file from the URI and convert it to SXML."
- (let-values (((res body)
- (http-get uri
- #:body #f
- #:version '(1 . 1)
- #:keep-alive? #f
- #:headers '()
- #:decode-body? #t
- #:streaming? #f)))
- (match (response-code res)
- (200
- (xml->sxml (if (bytevector? body)
- (bytevector->string body "utf-8")
- body)
- #:trim-whitespace? #t))
- (_
- ;; Error
- (throw 'tapris `(("request" . ,uri)
- ("response-code" . ,(response-code res))
- ("response-phrase" .
- ,(response-reason-phrase res))
- ("response" .
- ,(if (bytevector? body)
- (bytevector->string body "utf-8")
- body))))))))
- (define (local-sxml file)
- "Read XML as SXML from a local file."
- (let* ((port (open-input-file file))
- (text (get-string-all port))
- (res (xml->sxml text #:trim-whitespace? #t)))
- (close-port port)
- res))
- (define (sxml->rss-item sxml)
- "Parse a single RSS item in SXML as an <rss-item>."
- (sxml-match sxml
- [(item (title ,title) (link ,link) (guid ,guid)
- (description ,desc) (pubDate ,date) . ,rest)
- (make-rss-item
- guid
- title
- link
- desc
- date)]
- [,otherwise (throw 'tapris `((error "sxml->rss-item: match failed") (args . ,sxml)))]))
- (define (get-read-items)
- "Reads the list of GUIDs of all the read items from `_read_items'."
- (let* ((port (open-file "_read_items" "a+"))
- (guids (read port)))
- (close-port port)
- (unless (or (list? guids)
- (eof-object? guids))
- (throw 'system-error `((file "_read_items")
- (contents ,guids)
- (error "Content is not a list"))))
- (if (eof-object? guids) '() guids)))
- (define (set-read-items! guids)
- "Writes the list of GUIDs for all the read items to `_read_items'."
- (let* ((port (open-file "_read_items" "w")))
- (write guids port)
- (close-port port)))
- (define (mark-read-items items)
- "Take a list of <rss-item>s, filter out the unread ones, and mark
- them as read. Return the list of the unread items."
- (let* ((guids (get-read-items))
- (f (lambda (item) (not (member (rss-item-guid item) guids))))
- (unread-items (filter f items))
- (unread-guids (map rss-item-guid unread-items))
- (total-read-guids (append unread-guids guids)))
- (set-read-items! total-read-guids)
- unread-items))
- ;; MAIN
- (define (display-entry item)
- (format #t "~a: ~a\n" (rss-item-guid item) (rss-item-title item)))
- (define (entries) (map sxml->rss-item
- ((sxpath '(// item))
- (download-sxml rss-uri))))
- (define (main)
- (let* ((client (obtain-client))
- (all-items (entries))
- (items (mark-read-items all-items))
- (publish (lambda (item)
- (display-entry item)
- (let ((status (format #f "~a\n<br /><p><a href=\"~a\">~a</a></p>"
- (rss-item-description item)
- (rss-item-link item)
- (rss-item-link item))))
- (post-status client status)))))
- (format #t "Logged in as ~a.\n" (assoc-ref (verify-credentials client) "acct"))
- (format #t "Publishing new entries...\n")
- (for-each publish (reverse items))))
- (main)
|