Posted to tcl by dbohdan at Sat May 15 16:05:23 GMT 2021view raw
- # Based upon the picoirc code by Salvatore Sanfillipo and Richard Suchenwirth
- # See http://wiki.tcl.tk/13134 for the original standalone version.
- #
- # This package provides a general purpose minimal IRC client suitable for
- # embedding in other applications. All communication with the parent
- # application is done via an application provided callback procedure.
- #
- # Copyright (c) 2004 Salvatore Sanfillipo
- # Copyright (c) 2004 Richard Suchenwirth
- # Copyright (c) 2007 Patrick Thoyts
- #
- # -------------------------------------------------------------------------
- # See the file "license.terms" for information on usage and redistribution
- # of this file, and for a DISCLAIMER OF ALL WARRANTIES.
- # -------------------------------------------------------------------------
- package require Tcl 8.6
- # -------------------------------------------------------------------------
- namespace eval ::picoirc {
- variable uid
- if {![info exists uid]} { set uid 0 }
- variable defaults {
- server "irc.freenode.net"
- port 6667
- secure 0
- channels ""
- callback ""
- motd {}
- users {}
- keys {}
- }
- namespace export connect send post splituri
- }
- proc ::picoirc::splituri {uri} {
- lassign {{} {} {} {}} secure server port channels
- if {![regexp {^irc(s)?://([^:/]+)(?::([^/]+))?(?:/([^ ]+))?} $uri -> secure server port channels]} {
- regexp {^(?:([^@]+)@)?([^:]+)(?::(\d+))?} $uri -> channels server port
- }
- set secure [expr {$secure eq "s"}]
- set channels [lmap x [split $channels ,] {
- # Filter out parameters that are special according to the IRC URL
- # scheme Internet-Draft.
- if {$x in {needkey needpass}} continue
- set x
- }]
- if {[llength $channels] == 1} {
- set channels [lindex $channels 0]
- }
- if {$port eq {}} { set port [expr {$secure ? 6697: 6667}] }
- return [list $server $port $channels $secure]
- }
- proc ::picoirc::connect args {
- switch [llength $args] {
- 3 { lassign $args callback nick url }
- 4 { lassign $args callback nick passwd url }
- 5 { lassign $args callback nick passwd url keys }
- default {
- return -code error "wrong # args: must be \"callback nick\
- ?passwd? url ?channel-keys?\""
- }
- }
- variable defaults
- variable uid
- set context [namespace current]::irc[incr uid]
- upvar #0 $context irc
- array set irc $defaults
- lassign [splituri $url] server port channels secure
- if {[info exists channels] && $channels ne ""} {set irc(channels) $channels}
- if {[info exists server] && $server ne ""} {set irc(server) $server}
- if {[info exists port] && $port ne ""} {set irc(port) $port}
- if {[info exists secure] && $secure} {set irc(secure) $secure}
- if {[info exists passwd] && $passwd ne ""} {set irc(passwd) $passwd}
- if {[info exists keys] && $keys ne ""} {set irc(keys) $keys}
- set irc(callback) $callback
- set irc(nick) $nick
- Callback $context init
- if {$irc(secure)} {
- set irc(socket) [::tls::socket $irc(server) $irc(port)]
- } else {
- set irc(socket) [socket -async $irc(server) $irc(port)]
- }
- fileevent $irc(socket) readable [list [namespace origin Read] $context]
- fileevent $irc(socket) writable [list [namespace origin Write] $context]
- return $context
- }
- proc ::picoirc::Callback {context state args} {
- upvar #0 $context irc
- if {[llength $irc(callback)] > 0
- && [llength [info commands [lindex $irc(callback) 0]]] == 1} {
- if {[catch {eval $irc(callback) [list $context $state] $args} result]} {
- puts stderr "callback error: $result"
- } else {
- return $result
- }
- }
- }
- proc ::picoirc::Version {context} {
- if {[catch {Callback $context version} ver]} { set ver {} }
- if {$ver eq {}} {
- set ver "PicoIRC:[package provide picoirc]:Tcl [info patchlevel]"
- }
- return $ver
- }
- proc ::picoirc::Write {context} {
- upvar #0 $context irc
- fileevent $irc(socket) writable {}
- if {[set err [fconfigure $irc(socket) -error]] ne ""
- || $irc(secure) && [catch {while {![::tls::handshake $irc(socket)]} {}} err] != 0} {
- Callback $context close $err
- close $irc(socket)
- unset irc
- return
- }
- fconfigure $irc(socket) -blocking 0 -buffering line -translation crlf -encoding utf-8
- Callback $context connect
- if {[info exists irc(passwd)]} {
- send $context "PASS $irc(passwd)"
- }
- set ver [join [lrange [split [Version $context] :] 0 1] " "]
- send $context "NICK $irc(nick)"
- send $context "USER $::tcl_platform(user) 0 * :$ver user"
- foreach channel $irc(channels) key $irc(keys) {
- set command "JOIN $channel"
- if {$key ne {}} {
- append command " $key"
- }
- after idle [list [namespace origin send] $context $command]
- }
- return
- }
- proc ::picoirc::Getnick {s} {
- set nick {}
- regexp {^([^!]*)!} $s -> nick
- return $nick
- }
- proc ::picoirc::Read {context} {
- upvar #0 $context irc
- if {[eof $irc(socket)]} {
- fileevent $irc(socket) readable {}
- Callback $context close
- close $irc(socket)
- unset irc
- return
- }
- if {[gets $irc(socket) line] != -1} {
- if {[string match "PING*" $line]} {
- send $context "PONG [info hostname] [lindex [split $line] 1]"
- return
- }
- # the callback can return -code break to prevent processing the read
- if {[catch {Callback $context debug read $line}] == 3} {
- return
- }
- if {[regexp {:([^!]*)![^ ].* +PRIVMSG ([^ :]+) +:(.*)} $line -> \
- nick target msg]} {
- set type ""
- if {[regexp {^\001(\S+)(?: (.*))?\001$} $msg -> ctcp data]} {
- switch -- $ctcp {
- ACTION { set type ACTION ; set msg $data }
- VERSION {
- send $context "NOTICE $nick :\001VERSION [Version $context]\001"
- return
- }
- PING {
- send $context "NOTICE $nick :\001PING [lindex $data 0]\001"
- return
- }
- TIME {
- set time [clock format [clock seconds] \
- -format {%a %b %d %H:%M:%S %Y %Z}]
- send $context "NOTICE $nick :\001TIME $time\001"
- return
- }
- default {
- set err [string map [list \001 ""] $msg]
- send $context "NOTICE $nick :\001ERRMSG $err : unknown query\001"
- return
- }
- }
- }
- if {[lsearch -exact {ijchain wubchain} $nick] != -1} {
- if {$type eq "ACTION"} {
- regexp {(\S+) (.+)} $msg -> nick msg
- } else {
- regexp {<([^>]+)> (.+)} $msg -> nick msg
- }
- }
- if {$irc(nick) == $target} {set target $nick}
- Callback $context chat $target $nick $msg $type
- } elseif {[regexp {^:([^ ]+(?: +([^ :]+))*)(?: :(.*))?} $line -> parts junk rest]} {
- lassign [split $parts] server code target fourth fifth
- switch -- $code {
- 001 - 002 - 003 - 004 - 005 - 250 - 251 - 252 -
- 254 - 255 - 265 - 266 { return }
- 433 {
- variable nickid ; if {![info exists nickid]} {set nickid 0}
- set seqlen [string length [incr nickid]]
- set irc(nick) [string range $irc(nick) 0 [expr 8-$seqlen]]$nickid
- send $context "NICK $irc(nick)"
- }
- 353 { set irc(users) [concat $irc(users) $rest]; return }
- 366 {
- Callback $context userlist $fourth $irc(users)
- set irc(users) {}
- return
- }
- 332 { Callback $context topic $fourth $rest; return }
- 333 { return }
- 375 { set irc(motd) {} ; return }
- 372 { append irc(motd) $rest ; return}
- 376 { return }
- 311 {
- lassign [split $parts] server code target nick name host x
- set irc(whois,$fourth) [list name $name host $host userinfo $rest]
- return
- }
- 301 - 312 - 317 - 320 { return }
- 319 { lappend irc(whois,$fourth) channels $rest; return }
- 318 {
- if {[info exists irc(whois,$fourth)]} {
- Callback $context userinfo $fourth $irc(whois,$fourth)
- unset irc(whois,$fourth)
- }
- return
- }
- JOIN {
- set nick [Getnick $server]
- Callback $context traffic entered $target $nick
- return
- }
- NICK {
- set nick [Getnick $server]
- if {$irc(nick) == $nick} {set irc(nick) $rest}
- Callback $context traffic nickchange {} $nick $rest
- return
- }
- QUIT - PART {
- set nick [Getnick $server]
- Callback $context traffic left $target $nick
- return
- }
- MODE {
- set nick [Getnick $server]
- if {$fourth != ""} {
- Callback $context mode $nick $target "$fourth $fifth"
- } else {
- Callback $context mode $nick $target $rest
- }
- return
- }
- NOTICE {
- if {$target in [list $irc(nick) *]} {
- set target {}
- }
- Callback $context chat $target [Getnick $server] $rest NOTICE
- return
- }
- }
- Callback $context system "" "[lrange [split $parts] 1 end] $rest"
- } else {
- Callback $context system "" $line
- }
- }
- }
- proc ::picoirc::post {context channel msg} {
- upvar #0 $context irc
- foreach line [split $msg \n] {
- if [regexp {^/([^ ]+) *(.*)} $line -> cmd msg] {
- regexp {^([^ ]+)?(?: +(.*))?} $msg -> first rest
- switch -- $cmd {
- me {
- if {$channel eq ""} {
- Callback $context system "" "not in channel"
- continue
- }
- send $context "PRIVMSG $channel :\001ACTION $msg\001"
- Callback $context chat $channel $irc(nick) $msg ACTION
- }
- nick {send $context "NICK $msg"}
- quit {send $context "QUIT"}
- part {send $context "PART $channel"}
- names {send $context "NAMES $channel"}
- whois {send $context "WHOIS $msg"}
- kick {send $context "KICK $channel $first :$rest"}
- mode {send $context "MODE $msg"}
- topic {send $context "TOPIC $channel :$msg"}
- quote {send $context $msg}
- join {send $context "JOIN $msg"}
- version {send $context "PRIVMSG $first :\001VERSION\001"}
- msg - notice {
- set type [expr {$cmd == "msg" ? "" : "NOTICE"}]
- set cmd [expr {$cmd == "msg" ? "PRIVMSG" : "NOTICE"}]
- send $context "$cmd $first :$rest"
- Callback $context chat $first $irc(nick) $rest $type
- }
- default {Callback $context system $channel "unknown command /$cmd"}
- }
- continue
- }
- if {$channel eq ""} {
- Callback $context system "" "not in channel"
- continue
- }
- send $context "PRIVMSG $channel :$line"
- Callback $context chat $channel $irc(nick) $line
- }
- }
- proc ::picoirc::send {context line} {
- upvar #0 $context irc
- # the callback can return -code break to prevent writing to socket
- if {[catch {Callback $context debug write $line}] != 3} {
- puts $irc(socket) $line
- }
- }
- # -------------------------------------------------------------------------
- package provide picoirc 0.10.0
- # -------------------------------------------------------------------------
- return