#!/bin/bash # FreshRSS Smart Digest # Pulls unread articles, ranks them by relevance, delivers categorized summary # Runs daily at 7:00 AM (before AI newsletter digest) set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../../.env" 2>/dev/null || true # Config FRESHRSS_URL="${FRESHRSS_URL:-http://freshrss.kangaroo-eel.ts.net}" FRESHRSS_USER="${FRESHRSS_USER:-anthony}" TELEGRAM_CHAT="${TELEGRAM_CHAT:-1793951355}" NTFY_URL="${NTFY_URL:-}" NTFY_TOPIC="${NTFY_TOPIC:-}" NTFY_MIN_PRIORITY="${NTFY_MIN_PRIORITY:-4}" # Your interest keywords for relevance ranking INTERESTS=( "AI" "artificial intelligence" "machine learning" "LLM" "renewable energy" "solar" "wind" "battery" "EV" "electric vehicle" "politics" "Labor" "election" "government" "LGBTQ" "LGBT" "queer" "transgender" "Perth" "Western Australia" "WA" "climate" "environment" "mental health" "depression" "therapy" ) # Priority sources (always include these) PRIORITY_SOURCES=( "CNN" "MSNBC" "Al Jazeera" "ABC News" "The Guardian" "BlueSky" ) log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" } fetch_unread() { log "Fetching unread articles from FreshRSS..." local raw_output raw_output=$(/home/openclaw/.openclaw/workspace/skills/freshrss-reader/scripts/freshrss.sh headlines \ --unread --count 50 2>/dev/null) || { log "ERROR: Failed to fetch from FreshRSS" return 1 } echo "$raw_output" } # Score an article based on interest keywords score_article() { local title="$1" local source="$2" local categories="$3" local score=0 local text="${title,,} ${categories,,}" # Check interest keywords for interest in "${INTERESTS[@]}"; do if [[ "$text" == *"${interest,,}"* ]]; then ((score+=2)) fi done # Priority sources get a boost for priority in "${PRIORITY_SOURCES[@]}"; do if [[ "$source" == *"$priority"* ]]; then ((score+=3)) fi done echo "$score" } # Parse articles and build digest build_digest() { local raw="$1" local output="" local must_read="" local skimmable="" local total_count=0 # Parse the output - each article is 3 lines: date/source/title, URL, categories local current_date="" local current_source="" local current_title="" local current_url="" local current_cats="" while IFS= read -r line; do # Skip empty lines [[ -z "$line" ]] && continue # Check if this is a title line (starts with [) if [[ "$line" =~ ^\[.+Z\].*\]\ (.*)$ ]]; then # Save previous article if exists if [[ -n "$current_title" ]]; then local score score=$(score_article "$current_title" "$current_source" "$current_cats") local article_formatted="• [${current_title}](${current_url}) \n _${current_source}_\n" if ((score >= 5)); then must_read+="$article_formatted\n" elif ((score >= 2)); then skimmable+="$article_formatted\n" fi ((total_count++)) fi # Parse new article - format: [timestamp] [source] title current_date=$(echo "$line" | sed 's/^\[\([^]]*\)\].*/\1/') current_source=$(echo "$line" | sed -E 's/^\[.+Z\] \[([^]]+)\].*/\1/') current_title=$(echo "$line" | sed -E 's/^\[.+Z\] \[[^]]+\] //') current_url="" current_cats="" # Check if this is a URL line elif [[ "$line" =~ ^[[:space:]]*https?:// ]]; then current_url="$(echo "$line" | xargs)" # Check if this is categories line elif [[ "$line" =~ ^Categories:\ ]]; then current_cats=$(echo "$line" | sed 's/Categories: //') fi done <<< "$raw" # Don't forget the last article if [[ -n "$current_title" ]]; then local score score=$(score_article "$current_title" "$current_source" "$current_cats") local article_formatted="• [${current_title}](${current_url}) \n _${current_source}_\n" if ((score >= 5)); then must_read+="$article_formatted\n" elif ((score >= 2)); then skimmable+="$article_formatted\n" fi ((total_count++)) fi # Limit items per section to keep message under Telegram's 4096 char limit local must_read_limited="$(echo -e "$must_read" | head -n 16)" # ~8 items (2 lines each) local skimmable_limited="$(echo -e "$skimmable" | head -n 10)" # ~5 items (2 lines each) # Build final message output="šŸ“° *FreshRSS Daily Digest*\n\n" output+="Found *$total_count* unread articles.\n\n" if [[ -n "$must_read" ]]; then local count_must=$(($(echo -e "$must_read" | grep -c '^•') + 0)) output+="šŸ”„ *Must Read* ($count_must)\n$must_read_limited" [[ "$must_read" != "$must_read_limited" ]] && output+="\n _...and more_\n\n" || output+="\n" fi if [[ -n "$skimmable" ]]; then local count_skim=$(($(echo -e "$skimmable" | grep -c '^•') + 0)) output+="šŸ“Ž *Skimmable* ($count_skim)\n$skimmable_limited" [[ "$skimmable" != "$skimmable_limited" ]] && output+="\n _...and more_\n\n" || output+="\n" fi if [[ -z "$must_read" && -z "$skimmable" ]]; then output+="No high-priority articles today. You're caught up! šŸŽ‰\n" fi output+="\nšŸ“– [Open FreshRSS]($FRESHRSS_URL)" # Truncate if still too long (Telegram limit is 4096) if [[ ${#output} -gt 4000 ]]; then output="${output:0:3950}...\n\n_(truncated — more articles in FreshRSS)_\n\nšŸ“– [Open FreshRSS]($FRESHRSS_URL)" fi echo -e "$output" } send_telegram() { local message="$1" log "Sending to Telegram..." if [[ -z "${TELEGRAM_BOT_TOKEN:-}" ]]; then log "ERROR: TELEGRAM_BOT_TOKEN is not set" return 1 fi local tg_response tg_response=$(curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -H "Content-Type: application/json" \ -d "{ \"chat_id\": \"$TELEGRAM_CHAT\", \"text\": \"$message\", \"parse_mode\": \"Markdown\", \"disable_web_page_preview\": true }") || { log "ERROR: Failed to send Telegram message" return 1 } if ! echo "$tg_response" | jq -e '.ok == true' > /dev/null 2>&1; then log "ERROR: Telegram API rejected message: $tg_response" return 1 fi log "Sent to Telegram successfully" } send_ntfy() { local title="$1" local message="$2" local priority="${3:-4}" local sound="${4:-default}" [[ -z "$NTFY_URL" ]] && return 0 [[ -z "$NTFY_TOPIC" ]] && return 0 # Enforce minimum priority (default 4) if [[ "$priority" =~ ^[0-9]+$ ]] && [[ "$NTFY_MIN_PRIORITY" =~ ^[0-9]+$ ]]; then if (( priority < NTFY_MIN_PRIORITY )); then priority="$NTFY_MIN_PRIORITY" fi fi log "Sending to ntfy..." curl -s -X POST "${NTFY_URL%/}/${NTFY_TOPIC}" \ -H "Title: $title" \ -H "Priority: $priority" \ -H "Sound: $sound" \ -d "$message" > /dev/null || { log "ERROR: Failed to send ntfy message" return 1 } log "Sent to ntfy successfully" } main() { log "Starting FreshRSS digest..." # Fetch articles local raw_articles raw_articles=$(fetch_unread) || exit 1 # Build digest local digest digest=$(build_digest "$raw_articles") # Send to both channels send_telegram "$digest" # Plain text version for ntfy (strip markdown) local plain_digest plain_digest=$(echo -e "$digest" | sed 's/\*//g' | sed 's/\[\([^]]*\)\]([^)]*)/\1/g') send_ntfy "FreshRSS Digest" "$plain_digest" 4 default log "Digest complete!" } main "$@"