#!/usr/bin/env tclsh

# jsend.tcl --
#
#       This file is an example provided with the XMPP library. It allows to
#       send messages via XMPP non-interactively. It was initially developed
#       by Marshall T. Rose and adapted to the XMPP library by Sergei Golovan.
#
# Copyright (c) 2008-2013 Sergei Golovan <sgolovan@nes.ru>
#
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAMER OF ALL WARRANTIES.

package require sha1
package require tls

package require xmpp
package require xmpp::transport::bosh
package require xmpp::auth
package require xmpp::sasl
package require xmpp::starttls
package require xmpp::roster
package require xmpp::delay

# Register IQ XMLNS
::xmpp::iq::register get * http://jabber.org/protocol/disco#info \
                           jsend::iqDiscoInfo
::xmpp::iq::register get * http://jabber.org/protocol/disco#items \
                           jsend::iqDiscoItems
::xmpp::iq::register get * jabber:iq:last    jsend::iqLast
::xmpp::iq::register get * jabber:iq:time    jsend::iqTime
::xmpp::iq::register get * jabber:iq:version jsend::iqVersion

namespace eval jsend {}

proc jsend::sendit {stayP to args} {
    global xlib
    global env

    variable lib
    variable sendit_result

    array set options [list -to          $to   \
                            -from        ""    \
                            -password    ""    \
                            -host        ""    \
                            -port        ""    \
                            -activity    ""    \
                            -type        chat  \
                            -subject     ""    \
                            -body        ""    \
                            -xhtml       ""    \
                            -date        ""    \
                            -description ""    \
                            -url         ""    \
                            -bosh        ""    \
                            -tls         false \
                            -starttls    true  \
                            -sasl        true  \
                            -digest      true]
    array set options $args

    if {[string equal $options(-host) ""]} {
        if {[string first @ $options(-from)] < 0} {
            set options(-host) [info hostname]
        } else {
            set options(-host) [::xmpp::jid::server $options(-from)]
        }
    }

    set params [list from]
    if {![string equal $options(-to) "-"]} {
        lappend params to
    }
    foreach k $params {
        if {[string first @ $options(-$k)] < 0} {
            if {[set x [string first / $options(-$k)]] >= 0} {
                set options(-$k) [string replace $options(-$k) $x $x \
                                         @$options(-host)/]
            } else {
                append options(-$k) @$options(-host)
            }
        }
        if {([string first @ $options(-$k)] == 0) \
                && ([info exists env(USER)])} {
            set options(-$k) $env(USER)$options(-$k)
        }
    }
    if {![string equal $options(-to) "-"]} {
        set options(-to) [list $options(-to)]
    }

    foreach k [list tls starttls] {
        switch -- [string tolower $options(-$k)] {
            1 - 0               {}
            false - no  - off   { set options(-$k) 0 }
            true  - yes - on    { set options(-$k) 1 }
            default {
                error "invalid value for -$k: $options(-$k)"
            }
        }
    }

    ::xmpp::jid::split $options(-from) node domain resource
    if {[string equal $resource ""]} {
        set resource "jsend"
    }

    if {[string equal $options(-body) ""] && $stayP < 2} {
        set options(-body) [read -nonewline stdin]
    }

    set options(-xlist) {}
    if {![string equal $options(-url)$options(-description) ""]} {
        lappend options(-xlist) \
                [::xmpp::xml::create x \
                       -xmlns jabber:x:oob \
                       -subelement [::xmpp::xml::create url \
                                        -cdata $options(-url)] \
                       -subelement [::xmpp::xml::create desc \
                                        -cdata $options(-description)]]
    }
    if {[string compare $options(-date) ""]} {
        lappend options(-xlist) \
                [::xmpp::delay::create $options(-date)]
    }
    if {![string equal $options(-xhtml) ""] \
            && ![string equal $options(-body) ""] \
            && $stayP < 1} {
        lappend options(-xlist) \
                [::xmpp::xml::create html \
                       -xmlns http://jabber.org/protocol/xhtml-im \
                       -subelement [::xmpp::xml::create body \
                                        -xmlns http://www.w3.org/1999/xhtml \
                                        -subelements [jsend::parse_xhtml \
                                                            $options(-xhtml)]]]
    }
    if {[string equal $options(-type) announce]} {
        set options(-type) normal
        set announce [sha1::sha1 \
                          [clock seconds]$options(-subject)$options(-body)]
        lappend options(-xlist) \
                [::xmpp::xml::create x \
                     -xmlns http://2entwine.com/protocol/gush-announce-1_0 \
                     -subelement [::xmpp::xml::create id -cdata $announce]]
    }

    set lib(lastwhat) $options(-activity)
    if {[catch { clock scan $options(-time) } lib(lastwhen)]} {
        set lib(lastwhen) [clock seconds]
    }

    set params {}
    foreach k [list body subject type xlist] {
        if {![string equal $options(-$k) ""]} {
            lappend params -$k $options(-$k)
        }
    }

    if {![info exists xlib]} {
        # Create an XMPP library instance
        set xlib [::xmpp::new]

        if (![string equal $options(-bosh) ""]) {
            set transport bosh
            set port 0
        } elseif {$options(-tls)} {
            set transport tls
            if {![string equal $options(-port) ""]} {
                set port $options(-port)
            } else {
                set port 5223
            }
        } else {
            set transport tcp
            if {![string equal $options(-port) ""]} {
                set port $options(-port)
            } else {
                set port 5222
            }
        }

        # Connect to a server
        ::xmpp::connect $xlib $options(-host) $port \
                              -transport $transport \
                              -url $options(-bosh)

        if {[string equal $options(-bosh) ""] && !$options(-tls) && $options(-starttls)} {
            # Open XMPP stream
            set sessionID [::xmpp::openStream $xlib $domain \
                                                    -version 1.0]

            ::xmpp::starttls::starttls $xlib

            ::xmpp::sasl::auth $xlib -username  $node \
                                     -password  $options(-password) \
                                     -resource  $resource \
                                     -digest    $options(-digest)
        } elseif {$options(-sasl)} {
            # Open XMPP stream
            set sessionID [::xmpp::openStream $xlib $domain \
                                                    -version 1.0]

            ::xmpp::sasl::auth $xlib -username  $node \
                                     -password  $options(-password) \
                                     -resource  $resource \
                                     -digest    $options(-digest)
        } else {
            # Open XMPP stream
            set sessionID [::xmpp::openStream $xlib $domain]

            # Authenticate
            ::xmpp::auth::auth $xlib -sessionid $sessionID \
                                     -username  $node \
                                     -password  $options(-password) \
                                     -resource  $resource
        }

        set roster [::xmpp::roster::new $xlib]
        ::xmpp::roster::get $roster
    }

    if {[string equal $options(-to) "-"]} {
        set options(-to) [::xmpp::roster::items $roster]
    }

    if {$stayP > 1} {
        ::xmpp::sendPresence $xlib -status Online

        if {[string equal $options(-type) groupchat]} {
            set nick [::xmpp::jid::jid $username $domain $resource]
            set nick [string range [sha1::sha1 $nick+[clock seconds]] 0 7]
            foreach to $options(-to) {
                ::xmpp::sendPresence $xlib -to $to/$nick
            }
        }
        return 1
    }

    foreach to $options(-to) {
        switch -- [eval [list ::xmpp::sendMessage $xlib $to] $params] {
            -1 -
            -2 {
                if {$stayP} {
                    set cmd [list ::LOG]
                } else {
                    set cmd [list error]
                }
                eval $cmd [list "error writing to socket, continuing..."]
                return 0
            }

            default {}
        }
    }
    if {!$stayP} {
        set jsend::stayP 0
        ::xmpp::disconnect $xlib -wait 1
    }

    return 1
}

