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