chiark / gitweb /
Many bugfixes from actual testing!
[ircbot] / irccore.tcl
1 proc defset {varname val} {
2     upvar 1 $varname var
3     if {![info exists var]} { set var $val }
4 }
5
6 # must set host
7 defset port 6667
8
9 defset nick testbot
10 defset ident blight
11 defset ownfullname "testing bot"
12 defset ownmailaddr test-irc-bot@example.com
13
14 defset muststartby_ms 10000
15 defset out_maxburst 6
16 defset out_interval 2100
17 defset out_lag_lag 5000
18 defset out_lag_very 25000
19 defset ownping_every 300000
20
21 proc manyset {list args} {
22     foreach val $list var $args {
23         upvar 1 $var my
24         set my $val
25     }
26 }
27
28 proc try_except_finally {try except finally} {
29     global errorInfo errorCode
30     set er [catch { uplevel 1 $try } emsg]
31     if {$er} {
32         set ei $errorInfo
33         set ec $errorCode
34         if {[catch { uplevel 1 $except } emsg3]} {
35             append ei "\nALSO ERROR HANDLING ERROR:\n$emsg3"
36         }
37     }
38     set er2 [catch { uplevel 1 $finally } emsg2]
39     if {$er} {
40         if {$er2} {
41             append ei "\nALSO ERROR CLEANING UP:\n$emsg2"
42         }
43         return -code $er -errorinfo $ei -errorcode $ec $emsg
44     } elseif {$er2} {
45         return -code $er2 -errorinfo $errorInfo -errorcode $errorCode $emsg2
46     } else {
47         return $emsg
48     }
49 }
50
51 proc out__vars {} {
52     uplevel 1 {
53         global out_queue out_creditms out_creditat out_interval out_maxburst
54         global out_lag_lag out_lag_very
55 #set pr [lindex [info level 0] 0]
56 #puts $pr>[clock seconds]|$out_creditat|$out_creditms|[llength $out_queue]<
57     }
58 }
59
60 proc out_lagged {} {
61     out__vars
62     if {[llength $out_queue]*$out_interval > $out_lag_very} {
63         return 2
64     } elseif {[llength $out_queue]*$out_interval > $out_lag_lag} {
65         return 1
66     } else {
67         return 0
68     }
69 }
70
71 proc out_restart {} {
72     out__vars
73     
74     set now [clock seconds]
75     incr out_creditms [expr {($now - $out_creditat) * 1000}]
76     set out_creditat $now
77     if {$out_creditms > $out_maxburst*$out_interval} {
78         set out_creditms [expr {$out_maxburst*$out_interval}]
79     }
80     out_runqueue $now
81 }
82
83 proc out_runqueue {now} {
84     global sock
85     out__vars
86     
87     while {[llength $out_queue] && $out_creditms >= $out_interval} {
88 #puts rq>$now|$out_creditat|$out_creditms|[llength $out_queue]<
89         manyset [lindex $out_queue 0] orgwhen msg
90         set out_queue [lrange $out_queue 1 end]
91         if {[llength $out_queue]} {
92             append orgwhen "+[expr {$now - $orgwhen}]"
93             append orgwhen "([llength $out_queue])"
94         }
95         puts "$orgwhen -> $msg"
96         puts $sock $msg
97         incr out_creditms -$out_interval
98     }
99     if {[llength $out_queue]} {
100         after $out_interval out_nextmessage
101     }
102 }
103
104 proc out_nextmessage {} {
105     out__vars
106     set now [clock seconds]
107     incr out_creditms $out_interval
108     set out_creditat $now
109     out_runqueue $now
110 }
111
112 proc sendout_priority {priority command args} {
113     global sock out_queue
114     if {[llength $args]} {
115         set la [lindex $args end]
116         set args [lreplace $args end end]
117         foreach i $args {
118             if {[regexp {[: ]} $i]} {
119                 error "bad argument in output $i ($command $args)"
120             }
121         }
122         lappend args :$la
123     }
124     set args [lreplace $args 0 -1 $command]
125     set string [join $args { }]
126     set now [clock seconds]
127     set newe [list $now $string]
128     if {$priority} {
129         set out_queue [concat [list $newe] $out_queue]
130     } else {
131         lappend out_queue $newe
132     }
133     if {[llength $out_queue] == 1} {
134         out_restart
135     }
136 }
137
138 proc sendout {command args} { eval sendout_priority [list 0 $command] $args }
139     
140 proc log {data} {
141     puts $data
142 }
143     
144 proc log_intern {what data} {
145     puts "[clock seconds] ++ $what $data"
146 }
147
148 proc logerror {data} {
149     log $data
150 }
151
152 proc saveeic {} {
153     global saveei saveec errorInfo errorCode
154
155     set saveei $errorInfo
156     set saveec $errorCode
157
158     puts ">$saveec|$saveei<"
159 }
160
161 proc bgerror {msg} {
162     global save
163     logerror $msg
164     saveeic
165 }
166
167 proc onread {args} {
168     global sock nick calling_nick errorInfo errorCode line_org_endchar
169
170     if {[catch { gets $sock line } rv]} { fail "error on input: $rv" }
171     if {$rv == -1} { fail "EOF on input" }
172
173     set line_org_endchar [string range $line end end]
174     regsub -all "\[^ -\176\240-\376\]" $line ? line
175     set org $line
176     
177     new_event
178     
179     if {[regexp -nocase {^:([^ ]+) (.*)} $line dummy prefix remain]} {
180         set line $remain
181         if {[regexp {^([^!]+)!} $prefix dummy maybenick]} {
182             set calling_nick $maybenick
183             if {"[irctolower $maybenick]" == "[irctolower $nick]"} return
184         }
185     } else {
186         set prefix {}
187     }
188     if {![string length $line]} { return }
189     if {![regexp -nocase {^([0-9a-z]+) *(.*)} $line dummy command line]} {
190         log "bad command: $org"
191         return
192     }
193     set command [string toupper $command]
194     set params {}
195     while {[regexp {^([^ :]+) *(.*)} $line dummy thisword line]} {
196         lappend params $thisword
197     }
198     if {[regexp {^:(.*)} $line dummy thisword]} {
199         lappend params $thisword
200     } elseif {[string length $line]} {
201         log "junk at end: $org"
202         return
203     }
204     if {"$command" == "PRIVMSG" && \
205         [privmsg_unlogged $prefix [ischan [lindex $params 0]] $params]} {
206         return
207     }
208     log "[clock seconds] <- $org"
209     set procname msg_$command
210     if {[catch { info body $procname }]} { return }
211     if {[catch {
212         eval [list $procname $prefix $command] $params
213     } emsg]} {
214         logerror "error: $emsg ($prefix $command $params)"
215         saveeic
216     }
217 }
218
219 proc catch_restoreei {body} {
220     global errorInfo errorCode
221     set l [list $errorInfo $errorCode]
222     catch { uplevel 1 $body }
223     manyset $l errorInfo errorCode
224 }
225
226 proc catch_logged {body} {
227     global errorInfo
228     if {[catch { uplevel 1 $body } emsg]} {
229         logerror "error (catch_logged): $emsg\n $errorInfo"
230     }
231 }
232
233 proc sendprivmsg {dest l} {
234     foreach v [split $l "\n"] {
235         sendout [expr {[ischan $dest] ? "PRIVMSG" : "NOTICE"}] $dest $v
236     }
237 }
238 proc sendaction_priority {priority dest what} {
239     sendout_priority $priority PRIVMSG $dest "\001ACTION $what\001"
240 }
241 proc msendprivmsg {dest ll} { foreach l $ll { sendprivmsg $dest $l } }
242 proc msendprivmsg_delayed {delay dest ll} { after $delay [list msendprivmsg $dest $ll] }
243
244 proc check_nick {n} {
245     if {[regexp -nocase {[^][\\`_^{|}a-z0-9-]} $n]} { error "bad char in nick" }
246     if {[regexp {^[-0-9]} $n]} { error "bad nick start" }
247     if {[string length $n] > 18} { error "nick too long" }
248 }
249
250 proc ischan {dest} {
251     return [regexp {^[&#+!]} $dest]
252 }
253
254 proc irctolower {v} {
255     foreach {from to} [list "\\\[" "{" \
256                           "\\\]" "}" \
257                           "\\\\" "|" \
258                           "~"    "^"] {
259         regsub -all $from $v $to v
260     }
261     return [string tolower $v]
262 }
263
264 proc prefix_none {} {
265     upvar 1 p p
266     if {[string length $p]} { error "prefix specified" }
267 }
268
269 proc prefix_nick {} {
270     global nick
271     upvar 1 p p
272     upvar 1 n n
273     if {![regexp {^([^!]+)!} $p dummy n]} { error "not from nick" }
274     check_nick $n
275     if {"[irctolower $n]" == "[irctolower $nick]"} {
276         error "from myself" {} {}
277     }
278 }
279
280 proc msg_PING {p c s1} {
281     prefix_none
282     sendout PONG $s1
283 }
284
285 proc sendownping {} {
286     global ownping_every nick
287     sendout ping $nick
288     after $ownping_every sendownping
289 }
290
291 proc msg_001 {args} {
292     global muststartby_after
293     if {[info exists muststartby_after]} {
294         after cancel $muststartby_after
295         unset muststartby_after
296         sendownping
297         connected
298     }
299 }
300
301 proc ensure_outqueue {} {
302     out__vars
303     if {[info exists out_queue]} return
304     set out_creditms 0
305     set out_creditat [clock seconds]
306     set out_queue {}
307     set out_lag_reported 0
308     set out_lag_reportwhen $out_creditat
309 }
310
311 proc fail {msg} {
312     logerror "failing: $msg"
313     exit 1
314 }
315
316 proc ensure_connecting {} {
317     global sock ownfullname host port nick ident socketargs
318     global muststartby_ms muststartby_after
319
320     ensure_outqueue
321     
322     if {[info exists sock]} return
323     set sock [eval socket $socketargs [list $host $port]]
324     fconfigure $sock -buffering line
325     fconfigure $sock -translation crlf
326
327     sendout USER $ident 0 * $ownfullname
328     sendout NICK $nick
329     fileevent $sock readable onread
330
331     set muststartby_after [after $muststartby_ms \
332             {fail "no successfuly connect within timeout"}]
333 }