proc jsend::iqDiscoInfo {xlib from xmlElement args} {
    ::LOG "jsend::iqDiscoInfo $from"

    ::xmpp::xml::split $xmlElement tag xmlns attrs cdata subels

    if {[::xmpp::xml::isAttr $attrs node]} {
        return [list error cancel service-unavailable]
    }

    set identity [::xmpp::xml::create identity \
                                      -attrs [list name     jsend \
                                                   category client \
                                                   type     bot]]

    set subelements {}
    foreach var [list http://jabber.org/protocol/disco#info \
                      http://jabber.org/protocol/disco#items \
                      jabber:iq:last \
                      jabber:iq:time \
                      jabber:iq:version] {
        lappend subelements [::xmpp::xml::create feature \
                                    -attrs [list var $var]]
    }
    set xmldata \
        [::xmpp::xml::create query -xmlns       $xmlns \
                                   -attrs       [list type client] \
                                   -subelement  $identity \
                                   -subelements $subelements]
    return [list result $xmldata]
}

proc jsend::iqDiscoItems {xlib from xmlElement args} {
    ::LOG "jsend::iqDiscoItems $from"

    ::xmpp::xml::split $xmlElement tag xmlns attrs cdata subels

    if {[::xmpp::xml::isAttr $attrs node]} {
        return [list error cancel service-unavailable]
    }

    return [list result [::xmpp::xml::create query -xmlns $xmlns]]
}

