qs_net.tcl 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096
  1. # TCL Script to retrieve queries from Quakeservers.net (they come as rss)
  2. # Copyright (C) 2004 Paul-Dieter Klumpp
  3. #
  4. # This program is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU General Public License
  6. # as published by the Free Software Foundation; either version 2
  7. # of the License, or (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  17. # have a binding to call url
  18. # if binding called,
  19. # get url
  20. # if retrieved data
  21. # parse retrieved data
  22. # output to channel where called
  23. namespace eval ::qsnet {
  24. if {[file isfile "scripts/cims/putils.tcl"] == 1} {
  25. source "scripts/cims/putils.tcl"
  26. }
  27. bind pub - !qw ::qsnet::feed_get_qw_and_display
  28. }
  29. namespace eval ::rss-synd {
  30. variable rss
  31. variable default
  32. set rss(qservers) {
  33. "url" "http://www.quakeservers.net/shambler_players.php?n="
  34. "channels" "#quad.dev"
  35. "database" "./scripts/feeds/qservers"
  36. "output" "\[\002!qw fp\002\] @@item!player@@ (@@item!description@@ - qw://@@item!ip@@:@@item!port@@)"
  37. "interval-or-forced" "forced"
  38. }
  39. set default {
  40. "announce-output" 3
  41. "trigger-output" 3
  42. "remove-empty" 1
  43. "trigger-type" 0:2
  44. "announce-type" 0
  45. "max-depth" 5
  46. "evaluate-tcl" 0
  47. "update-interval" 30
  48. "output-order" 0
  49. "timeout" 60000
  50. "channels" "#channel1"
  51. "trigger" "!rss @@feedid@@"
  52. "output" "\[\002@@channel!title@@@@title@@\002\] @@item!title@@@@entry!title@@ - @@item!link@@@@entry!link!=href@@"
  53. "user-agent" "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2"
  54. }
  55. }
  56. proc ::qsnet::feed_get_qw_and_display {nick mask hand chan text} {
  57. # ist im channel erlaubt?
  58. # ist abfrageintervall eingehalten?
  59. # lese parameter
  60. # verändere URL entsprechend..
  61. # verändere channel auf den aufrufenden..
  62. # dann start feed_get
  63. putlog "here i am: $nick $mask $hand $chan $text"
  64. #set rss(qservers) {
  65. # "url" "http://www.quakeservers.net/shambler_players.php?n=$params"
  66. # "channels" "#quad.dev"
  67. # "database" "./scripts/feeds/qservers"
  68. # "output" "\\\[\002qw fp\002\\\] @@item!player@@ (@@item!description@@ - @@item!ip@@:@@item!port@@)"
  69. # "interval-or-forced" "forced"
  70. #}
  71. set tmp_cmd [lindex ${text} 0]
  72. set tmp_value [lindex ${text} 1]
  73. set cmd [string tolower [::putils::kill_spaces ${tmp_cmd}]]
  74. set value [string tolower [::putils::kill_spaces ${tmp_value}]]
  75. if {${cmd} == "fp"} {
  76. # check $value ... if there
  77. if {${value} != ""} {
  78. if {[string length ${value}] > "1"} {
  79. set rofg [::rss-synd::feed_get $value]
  80. if {$rofg == 666} {
  81. ::putils::put_local_msg ${chan} "Mnet! Player not found on any qw server."
  82. }
  83. } else {
  84. ::putils::put_local_msg ${chan} "Mnet! Searchvalue has to be longer than 1 character."
  85. }
  86. } else {
  87. ::putils::put_local_msg ${chan} "Mnet! No searchvalue given.."
  88. }
  89. return 0
  90. }
  91. ::putils::put_local_msg ${chan} "Possible !qw commands are:"
  92. ::putils::put_local_msg ${chan} "'!qw fp xyz' to find a player named xyz"
  93. }
  94. #
  95. # Feed Retrieving Functions
  96. ##
  97. proc ::rss-synd::feed_get {args} {
  98. variable rss
  99. set i 0
  100. foreach name [array names rss] {
  101. if {$i == 3} { break }
  102. array set feed $rss($name)
  103. #putlog "currently on $rss($name)"
  104. # PaulK: let it be called, even if quite up2date .. because it's forced.
  105. if {($feed(updated) <= [expr { [unixtime] - ($feed(update-interval) * 60) }]) || ( $feed(interval-or-forced) == "forced" )} {
  106. ::http::config -useragent $feed(user-agent)
  107. set feed(type) $feed(announce-type)
  108. set feed(headers) [list]
  109. if {$feed(url-auth) != ""} {
  110. lappend feed(headers) "Authorization" "Basic $feed(url-auth)"
  111. }
  112. if {([info exists feed(enable-gzip)]) && ($feed(enable-gzip) == 1)} {
  113. lappend feed(headers) "Accept-Encoding" "gzip"
  114. }
  115. set feedlist "[array get feed] depth 0"
  116. # PaulK: with $args
  117. set http_token [::http::geturl "$feed(url)$args" -timeout $feed(timeout) -headers $feed(headers)]
  118. # PaulK: finally, we can check here what errors really happened - before, with the callback, it wasn't possible HERE in this procedure. Stupid!
  119. catch {
  120. set callback [[namespace current]::feed_callback ${feedlist} ${http_token}]
  121. } return_of_callback
  122. if {$return_of_callback != ""} {
  123. putlog "roc: $return_of_callback"
  124. return $return_of_callback
  125. }
  126. ##
  127. set feed(updated) [unixtime]
  128. set rss($name) [array get feed]
  129. incr i
  130. }
  131. unset feed
  132. }
  133. }
  134. proc ::rss-synd::feed_callback {feedlist args} {
  135. set token [lindex $args end]
  136. array set feed $feedlist
  137. # PaulK: no callback anymore, so we can comment that
  138. upvar 0 $token state
  139. if {[set status $state(status)] != "ok"} {
  140. if {$status == "error"} { set status $state(error) }
  141. putlog "\002RSS HTTP Error\002: $state(url) (State: $status)"
  142. ::http::cleanup $token
  143. return 1
  144. }
  145. array set meta $state(meta)
  146. if {([::http::ncode $token] == 302) || ([::http::ncode $token] == 301)} {
  147. set feed(depth) [expr {$feed(depth) + 1 }]
  148. if {$feed(depth) < $feed(max-depth)} {
  149. catch {::http::geturl "$meta(Location)" -command "[namespace current]::feed_callback {$feedlist}" -timeout $feed(timeout) -headers $feed(headers)}
  150. } else {
  151. putlog "\002RSS HTTP Error\002: $state(url) (State: timeout, max refer limit reached)"
  152. }
  153. ::http::cleanup $token
  154. return 1
  155. } elseif {[::http::ncode $token] != 200} {
  156. putlog "\002RSS HTTP Error\002: $state(url) ($state(http))"
  157. ::http::cleanup $token
  158. return 1
  159. }
  160. set data [::http::data $token]
  161. if {[info exists feed(charset)]} {
  162. set data [encoding convertto [string tolower $feed(charset)] $data]
  163. }
  164. if {([info exists meta(Content-Encoding)]) && \
  165. ([string equal $meta(Content-Encoding) "gzip"])} {
  166. if {[catch {[namespace current]::feed_gzip $data} data] != 0} {
  167. putlog "\002RSS Error\002: Unable to decompress \"$state(url)\": $data"
  168. ::http::cleanup $token
  169. return 1
  170. }
  171. }
  172. if {[catch {[namespace current]::xml_list_create $data} data] != 0} {
  173. putlog "\002RSS Error\002: Unable to parse feed properly, parser returned error. \"$state(url)\""
  174. ::http::cleanup $token
  175. return 1
  176. }
  177. if {[string length $data] == 0} {
  178. putlog "\002RSS Error\002: Unable to parse feed properly, no data returned. \"$state(url)\""
  179. ::http::cleanup $token
  180. return 666
  181. }
  182. set odata ""
  183. if {[catch {set odata [[namespace current]::feed_read]} error] != 0} {
  184. putlog "\002RSS Warning\002: $error."
  185. }
  186. if {![[namespace current]::feed_info $data]} {
  187. putlog "\002RSS Error\002: Invalid feed format ($state(url))!"
  188. ::http::cleanup $token
  189. return 1
  190. }
  191. ::http::cleanup $token
  192. if {[catch {[namespace current]::feed_write $data} error] != 0} {
  193. putlog "\002RSS Database Error\002: $error."
  194. return 1
  195. }
  196. if {$feed(announce-output) > 0} {
  197. [namespace current]::feed_output $data
  198. }
  199. }
  200. #
  201. #
  202. ##
  203. proc ::rss-synd::init {args} {
  204. variable rss
  205. variable default
  206. variable version
  207. variable packages
  208. set version(number) "0.5"
  209. set version(date) "2011-01-05"
  210. package require http
  211. set packages(base64) [catch {package require base64}]; # http auth
  212. set packages(tls) [catch {package require tls}]; # https
  213. set packages(trf) [catch {package require Trf}]; # gzip compression
  214. foreach feed [array names rss] {
  215. array set tmp $default
  216. array set tmp $rss($feed)
  217. set required [list "announce-output" "trigger-output" "max-depth" "update-interval" "timeout" "channels" "output" "user-agent" "url" "database" "trigger-type" "announce-type"]
  218. foreach {key value} [array get tmp] {
  219. if {[set ptr [lsearch -exact $required $key]] >= 0} {
  220. set required [lreplace $required $ptr $ptr]
  221. }
  222. }
  223. if {[llength $required] == 0} {
  224. regsub -nocase -all -- {@@feedid@@} $tmp(trigger) $feed tmp(trigger)
  225. set ulist [regexp -nocase -inline -- {(http(?:s?))://(?:(.[^:]+:.[^@]+)?)(?:@?)(.*)} $tmp(url)]
  226. if {[llength $ulist] == 0} {
  227. putlog "\002RSS Error\002: Unable to parse URL, Invalid format for feed \"$feed\"."
  228. unset rss($feed)
  229. continue
  230. }
  231. set tmp(url) "[lindex $ulist 1]://[lindex $ulist 3]"
  232. if {[lindex $ulist 1] == "https"} {
  233. if {$packages(tls) != 0} {
  234. putlog "\002RSS Error\002: Unable to find tls package required for https, unloaded feed \"$feed\"."
  235. unset rss($feed)
  236. continue
  237. }
  238. ::http::register https 443 ::tls::socket
  239. }
  240. if {(![info exists tmp(url-auth)]) || ($tmp(url-auth) == "")} {
  241. set tmp(url-auth) ""
  242. if {[lindex $ulist 2] != ""} {
  243. if {$packages(base64) != 0} {
  244. putlog "\002RSS Error\002: Unable to find base64 package required for http authentication, unloaded feed \"$feed\"."
  245. unset rss($feed)
  246. continue
  247. }
  248. set tmp(url-auth) [::base64::encode [lindex $ulist 2]]
  249. }
  250. }
  251. if {[regexp {^[0123]{1}:[0123]{1}$} $tmp(trigger-type)] != 1} {
  252. putlog "\002RSS Error\002: Invalid 'trigger-type' syntax for feed \"$feed\"."
  253. unset rss($feed)
  254. continue
  255. }
  256. set tmp(trigger-type) [split $tmp(trigger-type) ":"]
  257. if {([info exists tmp(charset)]) && ([lsearch -exact [encoding names] [string tolower $tmp(charset)]] < 0)} {
  258. putlog "\002RSS Error\002: Unable to load feed \"$feed\", unknown encoding \"$tmp(encoding)\"."
  259. unset rss($feed)
  260. continue
  261. }
  262. set tmp(updated) 0
  263. if {([file exists $tmp(database)]) && ([set mtime [file mtime $tmp(database)]] < [unixtime])} {
  264. set tmp(updated) [file mtime $tmp(database)]
  265. }
  266. set rss($feed) [array get tmp]
  267. } else {
  268. putlog "\002RSS Error\002: Unable to load feed \"$feed\", missing one or more required settings. \"[join $required ", "]\""
  269. unset rss($feed)
  270. }
  271. unset tmp
  272. }
  273. bind evnt -|- prerehash [namespace current]::deinit
  274. #bind time -|- {* * * * *} [namespace current]::feed_get
  275. bind pubm -|- {* *} [namespace current]::trigger
  276. bind msgm -|- {*} [namespace current]::trigger
  277. putlog "\002RSS Syndication Script v$version(number)\002 ($version(date)): Loaded."
  278. }
  279. proc ::rss-synd::deinit {args} {
  280. catch {unbind evnt -|- prerehash [namespace current]::deinit}
  281. catch {unbind time -|- {* * * * *} [namespace current]::feed_get}
  282. catch {unbind pub - !qw [namespace current]::feed_get_qw_and_display}
  283. catch {unbind pubm -|- {* *} [namespace current]::trigger}
  284. catch {unbind msgm -|- {*} [namespace current]::trigger}
  285. foreach child [namespace children] {
  286. catch {[set child]::deinit}
  287. }
  288. namespace delete [namespace current]
  289. }
  290. #
  291. # Trigger Function
  292. ##
  293. proc ::rss-synd::trigger {nick user handle args} {
  294. variable rss
  295. variable default
  296. set i 0
  297. set chan ""
  298. if {[llength $args] == 2} {
  299. set chan [lindex $args 0]
  300. incr i
  301. }
  302. set text [lindex $args $i]
  303. array set tmp $default
  304. if {[info exists tmp(trigger)]} {
  305. regsub -all -- {@@(.*?)@@} $tmp(trigger) "" tmp_trigger
  306. set tmp_trigger [string trimright $tmp_trigger]
  307. if {[string equal -nocase $text $tmp_trigger]} {
  308. set list_feeds [list]
  309. }
  310. }
  311. unset -nocomplain tmp tmp_trigger
  312. foreach name [array names rss] {
  313. array set feed $rss($name)
  314. if {(![info exists list_feeds]) && \
  315. ([string equal -nocase $text $feed(trigger)])} {
  316. if {(![[namespace current]::check_channel $feed(channels) $chan]) && \
  317. ([string length $chan] != 0)} {
  318. continue
  319. }
  320. set feed(nick) $nick
  321. if {$chan != ""} {
  322. set feed(type) [lindex $feed(trigger-type) 0]
  323. set feed(channels) $chan
  324. } else {
  325. set feed(type) [lindex $feed(trigger-type) 1]
  326. set feed(channels) ""
  327. }
  328. if {[catch {set data [[namespace current]::feed_read]} error] == 0} {
  329. if {![[namespace current]::feed_info $data]} {
  330. putlog "\002RSS Error\002: Invalid feed database file format ($feed(database))!"
  331. return
  332. }
  333. if {$feed(trigger-output) > 0} {
  334. set feed(announce-output) $feed(trigger-output)
  335. [namespace current]::feed_output $data
  336. }
  337. } else {
  338. putlog "\002RSS Warning\002: $error."
  339. }
  340. } elseif {[info exists list_feeds]} {
  341. if {$chan != ""} {
  342. # triggered from a channel
  343. if {[[namespace current]::check_channel $feed(channels) $chan]} {
  344. lappend list_feeds $feed(trigger)
  345. }
  346. } else {
  347. # triggered from a privmsg
  348. foreach tmp_chan $feed(channels) {
  349. if {([catch {botonchan $tmp_chan}] == 0) && \
  350. ([onchan $nick $tmp_chan])} {
  351. lappend list_feeds $feed(trigger)
  352. continue
  353. }
  354. }
  355. }
  356. }
  357. }
  358. if {[info exists list_feeds]} {
  359. if {[llength $list_feeds] == 0} {
  360. lappend list_feeds "None"
  361. }
  362. lappend list_msgs "Available feeds: [join $list_feeds ", "]."
  363. if {$chan != ""} {
  364. set list_type [lindex $feed(trigger-type) 0]
  365. set list_targets $chan
  366. } else {
  367. set list_type [lindex $feed(trigger-type) 1]
  368. set list_targets ""
  369. }
  370. [namespace current]::feed_msg $list_type $list_msgs list_targets $nick
  371. }
  372. }
  373. proc ::rss-synd::feed_info {data {target "feed"}} {
  374. upvar 1 $target feed
  375. set length [[namespace current]::xml_get_info $data [list -1 "*"]]
  376. for {set i 0} {$i < $length} {incr i} {
  377. set type [[namespace current]::xml_get_info $data [list $i "*"] "name"]
  378. # tag-name: the name of the element that contains each article and its data
  379. # tag-list: the position in the xml structure where all 'tag-name' reside
  380. switch [string tolower $type] {
  381. rss {
  382. # RSS v0.9x & x2.0
  383. set feed(tag-list) [list 0 "channel"]
  384. set feed(tag-name) "item"
  385. break
  386. }
  387. rdf:rdf {
  388. # RSS v1.0
  389. set feed(tag-list) [list]
  390. set feed(tag-name) "item"
  391. break
  392. }
  393. feed {
  394. # ATOM
  395. set feed(tag-list) [list]
  396. set feed(tag-name) "entry"
  397. break
  398. }
  399. }
  400. }
  401. if {![info exists feed(tag-list)]} {
  402. return 0
  403. }
  404. set feed(tag-feed) [list 0 $type]
  405. return 1
  406. }
  407. # decompress gzip formatted data
  408. proc ::rss-synd::feed_gzip {cdata} {
  409. variable packages
  410. if {(![info exists packages(trf)]) || \
  411. ($packages(trf) != 0)} {
  412. error "Trf package not found."
  413. }
  414. # remove the 10 byte gzip header and 8 byte footer
  415. set cdata [string range $cdata 10 [expr { [string length $cdata] - 9 } ]]
  416. # decompress the raw data
  417. if {[catch {zip -mode decompress -nowrap 1 $cdata} data] != 0} {
  418. error $data
  419. }
  420. return $data
  421. }
  422. proc ::rss-synd::feed_read { } {
  423. upvar 1 feed feed
  424. if {[catch {open $feed(database) "r"} fp] != 0} {
  425. error $fp
  426. }
  427. set data [read -nonewline $fp]
  428. close $fp
  429. return $data
  430. }
  431. proc ::rss-synd::feed_write {data} {
  432. upvar 1 feed feed
  433. if {[catch {open $feed(database) "w+"} fp] != 0} {
  434. error $fp
  435. }
  436. set data [string map { "\n" "" "\r" "" } $data]
  437. puts -nonewline $fp $data
  438. close $fp
  439. }
  440. #
  441. # XML Functions
  442. ##
  443. proc ::rss-synd::xml_list_create {xml_data} {
  444. set xml_list [list]
  445. set ns_current [namespace current]
  446. set ptr 0
  447. while {[set tag_start [${ns_current}::xml_get_position $xml_data $ptr]] != ""} {
  448. set tag_start_first [lindex $tag_start 0]
  449. set tag_start_last [lindex $tag_start 1]
  450. set tag_string [string range $xml_data $tag_start_first $tag_start_last]
  451. # move the pointer to the next character after the current tag
  452. set last_ptr $ptr
  453. set ptr [expr { $tag_start_last + 2 }]
  454. array set tag [list]
  455. # match 'special' tags that dont close
  456. if {[regexp -nocase -- {^!(\[CDATA|--|DOCTYPE)} $tag_string]} {
  457. set tag_data $tag_string
  458. regexp -nocase -- {^!\[CDATA\[(.*?)\]\]$} $tag_string -> tag_data
  459. regexp -nocase -- {^!--(.*?)--$} $tag_string -> tag_data
  460. if {[info exists tag_data]} {
  461. set tag(data) [${ns_current}::xml_escape $tag_data]
  462. }
  463. } else {
  464. # we should only ever encounter opening tags, if we hit a closing one somethings wrong
  465. if {[string match {[/]*} $tag_string]} {
  466. putlog "\002RSS Malformed Feed\002: Tag not open: \"<$tag_string>\" ($tag_start_first => $tag_start_last)"
  467. continue
  468. }
  469. # split up the tag name and attributes
  470. regexp -- {(.[^ \/\n\r]*)(?: |\n|\r\n|\r|)(.*?)$} $tag_string -> tag_name tag_args
  471. set tag(name) [${ns_current}::xml_escape $tag_name]
  472. # split up all of the tags attributes
  473. set tag(attrib) [list]
  474. if {[string length $tag_args] > 0} {
  475. set values [regexp -inline -all -- {(?:\s*|)(.[^=]*)=["'](.[^"']*)["']} $tag_args]
  476. foreach {r_match r_tag r_value} $values {
  477. lappend tag(attrib) [${ns_current}::xml_escape $r_tag] [${ns_current}::xml_escape $r_value]
  478. }
  479. }
  480. # find the end tag of non-self-closing tags
  481. if {(![regexp {(\?|!|/)(\s*)$} $tag_args]) || \
  482. (![string match "\?*" $tag_string])} {
  483. set tmp_num 1
  484. set tag_success 0
  485. set tag_end_last $ptr
  486. # find the correct closing tag if there are nested elements
  487. # with the same name
  488. while {$tmp_num > 0} {
  489. # search for a possible closing tag
  490. set tag_success [regexp -indices -start $tag_end_last -- "</$tag_name>" $xml_data tag_end]
  491. set last_tag_end_last $tag_end_last
  492. set tag_end_first [lindex $tag_end 0]
  493. set tag_end_last [lindex $tag_end 1]
  494. # check to see if there are any NEW opening tags within the
  495. # previous closing tag and the new closing one
  496. incr tmp_num [regexp -all -- "<$tag_name\(\[\\s\\t\\n\\r\]+\(\[^/>\]*\)?\)?>" [string range $xml_data $last_tag_end_last $tag_end_last]]
  497. incr tmp_num -1
  498. }
  499. if {$tag_success == 0} {
  500. putlog "\002RSS Malformed Feed\002: Tag not closed: \"<$tag_name>\""
  501. return
  502. }
  503. # set the pointer to after the last closing tag
  504. set ptr [expr { $tag_end_last + 1 }]
  505. # remember tag_start*'s character index doesnt include the tag start and end characters
  506. set xml_sub_data [string range $xml_data [expr { $tag_start_last + 2 }] [expr { $tag_end_first - 1 }]]
  507. # recurse the data within the currently open tag
  508. set result [${ns_current}::xml_list_create $xml_sub_data]
  509. # set the list data returned from the recursion we just performed
  510. if {[llength $result] > 0} {
  511. set tag(children) $result
  512. # set the current data we have because we're already at the end of a branch
  513. # (ie: the recursion didnt return any data)
  514. } else {
  515. set tag(data) [${ns_current}::xml_escape $xml_sub_data]
  516. }
  517. }
  518. }
  519. # insert any plain data that appears before the current element
  520. if {$last_ptr != [expr { $tag_start_first - 1 }]} {
  521. lappend xml_list [list "data" [${ns_current}::xml_escape [string range $xml_data $last_ptr [expr { $tag_start_first - 2 }]]]]
  522. }
  523. # inset tag data
  524. lappend xml_list [array get tag]
  525. unset tag
  526. }
  527. # if there is still plain data left add it
  528. if {$ptr < [string length $xml_data]} {
  529. lappend xml_list [list "data" [${ns_current}::xml_escape [string range $xml_data $ptr end]]]
  530. }
  531. return $xml_list
  532. }
  533. # simple escape function
  534. proc ::rss-synd::xml_escape {string} {
  535. regsub -all -- {([\{\}])} $string {\\\1} string
  536. return $string
  537. }
  538. # this function is to replace:
  539. # regexp -indices -start $ptr {<(!\[CDATA\[.+?\]\]|!--.+?--|!DOCTYPE.+?|.+?)>} $xml_data -> tag_start
  540. # which doesnt work correctly with tcl's re_syntax
  541. proc ::rss-synd::xml_get_position {xml_data ptr} {
  542. set tag_start [list -1 -1]
  543. regexp -indices -start $ptr {<(.+?)>} $xml_data -> tmp(tag)
  544. regexp -indices -start $ptr {<(!--.*?--)>} $xml_data -> tmp(comment)
  545. regexp -indices -start $ptr {<(!DOCTYPE.+?)>} $xml_data -> tmp(doctype)
  546. regexp -indices -start $ptr {<(!\[CDATA\[.+?\]\])>} $xml_data -> tmp(cdata)
  547. # 'tag' regexp should be compared last
  548. foreach name [lsort [array names tmp]] {
  549. set tmp_s [split $tmp($name)]
  550. if {( ([lindex $tmp_s 0] < [lindex $tag_start 0]) && \
  551. ([lindex $tmp_s 0] > -1) ) || \
  552. ([lindex $tag_start 0] == -1)} {
  553. set tag_start $tmp($name)
  554. }
  555. }
  556. if {([lindex $tag_start 0] == -1) || \
  557. ([lindex $tag_start 1] == -1)} {
  558. set tag_start ""
  559. }
  560. return $tag_start
  561. }
  562. # recursivly flatten all data without tags or attributes
  563. proc ::rss-synd::xml_list_flatten {xml_list {level 0}} {
  564. set xml_string ""
  565. foreach e_list $xml_list {
  566. if {[catch {array set e_array $e_list}] != 0} {
  567. return $xml_list
  568. }
  569. if {[info exists e_array(children)]} {
  570. append xml_string [[namespace current]::xml_list_flatten $e_array(children) [expr { $level + 1 }]]
  571. } elseif {[info exists e_array(data)]} {
  572. append xml_string $e_array(data)
  573. }
  574. unset e_array
  575. }
  576. return $xml_string
  577. }
  578. # returns information on a data structure when given a path.
  579. # paths can be specified using: [struct number] [struct name] <...>
  580. proc ::rss-synd::xml_get_info {xml_list path {element "data"}} {
  581. set i 0
  582. foreach {t_data} $xml_list {
  583. array set t_array $t_data
  584. # if the name doesnt exist set it so we can still reference the data
  585. # using the 'stuct name' *
  586. if {![info exists t_array(name)]} {
  587. set t_array(name) ""
  588. }
  589. if {[string match -nocase [lindex $path 1] $t_array(name)]} {
  590. if {$i == [lindex $path 0]} {
  591. set result ""
  592. if {([llength $path] == 2) && \
  593. ([info exists t_array($element)])} {
  594. set result $t_array($element)
  595. } elseif {[info exists t_array(children)]} {
  596. # shift the first path reference of the front of the path and recurse
  597. set result [[namespace current]::xml_get_info $t_array(children) [lreplace $path 0 1] $element]
  598. }
  599. return $result
  600. }
  601. incr i
  602. }
  603. unset t_array
  604. }
  605. if {[lindex $path 0] == -1} {
  606. return $i
  607. }
  608. }
  609. # converts 'args' into a list in the same order
  610. proc ::rss-synd::xml_join_tags {args} {
  611. set list [list]
  612. foreach tag $args {
  613. foreach item $tag {
  614. if {[string length $item] > 0} {
  615. lappend list $item
  616. }
  617. }
  618. }
  619. return $list
  620. }
  621. #
  622. # Output Feed Functions
  623. ##
  624. proc ::rss-synd::feed_output {data {odata ""}} {
  625. upvar 1 feed feed
  626. set msgs [list]
  627. set path [[namespace current]::xml_join_tags $feed(tag-feed) $feed(tag-list) -1 $feed(tag-name)]
  628. set count [[namespace current]::xml_get_info $data $path]
  629. for {set i 0} {($i < $count) && ($i < $feed(announce-output))} {incr i} {
  630. set tmpp [[namespace current]::xml_join_tags $feed(tag-feed) $feed(tag-list) $i $feed(tag-name)]
  631. set tmpd [[namespace current]::xml_get_info $data $tmpp "children"]
  632. if {[[namespace current]::feed_compare $odata $tmpd]} {
  633. break
  634. }
  635. set tmp_msg [[namespace current]::cookie_parse $data $i]
  636. if {(![info exists feed(output-order)]) || \
  637. ($feed(output-order) == 0)} {
  638. set msgs [linsert $msgs 0 $tmp_msg]
  639. } else {
  640. lappend msgs $tmp_msg
  641. }
  642. }
  643. set nick [expr {[info exists feed(nick)] ? $feed(nick) : ""}]
  644. [namespace current]::feed_msg $feed(type) $msgs $feed(channels) $nick
  645. }
  646. proc ::rss-synd::feed_msg {type msgs targets {nick ""}} {
  647. # check if our target is a nick
  648. if {(($nick != "") && \
  649. ($targets == "")) || \
  650. ([regexp -- {[23]} $type])} {
  651. set targets $nick
  652. }
  653. foreach msg $msgs {
  654. foreach chan $targets {
  655. if {([catch {botonchan $chan}] == 0) || \
  656. ([regexp -- {^[#&]} $chan] == 0)} {
  657. foreach line [split $msg "\n"] {
  658. if {($type == 1) || ($type == 3)} {
  659. putserv "NOTICE $chan :$line"
  660. } else {
  661. putserv "PRIVMSG $chan :$line"
  662. }
  663. }
  664. }
  665. }
  666. }
  667. }
  668. proc ::rss-synd::feed_compare {odata data} {
  669. if {$odata == ""} {
  670. return 0
  671. }
  672. upvar 1 feed feed
  673. array set ofeed [list]
  674. [namespace current]::feed_info $odata "ofeed"
  675. if {[array size ofeed] == 0} {
  676. putlog "\002RSS Error\002: Invalid feed format ($feed(database))!"
  677. return 0
  678. }
  679. if {[string equal -nocase [lindex $feed(tag-feed) 1] "feed"]} {
  680. set cmp_items [list {0 "id"} "children" "" 3 {0 "link"} "attrib" "href" 2 {0 "title"} "children" "" 1]
  681. } else {
  682. set cmp_items [list {0 "guid"} "children" "" 3 {0 "link"} "children" "" 2 {0 "title"} "children" "" 1]
  683. }
  684. set path [[namespace current]::xml_join_tags $ofeed(tag-feed) $ofeed(tag-list) -1 $ofeed(tag-name)]
  685. set count [[namespace current]::xml_get_info $odata $path]
  686. for {set i 0} {$i < $count} {incr i} {
  687. # extract the current article from the database
  688. set tmpp [[namespace current]::xml_join_tags $ofeed(tag-feed) $ofeed(tag-list) $i $ofeed(tag-name)]
  689. set tmpd [[namespace current]::xml_get_info $odata $tmpp "children"]
  690. set w 0; # weight value
  691. set m 0; # item tag matches
  692. foreach {cmp_path cmp_element cmp_attrib cmp_weight} $cmp_items {
  693. # try and extract the tag info from the current article
  694. set oresult [[namespace current]::xml_get_info $tmpd $cmp_path $cmp_element]
  695. if {$cmp_element == "attrib"} {
  696. array set tmp $oresult
  697. catch {set oresult $tmp($cmp_attrib)}
  698. unset tmp
  699. }
  700. # if the tag doesnt exist in the article ignore it
  701. if {$oresult == ""} { continue }
  702. incr m
  703. # extract the tag info from the current article
  704. set result [[namespace current]::xml_get_info $data $cmp_path $cmp_element]
  705. if {$cmp_element == "attrib"} {
  706. array set tmp $result
  707. catch {set result $tmp($cmp_attrib)}
  708. unset tmp
  709. }
  710. if {[string equal -nocase $oresult $result]} {
  711. set w [expr { $w + $cmp_weight }]
  712. }
  713. }
  714. # value of 100 or more means its a match
  715. if {($m > 0) && \
  716. ([expr { round(double($w) / double($m) * 100) }] >= 100)} {
  717. return 1
  718. }
  719. }
  720. return 0
  721. }
  722. #
  723. # Cookie Parsing Functions
  724. ##
  725. proc ::rss-synd::cookie_parse {data current} {
  726. upvar 1 feed feed
  727. set output $feed(output)
  728. set eval 0
  729. if {([info exists feed(evaluate-tcl)]) && ($feed(evaluate-tcl) == 1)} { set eval 1 }
  730. set matches [regexp -inline -nocase -all -- {@@(.*?)@@} $output]
  731. foreach {match tmpc} $matches {
  732. set tmpc [split $tmpc "!"]
  733. set index 0
  734. set cookie [list]
  735. foreach piece $tmpc {
  736. set tmpp [regexp -nocase -inline -all -- {^(.*?)\((.*?)\)|(.*?)$} $piece]
  737. if {[lindex $tmpp 3] == ""} {
  738. lappend cookie [lindex $tmpp 2] [lindex $tmpp 1]
  739. } else {
  740. lappend cookie 0 [lindex $tmpp 3]
  741. }
  742. }
  743. # replace tag-item's index with the current article
  744. if {[string equal -nocase $feed(tag-name) [lindex $cookie 1]]} {
  745. set cookie [[namespace current]::xml_join_tags $feed(tag-list) [lreplace $cookie $index $index $current]]
  746. }
  747. set cookie [[namespace current]::xml_join_tags $feed(tag-feed) $cookie]
  748. if {[set tmp [[namespace current]::cookie_replace $cookie $data]] != ""} {
  749. set tmp [[namespace current]::xml_list_flatten $tmp]
  750. regsub -all -- {([\"\$\[\]\{\}\(\)\\])} $match {\\\1} match
  751. regsub -- $match $output "[string map { "&" "\\\x26" } [[namespace current]::html_decode $eval $tmp]]" output
  752. }
  753. }
  754. # remove empty cookies
  755. if {(![info exists feed(remove-empty)]) || ($feed(remove-empty) == 1)} {
  756. regsub -nocase -all -- "@@.*?@@" $output "" output
  757. }
  758. # evaluate tcl code
  759. if {$eval == 1} {
  760. if {[catch {set output [subst $output]} error] != 0} {
  761. putlog "\002RSS Eval Error\002: $error"
  762. }
  763. }
  764. return $output
  765. }
  766. proc ::rss-synd::cookie_replace {cookie data} {
  767. set element "children"
  768. set tags [list]
  769. foreach {num section} $cookie {
  770. if {[string equal "=" [string range $section 0 0]]} {
  771. set attrib [string range $section 1 end]
  772. set element "attrib"
  773. break
  774. } else {
  775. lappend tags $num $section
  776. }
  777. }
  778. set return [[namespace current]::xml_get_info $data $tags $element]
  779. if {[string equal -nocase "attrib" $element]} {
  780. array set tmp $return
  781. if {[catch {set return $tmp($attrib)}] != 0} {
  782. return
  783. }
  784. }
  785. return $return
  786. }
  787. #
  788. # Misc Functions
  789. ##
  790. proc ::rss-synd::html_decode {eval data {loop 0}} {
  791. array set chars {
  792. nbsp \x20 amp \x26 quot \x22 lt \x3C
  793. gt \x3E iexcl \xA1 cent \xA2 pound \xA3
  794. curren \xA4 yen \xA5 brvbar \xA6 brkbar \xA6
  795. sect \xA7 uml \xA8 die \xA8 copy \xA9
  796. ordf \xAA laquo \xAB not \xAC shy \xAD
  797. reg \xAE hibar \xAF macr \xAF deg \xB0
  798. plusmn \xB1 sup2 \xB2 sup3 \xB3 acute \xB4
  799. micro \xB5 para \xB6 middot \xB7 cedil \xB8
  800. sup1 \xB9 ordm \xBA raquo \xBB frac14 \xBC
  801. frac12 \xBD frac34 \xBE iquest \xBF Agrave \xC0
  802. Aacute \xC1 Acirc \xC2 Atilde \xC3 Auml \xC4
  803. Aring \xC5 AElig \xC6 Ccedil \xC7 Egrave \xC8
  804. Eacute \xC9 Ecirc \xCA Euml \xCB Igrave \xCC
  805. Iacute \xCD Icirc \xCE Iuml \xCF ETH \xD0
  806. Dstrok \xD0 Ntilde \xD1 Ograve \xD2 Oacute \xD3
  807. Ocirc \xD4 Otilde \xD5 Ouml \xD6 times \xD7
  808. Oslash \xD8 Ugrave \xD9 Uacute \xDA Ucirc \xDB
  809. Uuml \xDC Yacute \xDD THORN \xDE szlig \xDF
  810. agrave \xE0 aacute \xE1 acirc \xE2 atilde \xE3
  811. auml \xE4 aring \xE5 aelig \xE6 ccedil \xE7
  812. egrave \xE8 eacute \xE9 ecirc \xEA euml \xEB
  813. igrave \xEC iacute \xED icirc \xEE iuml \xEF
  814. eth \xF0 ntilde \xF1 ograve \xF2 oacute \xF3
  815. ocirc \xF4 otilde \xF5 ouml \xF6 divide \xF7
  816. oslash \xF8 ugrave \xF9 uacute \xFA ucirc \xFB
  817. uuml \xFC yacute \xFD thorn \xFE yuml \xFF
  818. ensp \x20 emsp \x20 thinsp \x20 zwnj \x20
  819. zwj \x20 lrm \x20 rlm \x20 euro \x80
  820. sbquo \x82 bdquo \x84 hellip \x85 dagger \x86
  821. Dagger \x87 circ \x88 permil \x89 Scaron \x8A
  822. lsaquo \x8B OElig \x8C oelig \x8D lsquo \x91
  823. rsquo \x92 ldquo \x93 rdquo \x94 ndash \x96
  824. mdash \x97 tilde \x98 scaron \x9A rsaquo \x9B
  825. Yuml \x9F apos \x27
  826. }
  827. regsub -all -- {<(.[^>]*)>} $data " " data
  828. if {$eval != 1} {
  829. regsub -all -- {([\$\[\]\{\}\(\)\\])} $data {\\\1} data
  830. } else {
  831. regsub -all -- {([\$\[\]\{\}\(\)\\])} $data {\\\\\\\1} data
  832. }
  833. regsub -all -- {&#(\d+);} $data {[subst -nocomm -novar [format \\\u%04x [scan \1 %d]]]} data
  834. regsub -all -- {&#x(\w+);} $data {[format %c [scan \1 %x]]} data
  835. regsub -all -- {&([0-9a-zA-Z#]*);} $data {[if {[catch {set tmp $chars(\1)} char] == 0} { set tmp }]} data
  836. regsub -all -- {&([0-9a-zA-Z#]*);} $data {[if {[catch {set tmp [string tolower $chars(\1)]} char] == 0} { set tmp }]} data
  837. regsub -nocase -all -- "\\s{2,}" $data " " data
  838. set data [subst $data]
  839. if {[incr loop] == 1} {
  840. set data [[namespace current]::html_decode 0 $data $loop]
  841. }
  842. return $data
  843. }
  844. proc ::rss-synd::check_channel {chanlist chan} {
  845. foreach match [split $chanlist] {
  846. if {[string equal -nocase $match $chan]} {
  847. return 1
  848. }
  849. }
  850. return 0
  851. }
  852. proc ::rss-synd::urldecode {str} {
  853. regsub -all -- {([\"\$\[\]\{\}\(\)\\])} $str {\\\1} str
  854. regsub -all -- {%([aAbBcCdDeEfF0-9][aAbBcCdDeEfF0-9]);?} $str {[format %c [scan \1 %x]]} str
  855. return [subst $str]
  856. }
  857. ::rss-synd::init