1#!/usr/bin/env tclsh 2 3# jsend.tcl -- 4# 5# This file is an example provided with the XMPP library. It allows to 6# send messages via XMPP non-interactively. It was initially developed 7# by Marshall T. Rose and adapted to the XMPP library by Sergei Golovan. 8# 9# Copyright (c) 2008-2013 Sergei Golovan <sgolovan@nes.ru> 10# 11# See the file "license.terms" for information on usage and redistribution 12# of this file, and for a DISCLAMER OF ALL WARRANTIES. 13# 14# $Id$ 15 16package require sha1 17package require tls 18 19package require xmpp 20package require xmpp::transport::bosh 21package require xmpp::auth 22package require xmpp::sasl 23package require xmpp::starttls 24package require xmpp::roster 25package require xmpp::delay 26 27# Register IQ XMLNS 28::xmpp::iq::register get * http://jabber.org/protocol/disco#info \ 29 jsend::iqDiscoInfo 30::xmpp::iq::register get * http://jabber.org/protocol/disco#items \ 31 jsend::iqDiscoItems 32::xmpp::iq::register get * jabber:iq:last jsend::iqLast 33::xmpp::iq::register get * jabber:iq:time jsend::iqTime 34::xmpp::iq::register get * jabber:iq:version jsend::iqVersion 35 36namespace eval jsend {} 37 38proc jsend::sendit {stayP to args} { 39 global xlib 40 global env 41 42 variable lib 43 variable sendit_result 44 45 array set options [list -to $to \ 46 -from "" \ 47 -password "" \ 48 -host "" \ 49 -port "" \ 50 -activity "" \ 51 -type chat \ 52 -subject "" \ 53 -body "" \ 54 -xhtml "" \ 55 -date "" \ 56 -description "" \ 57 -url "" \ 58 -bosh "" \ 59 -tls false \ 60 -starttls true \ 61 -sasl true \ 62 -digest true] 63 array set options $args 64 65 if {[string equal $options(-host) ""]} { 66 if {[string first @ $options(-from)] < 0} { 67 set options(-host) [info hostname] 68 } else { 69 set options(-host) [::xmpp::jid::server $options(-from)] 70 } 71 } 72 73 set params [list from] 74 if {![string equal $options(-to) "-"]} { 75 lappend params to 76 } 77 foreach k $params { 78 if {[string first @ $options(-$k)] < 0} { 79 if {[set x [string first / $options(-$k)]] >= 0} { 80 set options(-$k) [string replace $options(-$k) $x $x \ 81 @$options(-host)/] 82 } else { 83 append options(-$k) @$options(-host) 84 } 85 } 86 if {([string first @ $options(-$k)] == 0) \ 87 && ([info exists env(USER)])} { 88 set options(-$k) $env(USER)$options(-$k) 89 } 90 } 91 if {![string equal $options(-to) "-"]} { 92 set options(-to) [list $options(-to)] 93 } 94 95 foreach k [list tls starttls] { 96 switch -- [string tolower $options(-$k)] { 97 1 - 0 {} 98 false - no - off { set options(-$k) 0 } 99 true - yes - on { set options(-$k) 1 } 100 default { 101 error "invalid value for -$k: $options(-$k)" 102 } 103 } 104 } 105 106 ::xmpp::jid::split $options(-from) node domain resource 107 if {[string equal $resource ""]} { 108 set resource "jsend" 109 } 110 111 if {[string equal $options(-body) ""] && $stayP < 2} { 112 set options(-body) [read -nonewline stdin] 113 } 114 115 set options(-xlist) {} 116 if {![string equal $options(-url)$options(-description) ""]} { 117 lappend options(-xlist) \ 118 [::xmpp::xml::create x \ 119 -xmlns jabber:x:oob \ 120 -subelement [::xmpp::xml::create url \ 121 -cdata $options(-url)] \ 122 -subelement [::xmpp::xml::create desc \ 123 -cdata $options(-description)]] 124 } 125 if {[string compare $options(-date) ""]} { 126 lappend options(-xlist) \ 127 [::xmpp::delay::create $options(-date)] 128 } 129 if {![string equal $options(-xhtml) ""] \ 130 && ![string equal $options(-body) ""] \ 131 && $stayP < 1} { 132 lappend options(-xlist) \ 133 [::xmpp::xml::create html \ 134 -xmlns http://jabber.org/protocol/xhtml-im \ 135 -subelement [::xmpp::xml::create body \ 136 -xmlns http://www.w3.org/1999/xhtml \ 137 -subelements [jsend::parse_xhtml \ 138 $options(-xhtml)]]] 139 } 140 if {[string equal $options(-type) announce]} { 141 set options(-type) normal 142 set announce [sha1::sha1 \ 143 [clock seconds]$options(-subject)$options(-body)] 144 lappend options(-xlist) \ 145 [::xmpp::xml::create x \ 146 -xmlns http://2entwine.com/protocol/gush-announce-1_0 \ 147 -subelement [::xmpp::xml::create id -cdata $announce]] 148 } 149 150 set lib(lastwhat) $options(-activity) 151 if {[catch { clock scan $options(-time) } lib(lastwhen)]} { 152 set lib(lastwhen) [clock seconds] 153 } 154 155 set params {} 156 foreach k [list body subject type xlist] { 157 if {![string equal $options(-$k) ""]} { 158 lappend params -$k $options(-$k) 159 } 160 } 161 162 if {![info exists xlib]} { 163 # Create an XMPP library instance 164 set xlib [::xmpp::new] 165 166 if (![string equal $options(-bosh) ""]) { 167 set transport bosh 168 set port 0 169 } elseif {$options(-tls)} { 170 set transport tls 171 if {![string equal $options(-port) ""]} { 172 set port $options(-port) 173 } else { 174 set port 5223 175 } 176 } else { 177 set transport tcp 178 if {![string equal $options(-port) ""]} { 179 set port $options(-port) 180 } else { 181 set port 5222 182 } 183 } 184 185 # Connect to a server 186 ::xmpp::connect $xlib $options(-host) $port \ 187 -transport $transport \ 188 -url $options(-bosh) 189 190 if {[string equal $options(-bosh) ""] && !$options(-tls) && $options(-starttls)} { 191 # Open XMPP stream 192 set sessionID [::xmpp::openStream $xlib $domain \ 193 -version 1.0] 194 195 ::xmpp::starttls::starttls $xlib 196 197 ::xmpp::sasl::auth $xlib -username $node \ 198 -password $options(-password) \ 199 -resource $resource \ 200 -digest $options(-digest) 201 } elseif {$options(-sasl)} { 202 # Open XMPP stream 203 set sessionID [::xmpp::openStream $xlib $domain \ 204 -version 1.0] 205 206 ::xmpp::sasl::auth $xlib -username $node \ 207 -password $options(-password) \ 208 -resource $resource \ 209 -digest $options(-digest) 210 } else { 211 # Open XMPP stream 212 set sessionID [::xmpp::openStream $xlib $domain] 213 214 # Authenticate 215 ::xmpp::auth::auth $xlib -sessionid $sessionID \ 216 -username $node \ 217 -password $options(-password) \ 218 -resource $resource 219 } 220 221 set roster [::xmpp::roster::new $xlib] 222 ::xmpp::roster::get $roster 223 } 224 225 if {[string equal $options(-to) "-"]} { 226 set options(-to) [::xmpp::roster::items $roster] 227 } 228 229 if {$stayP > 1} { 230 ::xmpp::sendPresence $xlib -status Online 231 232 if {[string equal $options(-type) groupchat]} { 233 set nick [::xmpp::jid::jid $username $domain $resource] 234 set nick [string range [sha1::sha1 $nick+[clock seconds]] 0 7] 235 foreach to $options(-to) { 236 ::xmpp::sendPresence $xlib -to $to/$nick 237 } 238 } 239 return 1 240 } 241 242 foreach to $options(-to) { 243 switch -- [eval [list ::xmpp::sendMessage $xlib $to] $params] { 244 -1 - 245 -2 { 246 if {$stayP} { 247 set cmd [list ::LOG] 248 } else { 249 set cmd [list error] 250 } 251 eval $cmd [list "error writing to socket, continuing..."] 252 return 0 253 } 254 255 default {} 256 } 257 } 258 if {!$stayP} { 259 set jsend::stayP 0 260 ::xmpp::disconnect $xlib -wait 1 261 } 262 263 return 1 264} 265 266proc jsend::iqDiscoInfo {xlib from xmlElement args} { 267 ::LOG "jsend::iqDiscoInfo $from" 268 269 ::xmpp::xml::split $xmlElement tag xmlns attrs cdata subels 270 271 if {[::xmpp::xml::isAttr $attrs node]} { 272 return [list error cancel service-unavailable] 273 } 274 275 set identity [::xmpp::xml::create identity \ 276 -attrs [list name jsend \ 277 category client \ 278 type bot]] 279 280 set subelements {} 281 foreach var [list http://jabber.org/protocol/disco#info \ 282 http://jabber.org/protocol/disco#items \ 283 jabber:iq:last \ 284 jabber:iq:time \ 285 jabber:iq:version] { 286 lappend subelements [::xmpp::xml::create feature \ 287 -attrs [list var $var]] 288 } 289 set xmldata \ 290 [::xmpp::xml::create query -xmlns $xmlns \ 291 -attrs [list type client] \ 292 -subelement $identity \ 293 -subelements $subelements] 294 return [list result $xmldata] 295} 296 297proc jsend::iqDiscoItems {xlib from xmlElement args} { 298 ::LOG "jsend::iqDiscoItems $from" 299 300 ::xmpp::xml::split $xmlElement tag xmlns attrs cdata subels 301 302 if {[::xmpp::xml::isAttr $attrs node]} { 303 return [list error cancel service-unavailable] 304 } 305 306 return [list result [::xmpp::xml::create query -xmlns $xmlns]] 307} 308 309proc jsend::iqLast {xlib from xmlElement args} { 310 variable lib 311 312 ::LOG "jsend::iqLast $from" 313 314 set now [clock seconds] 315 set xmldata \ 316 [::xmpp::xml::create query -xmlns jabber:iq:last \ 317 -attrs [list seconds \ 318 [expr {$now - $lib(lastwhen)}]]] 319 return [list result $xmldata] 320} 321 322proc jsend::iqTime {xlib from xmlElement args} { 323 ::LOG "jsend::iqTime $from" 324 325 set now [clock seconds] 326 set gmtP true 327 foreach {k f} [list utc "%Y%m%dT%T" \ 328 tz "%Z" \ 329 display "%a %b %d %H:%M:%S %Z %Y"] { 330 lappend tags [::xmpp::xml::create $k -cdata [clock format $now \ 331 -format $f \ 332 -gmt $gmtP]] 333 set gmtP false 334 } 335 set xmldata [::xmpp::xml::create query -xmlns jabber:iq:time \ 336 -subelements $tags] 337 return [list result $xmldata] 338} 339 340proc jsend::iqVersion {xlib from xmlElement args} { 341 global argv0 tcl_platform 342 343 ::LOG "jsend::iqVersion $from" 344 345 foreach {k v} [list name [file tail [file rootname $argv0]] \ 346 version "1.0 (Tcl [info patchlevel])" \ 347 os "$tcl_platform(os) $tcl_platform(osVersion)"] { 348 lappend tags [::xmpp::xml::create $k -cdata $v] 349 } 350 set xmldata [::xmpp::xml::create query -xmlns jabber:iq:version \ 351 -subelements $tags] 352 return [list result $xmldata] 353} 354 355proc client:reconnect {xlib} { 356 jsend::reconnect 357} 358 359proc client:disconnect {xlib} { 360 jsend::reconnect 361} 362 363proc client:status {args} { 364 ::LOG "client:status $args" 365} 366 367 368namespace eval jsend { 369 variable stayP 1 370} 371 372proc jsend::follow {file argv} { 373 proc [namespace current]::reconnect {} \ 374 [list [namespace current]::reconnect_aux $argv] 375 376 if {[catch { eval [list jsend::sendit 2] $argv } result]} { 377 ::bgerror $result 378 return 379 } 380 381 set buffer "" 382 set fd "" 383 set newP 1 384 array set st [list dev 0 ino 0 size 0] 385 386 for {set i 0} {1} {incr i} { 387 if {[expr {$i % 5}] == 0} { 388 if {[catch { file stat $file st2 } result]} { 389 ::LOG $result 390 break 391 } 392 393 if {($st(dev) != $st2(dev)) \ 394 || ($st(ino) != $st2(ino)) \ 395 || ($st(size) > $st2(size))} { 396 if {$newP} { 397 catch { close $fd } 398 } 399 400 fconfigure [set fd [open $file { RDONLY }]] -blocking off 401 unset st 402 array set st [array get st2] 403 404 if {!$newP && [string equal $st(type) file]} { 405 seek $fd 0 end 406 } 407 408 if {!$newP} { 409 set newP 0 410 } 411 412 if {[string length $buffer] > 0} { 413 if {[catch { eval [list jsend::sendit 1] $argv \ 414 [parse $buffer] \ 415 [list -body $buffer] } result]} { 416 ::LOG $result 417 break 418 } elseif {$result} { 419 set buffer "" 420 } 421 } 422 } 423 } 424 425 if {[fblocked $fd]} { 426 } elseif {[catch { 427 set len [string length [set line [read $fd]]] 428 append buffer $line 429 } result]} { 430 ::LOG $result 431 break 432 } elseif {[set x [string first "\n" $buffer]] < 0} { 433 } else { 434 set body [string range $buffer 0 [expr {$x-1}]] 435 while {[catch { eval [list jsend::sendit 1] $argv [parse $body] \ 436 [list -body $body] } result]} { 437 ::LOG $result 438 } 439 if {$result} { 440 set buffer [string range $buffer [expr {$x + 1}] end] 441 } 442 } 443 444 after 1000 "set alarmP 1" 445 vwait alarmP 446 } 447} 448 449proc jsend::parse {line} { 450 set args {} 451 452 if {![string equal [string index $line 15] " "]} { 453 return $args 454 } 455 catch { lappend args -time [clock scan [string range $line 0 14]] } 456 457 set line [string range $line 16 end] 458 if {([set d [string first " " $line]] > 0) \ 459 && ([string first ": " $line] > $d)} { 460 lappend args -activity [string trim [string range $line $d end]] 461 } 462 463 return $args 464} 465 466proc jsend::reconnect_aux {argv} { 467 variable stayP 468 469 while {$stayP} { 470 after [expr {60*1000}] 471 if {![catch { eval [list jsend::sendit 2] $argv } result]} { 472 break 473 } 474 475 ::LOG $result 476 } 477} 478 479proc jsend::parse_xhtml {text} { 480 return [::xmpp::xml::parseData "<body>$text</body>"] 481} 482 483proc ::LOG {text} { 484# puts stderr $text 485} 486 487proc ::debugmsg {args} { 488# ::LOG "debugmsg: $args" 489} 490 491proc ::bgerror {err} { 492 global errorInfo 493 494 ::LOG "$err\n$errorInfo" 495} 496 497 498set status 1 499 500array set jsend::lib [list lastwhen [clock seconds] lastwhat ""] 501 502if {[string equal [file tail [lindex $argv 0]] "jsend.tcl"]} { 503 incr argc -1 504 set argv [lrange $argv 1 end] 505} 506 507if {(([set x [lsearch -exact $argv -help]] >= 0) \ 508 || ([set x [lsearch -exact $argv --help]] >= 0)) \ 509 && (($x == 0) || ([expr {$x % 2}]))} { 510 puts stdout \ 511"usage: jsend.tcl recipient ?options...? 512 -follow file 513 -pidfile file 514 -from jid 515 -host hostname 516 -port number 517 -password string 518 -type string (e.g., 'chat') 519 -subject string 520 -body string 521 -xhtml string 522 -description string 523 -url string 524 -bosh string (BOSH URL) 525 -tls boolean (e.g., 'false') 526 -starttls boolean (e.g., 'true') 527 -sasl boolean (e.g., 'true') 528 529If recipient is '-', roster is used. 530 531If both '-body' and '-follow' are absent, the standard input is used. 532 533The file .jsendrc.tcl in the current or in home directory is consulted, 534e.g., 535 536 set args {-from fred@example.com/bedrock -password wilma} 537 538for default values." 539 540 set status 0 541} elseif {($argc < 1) || (![expr {$argc % 2}])} { 542 puts stderr "usage: jsend.tcl recipent ?-key value?..." 543} elseif {[catch { 544 if {([file exists [set file .jsendrc.tcl]]) \ 545 || ([file exists [set file ~/.jsendrc.tcl]])} { 546 set args {} 547 548 source $file 549 550 array set at [list -permissions 600] 551 array set at [file attributes $file] 552 553 if {[set x [lsearch -exact $args "-password"]] >= 0 \ 554 && ![expr {$x % 2}] \ 555 && ![string match *00 $at(-permissions)]} { 556 error "file should be mode 0600" 557 } 558 559 if {[llength $args] > 0} { 560 set argv [eval [list linsert $argv 1] $args] 561 } 562 } 563} result]} { 564 puts stderr "error in $file: $result" 565} elseif {[set x [lsearch -exact $argv "-follow"]] > 0 && [expr {$x % 2}]} { 566 set keep_alive 1 567 set keep_alive_interval 3 568 569 if {[set y [lsearch -exact $argv "-pidfile"]] > 0 && [expr {$y % 2}]} { 570 set fd [open [set pf [lindex $argv [expr {$y + 1}]]] \ 571 {WRONLY CREAT TRUNC}] 572 puts $fd [pid] 573 close $fd 574 } 575 576 jsend::follow [lindex $argv [expr {$x + 1}]] $argv 577 578 catch { file delete -- $pf } 579} elseif {[catch { eval [list jsend::sendit 0] $argv } result]} { 580 puts stderr $result 581} else { 582 set status 0 583} 584 585exit $status 586 587# vim:ft=tcl:ts=8:sw=4:sts=4:et 588