;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; NewsPhaser, Mastodon posts aggregator ;; Copyright (C) 2025 Gene Pasquet ;; ;; This program is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (in-package :phaser) (defvar *timeline-page-size* 50 "Maximum number of statuses retrieved from the timeline for each request") (defvar *timeline-min-age* (* 24 3600) "Minimum age of statuses before being considered for ranking") (defvar *timeline-max-age* (* 48 3600) ; 48h "Maximum age of statuses to be retrieved from timelines.") (defvar *timeline-min-timestamp* (- (get-universal-time) *timeline-max-age*)) (defvar *timeline-max-timestamp* (- (get-universal-time) *timeline-min-age*)) (defvar *pop-favs-factor* 1) (defvar *pop-boosts-factor* 1) (defvar *pop-replies-factor* 1) (defvar *max-requests* 20) (defvar *top-statuses-to-boost* 10) (defvar *excerpt-length* 100) (defun outdated-statuses (page) (remove-if (lambda (status) (> (tooter:created-at status) *timeline-min-timestamp*)) page)) (defun last-page-p (page) (or (null page) (not (null (outdated-statuses page))))) (defun load-timeline-pages (min-id &optional (pages '()) (iter-num 0)) "Load statuses from the timeline up to *TIMELINE-MAX-AGE* or MIN-ID." (let* ((max-id (if (null pages) nil (tooter:id (car (last (car pages)))))) (page (get-timeline :timeline :home :limit *timeline-page-size* :max-id max-id :min-id min-id)) (statuses-and-page (if (null pages) (list page) (cons page pages)))) (if (or (last-page-p page) (>= iter-num *max-requests*)) (alexandria:flatten statuses-and-page) (load-timeline-pages min-id statuses-and-page (1+ iter-num))))) (defun load-timeline () (remove-if (lambda (status) (let ((status-timestamp (tooter:created-at status))) (or (< status-timestamp *timeline-min-timestamp*) (> status-timestamp *timeline-max-timestamp*)))) (load-timeline-pages nil))) (defun status-popularity (status) (/ (+ (* (tooter:replies-count status) *pop-replies-factor*) (* (tooter:favourites-count status) *pop-favs-factor*) (* (tooter:reblogs-count status) *pop-boosts-factor*)) (- (get-universal-time) (tooter:created-at status)))) (defun load-top-statuses () (let* ((timeline (load-timeline)) (ranked-statuses (sort (mapcar (lambda (status) `(,(status-popularity status) . ,status)) timeline) '> :key 'car))) (mapcar 'cdr (butlast ranked-statuses (- (length ranked-statuses) 10))))) (defun summarize-top-statuses () (let ((statuses (load-top-statuses))) (remove-if #'null (mapcar (lambda (status) (let ((excerpt (text-excerpt (tooter:content status) *excerpt-length*))) (if excerpt (format nil "~a:~%~a" excerpt (tooter:url status)) nil))) statuses)))) (defun summarize-and-post-top-statuses () (let ((summaries (summarize-top-statuses))) (post-toot (format nil "~{~a~%~%~}" summaries)))) (when nil (summarize-top-statuses) (summarize-and-post-top-statuses))