proc jsend::iqLast {xlib from xmlElement args} {
    variable lib

    ::LOG "jsend::iqLast $from"

    set now [clock seconds]
    set xmldata \
        [::xmpp::xml::create query -xmlns jabber:iq:last \
                                   -attrs [list seconds \
                                                [expr {$now - $lib(lastwhen)}]]]
    return [list result $xmldata]
}

proc jsend::iqTime {xlib from xmlElement args} {
    ::LOG "jsend::iqTime $from"

    set now [clock seconds]
    set gmtP true
    foreach {k f} [list utc     "%Y%m%dT%T" \
                        tz      "%Z"        \
                        display "%a %b %d %H:%M:%S %Z %Y"] {
        lappend tags [::xmpp::xml::create $k -cdata [clock format $now \
                                                           -format $f  \
                                                           -gmt    $gmtP]]
        set gmtP false
    }
    set xmldata [::xmpp::xml::create query -xmlns jabber:iq:time \
                                           -subelements $tags]
    return [list result $xmldata]
}

proc jsend::iqVersion {xlib from xmlElement args} {
    global argv0 tcl_platform

    ::LOG "jsend::iqVersion $from"

    foreach {k v} [list name    [file tail [file rootname $argv0]] \
                        version "1.0 (Tcl [info patchlevel])"      \
                        os      "$tcl_platform(os) $tcl_platform(osVersion)"] {
        lappend tags [::xmpp::xml::create $k -cdata $v]
    }
    set xmldata [::xmpp::xml::create query -xmlns jabber:iq:version \
                                           -subelements $tags]
    return [list result $xmldata]
}

proc client:reconnect {xlib} {
    jsend::reconnect
}

proc client:disconnect {xlib} {
    jsend::reconnect
}

proc client:status {args} {
    ::LOG "client:status $args"
}


namespace eval jsend {
    variable stayP 1
}

proc jsend::follow {file argv} {
    proc [namespace current]::reconnect {} \
         [list [namespace current]::reconnect_aux $argv]

    if {[catch { eval [list jsend::sendit 2] $argv } result]} {
        ::bgerror $result
        return
    }

    set buffer ""
    set fd ""
    set newP 1
    array set st [list dev 0 ino 0 size 0]

    for {set i 0} {1} {incr i} {
        if {[expr {$i % 5}] == 0} {
            if {[catch { file stat $file st2 } result]} {
                ::LOG $result
                break
            }

            if {($st(dev) != $st2(dev)) \
                    || ($st(ino) != $st2(ino)) \
                    || ($st(size) > $st2(size))} {
                if {$newP} {
                    catch { close $fd }
                }

                fconfigure [set fd [open $file { RDONLY }]] -blocking off
                unset st
                array set st [array get st2]

                if {!$newP && [string equal $st(type) file]} {
                    seek $fd 0 end
                }

                if {!$newP} {
                    set newP 0
                }

                if {[string length $buffer] > 0} {
                    if {[catch { eval [list jsend::sendit 1] $argv \
                                      [parse $buffer] \
                                      [list -body $buffer] } result]} {
                        ::LOG $result
                        break
                    } elseif {$result} {
                        set buffer ""
                    }
                }
            }
        }

        if {[fblocked $fd]} {
        } elseif {[catch {
            set len [string length [set line [read $fd]]]
            append buffer $line
        } result]} {
            ::LOG $result
            break
        } elseif {[set x [string first "\n" $buffer]] < 0} {
        } else {
            set body [string range $buffer 0 [expr {$x-1}]]
            while {[catch { eval [list jsend::sendit 1] $argv [parse $body] \
                                 [list -body $body] } result]} {
                ::LOG $result
            }
            if {$result} {
                set buffer [string range $buffer [expr {$x + 1}] end]
            }
        }

        after 1000 "set alarmP 1"
        vwait alarmP
    }
}

