qs_net.tcl 41 KB

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