# Announces new Trump posts via trumpstruth.org RSS
# Feed: https://www.trumpstruth.org/feed
catch {setudef flag enabletruth}
catch {setudef flag enableTruth}
namespace eval ::trumptruthrss {
variable cfg
array set cfg {
feed_url "https://www.trumpstruth.org/feed"
poll_seconds 3600
curl "/usr/bin/curl"
curl_timeout 15
user_agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
state_file "trumptruthrss.state"
max_chars 420
strip_non_ascii 1
exclude_retruths 1
max_new_announce 5
debug 0
}
variable last_guid ""
variable in_progress 0
variable timer_id ""
}
if {[info commands lreverse] eq ""} {
proc lreverse {lst} {
set out {}
foreach x $lst { set out [linsert $out 0 $x] }
return $out
}
}
proc ::trumptruthrss::dlog {msg} {
variable cfg
if {$cfg(debug)} { putlog "trumptruthrss: $msg" }
}
proc ::trumptruthrss::chan_enabled {chan} {
if {![catch {set v [channel get $chan enabletruth]}]} { return $v }
if {![catch {set v [channel get $chan enableTruth]}]} { return $v }
putlog "trumptruthrss: channel flag enabletruth/enableTruth not defined"
return 0
}
proc ::trumptruthrss::enabled_channels {} {
set out {}
foreach c [channels] {
if {[::trumptruthrss::chan_enabled $c]} { lappend out $c }
}
return $out
}
proc ::trumptruthrss::load_state {} {
variable cfg
variable last_guid
if {![file exists $cfg(state_file)]} { set last_guid ""; return }
catch {
set f [open $cfg(state_file) r]
set last_guid [string trim [read $f]]
close $f
}
}
proc ::trumptruthrss::save_state {} {
variable cfg
variable last_guid
catch {
set f [open $cfg(state_file) w]
puts $f $last_guid
close $f
}
}
proc ::trumptruthrss::is_retruth {item} {
# look for "ReTruth", "RT @user ..."
# Sometimes the link points away from @realDonaldTrump
set raw [string tolower [dict get $item text]]
if {[string first "retruth" $raw] >= 0} { return 1 }
set clean [string tolower [::trumptruthrss::strip_html [dict get $item text]]]
if {[string first "rt @" $clean] == 0} { return 1 }
if {[dict exists $item link]} {
set link [string tolower [dict get $item link]]
if {$link ne "" && [string first "%40realdonaldtrump" $link] < 0 && [string first "/@realdonaldtrump" $link] < 0} {
return 1
}
}
return 0
}
proc ::trumptruthrss::curl_fetch {url} {
variable cfg
set cmd [list $cfg(curl) -s -L --compressed -m $cfg(curl_timeout) \
-H "Accept: application/rss+xml, application/xml;q=0.9, */*;q=0.8" \
-H "User-Agent: $cfg(user_agent)" \
-w "\n__CURLMETA__:%{http_code}:%{content_type}\n" \
$url \
]
dlog "Fetching: $url"
if {[catch {set out [eval exec $cmd]} err]} {
putlog "trumptruthrss: curl failed: $err"
return [list 0 "" ""]
}
set code 0
set ctype ""
if {[regexp {__CURLMETA__:(\d+):([^\r\n]+)} $out -> code ctype]} {
regsub {(\r?\n)?__CURLMETA__:[0-9]+:[^\r\n]+(\r?\n)?$} $out "" body
} else {
set body $out
}
dlog "HTTP $code (ctype=$ctype) bytes=[string length $body]"
return [list $code $ctype $body]
}
proc ::trumptruthrss::html_decode {s} {
set s [string map {
"&" "&"
"<" "<"
">" ">"
""" "\""
""" "\""
"'" "'"
"'" "'"
" " " "
} $s]
return $s
}
proc ::trumptruthrss::strip_html {html} {
set s $html
regsub -all -nocase {<br\s*/?>} $s "\n" s
regsub -all -nocase {</p>\s*<p>} $s "\n" s
regsub -all {<[^>]+>} $s "" s
set s [::trumptruthrss::html_decode $s]
regsub -all {[[:space:]]+} $s " " s
return [string trim $s]
}
proc ::trumptruthrss::rss_gettag {block tag} {
set open "<$tag"
set close "</$tag>"
set i [string first $open $block]
if {$i < 0} { return "" }
set gt [string first ">" $block $i]
if {$gt < 0} { return "" }
set j [string first $close $block $gt]
if {$j < 0} { return "" }
set val [string range $block [expr {$gt+1}] [expr {$j-1}]]
set val [string trim $val]
if {[string first {<![CDATA[} $val] == 0} {
set end [string last {]]>} $val]
if {$end > 9} {
set val [string range $val 9 [expr {$end-1}]]
set val [string trim $val]
}
}
}
proc ::trumptruthrss::parse_feed {xml} {
set head [string tolower [string range $xml 0 200]]
if {[string first "<!doctype" $head] >= 0 || [string first "<html" $head] >= 0} {
putlog "trumptruthrss: Got HTML instead of RSS/XML."
return {}
}
if {([string first "<?xml" $head] < 0) && ([string first "<rss" $head] < 0) && ([string first "<feed" $head] < 0)} {
putlog "trumptruthrss: Response doesn't look like RSS/XML. Head: [string range $xml 0 120]"
return {}
}
set items {}
set pos 0
while {1} {
set s [string first "<item" $xml $pos]
if {$s < 0} break
set e [string first "</item>" $xml $s]
if {$e < 0} break
lappend items [string range $xml $s [expr {$e+6}]]
set pos [expr {$e+7}]
}
if {[llength $items] == 0} {
putlog "trumptruthrss: No <item> elements found."
return {}
}
set out {}
foreach it $items {
set guid [rss_gettag $it "guid"]
set link [rss_gettag $it "link"]
set title [rss_gettag $it "title"]
set desc [rss_gettag $it "description"]
set enc [rss_gettag $it "content:encoded"]
set pub [rss_gettag $it "pubDate"]
set text ""
if {$enc ne ""} {
set text $enc
} elseif {$desc ne ""} {
set text $desc
} else {
set text $title
}
if {$guid eq ""} {
if {$link ne ""} { set guid $link } else { set guid $title }
}
lappend out [dict create guid $guid link $link text $text pubDate $pub]
}
return $out
}
proc ::trumptruthrss::format_item {item} {
variable cfg
set text [::trumptruthrss::strip_html [dict get $item text]]
regsub -all {[[:space:]]+} $text " " text
set text [string trim $text]
if {$cfg(strip_non_ascii)} {
regsub -all {[^\x00-\x7F]} $text "" text
}
if {[string length $text] > $cfg(max_chars)} {
set text "[string range $text 0 [expr {$cfg(max_chars)-4}]]..."
}
set pub [dict get $item pubDate]
set link [dict get $item link]
set msg "Orange Truth: $text"
if {$pub ne ""} { append msg " | $pub" }
if {$link ne ""} { append msg " | $link" }
return $msg
}
proc ::trumptruthrss::announce_items {items} {
set chans [::trumptruthrss::enabled_channels]
if {[llength $chans] == 0} { return }
foreach it $items {
set msg [::trumptruthrss::format_item $it]
foreach c $chans {
putserv "PRIVMSG $c :$msg"
}
}
}
proc ::trumptruthrss::schedule {} {
variable cfg
variable timer_id
if {$timer_id ne ""} { catch {killutimer $timer_id} }
set timer_id [utimer $cfg(poll_seconds) ::trumptruthrss::poll]
}
proc ::trumptruthrss::poll {} {
variable cfg
variable last_guid
variable in_progress
::trumptruthrss::schedule
if {$in_progress} { return }
set in_progress 1
if {[llength [::trumptruthrss::enabled_channels]] == 0} {
set in_progress 0
return
}
lassign [::trumptruthrss::curl_fetch $cfg(feed_url)] code ctype body
if {$code != 200 || $body eq ""} {
putlog "trumptruthrss: fetch failed (HTTP=$code ctype=$ctype)"
set in_progress 0
return
}
set items [::trumptruthrss::parse_feed $body]
if {[info exists cfg(exclude_retruths)] && $cfg(exclude_retruths)} {
set filtered {}
foreach it $items {
if {[::trumptruthrss::is_retruth $it]} { continue }
lappend filtered $it
}
set items $filtered
}
if {[llength $items] == 0} { set in_progress 0; return }
if {$last_guid eq ""} {
set last_guid [dict get [lindex $items 0] guid]
::trumptruthrss::save_state
set in_progress 0
return
}
set new {}
set found 0
foreach it $items {
set g [dict get $it guid]
if {$g eq $last_guid} { set found 1; break }
lappend new $it
}
if {[llength $new] == 0} { set in_progress 0; return }
if {[llength $new] > $cfg(max_new_announce)} {
set new [lrange $new 0 [expr {$cfg(max_new_announce)-1}]]
}
::trumptruthrss::announce_items [lreverse $new]
set last_guid [dict get [lindex $items 0] guid]
::trumptruthrss::save_state
set in_progress 0
}
proc ::trumptruthrss::cmd_latest {nick host hand chan text} {
variable cfg
if {![::trumptruthrss::chan_enabled $chan]} { return }
lassign [::trumptruthrss::curl_fetch $cfg(feed_url)] code ctype body
if {$code != 200 || $body eq ""} {
putserv "PRIVMSG $chan :Orange Truth: couldn't fetch feed (HTTP=$code)."
return
}
set items [::trumptruthrss::parse_feed $body]
if {[llength $items] == 0} {
putserv "PRIVMSG $chan :Orange Truth: couldn't parse feed."
return
}
putserv "PRIVMSG $chan :[::trumptruthrss::format_item [lindex $items 0]]"
}
proc ::trumptruthrss::cmd_sync {nick host hand chan text} {
variable cfg
variable last_guid
if {![::trumptruthrss::chan_enabled $chan]} { return }
lassign [::trumptruthrss::curl_fetch $cfg(feed_url)] code ctype body
if {$code != 200 || $body eq ""} {
putserv "PRIVMSG $chan :Orange Truth: couldn't sync (HTTP=$code)."
return
}
set items [::trumptruthrss::parse_feed $body]
if {[llength $items] == 0} {
putserv "PRIVMSG $chan :Orange Truth: couldn't parse feed."
return
}
# Collect items newer than last_guid
set new {}
if {$last_guid ne ""} {
foreach it $items {
set g [dict get $it guid]
if {$g eq $last_guid} break
lappend new $it
}
} else {
# empty, treat everything in feed as "new"
set new $items
}
if {[llength $new] == 0} {
putserv "PRIVMSG $chan :Orange Truth: no new posts."
return
}
# Announce oldest to newest, paced
set new [lreverse $new]
set delay 0
foreach it $new {
set msg [::trumptruthrss::format_item $it]
utimer $delay [list putserv "PRIVMSG $chan :$msg"]
incr delay 2
}
# Update pointer to newest item in feed
set last_guid [dict get [lindex $items 0] guid]
::trumptruthrss::save_state
putserv "PRIVMSG $chan :Orange Truth: synced (+[llength $new] announced)."
}
catch {unbind pub - "!truth" ::trumptruthrss::cmd_latest}
catch {unbind pub - "!truthsync" ::trumptruthrss::cmd_sync}
bind pub - "!truth" ::trumptruthrss::cmd_latest
bind pub - "!truthsync" ::trumptruthrss::cmd_sync
::trumptruthrss::load_state
::trumptruthrss::schedule
putlog "trumptruths.tcl loaded - Enable with: .chanset #chan +enabletruth"