proc jsend::parse {line} {
    set args {}

    if {![string equal [string index $line 15] " "]} {
        return $args
    }
    catch { lappend args -time [clock scan [string range $line 0 14]] }

    set line [string range $line 16 end]
    if {([set d [string first " " $line]] > 0) \
            && ([string first ": " $line] > $d)} {
        lappend args -activity [string trim [string range $line $d end]]
    }

    return $args
}

proc jsend::reconnect_aux {argv} {
    variable stayP

    while {$stayP} {
        after [expr {60*1000}]
        if {![catch { eval [list jsend::sendit 2] $argv } result]} {
            break
        }

        ::LOG $result
    }
}

proc jsend::parse_xhtml {text} {
    return [::xmpp::xml::parseData "<body>$text</body>"]
}

proc ::LOG {text} {
#    puts stderr $text
}

proc ::debugmsg {args} {
#    ::LOG "debugmsg: $args"
}

proc ::bgerror {err} {
    global errorInfo

    ::LOG "$err\n$errorInfo"
}


set status 1

array set jsend::lib [list lastwhen [clock seconds] lastwhat ""]

if {[string equal [file rootname [file tail [lindex $argv 0]]] jsend]} {
    incr argc -1
    set argv [lrange $argv 1 end]
}

if {(([set x [lsearch -exact $argv -help]] >= 0) \
            || ([set x [lsearch -exact $argv --help]] >= 0)) \
        && (($x == 0) || ([expr {$x % 2}]))} {
    puts stdout \
"usage: $argv0 recipient ?options...?
            -follow      file
            -pidfile     file
            -from        jid
            -host        hostname
            -port        number
            -password    string
            -type        string (e.g., 'chat')
            -subject     string
            -body        string
            -xhtml       string
            -description string
            -url         string
            -bosh        string (BOSH URL)
            -tls         boolean (e.g., 'false')
            -starttls    boolean (e.g., 'true')
            -sasl        boolean (e.g., 'true')

If recipient is '-', roster is used.

If both '-body' and '-follow' are absent, the standard input is used.

The file .jsendrc.tcl in the current or in home directory is consulted,
e.g.,

    set args {-from fred@example.com/bedrock -password wilma}

for default values."

    set status 0
} elseif {($argc < 1) || (![expr {$argc % 2}])} {
    puts stderr "usage: $argv0 recipent ?-key value?..."
} elseif {[catch {
    if {([file exists [set file .jsendrc.tcl]]) \
            || ([file exists [set file ~/.jsendrc.tcl]])} {
        set args {}

        source $file

        array set at [list -permissions 600]
        array set at [file attributes $file]

        if {[set x [lsearch -exact $args "-password"]] >= 0 \
                    && ![expr {$x % 2}] \
                    && ![string match *00 $at(-permissions)]} {
            error "file should be mode 0600"
        }

        if {[llength $args] > 0} {
            set argv [eval [list linsert $argv 1] $args]
        }
    }
} result]} {
    puts stderr "error in $file: $result"
} elseif {[set x [lsearch -exact $argv "-follow"]] > 0 && [expr {$x % 2}]} {
    set keep_alive 1
    set keep_alive_interval 3

    if {[set y [lsearch -exact $argv "-pidfile"]] > 0 && [expr {$y % 2}]} {
        set fd [open [set pf [lindex $argv [expr {$y + 1}]]] \
                     {WRONLY CREAT TRUNC}]
        puts $fd [pid]
        close $fd
    }

    jsend::follow [lindex $argv [expr {$x + 1}]] $argv

    catch { file delete -- $pf }
} elseif {[catch { eval [list jsend::sendit 0] $argv } result]} {
    puts stderr $result
} else {
    set status 0
}

exit $status

# vim:ft=tcl:ts=8:sw=4:sts=4:et
