Fossil SCM

Remove obsolete chat scripts from the tools/ folder.

drh 2021-02-06 09:24 trunk
Commit c2961945dfac0c7abb7e969690f14817a6b25c76626dabbb4f629bd00bff6e1b
D tools/chat.tcl
-509
--- a/tools/chat.tcl
+++ b/tools/chat.tcl
@@ -1,509 +0,0 @@
1
-#!/usr/bin/wapptclsh
2
-#
3
-# A chat program designed to run using the extcgi mechanism of Fossil.
4
-#
5
-encoding system utf-8
6
-
7
-# The name of the chat database file
8
-#
9
-proc chat-db-name {} {
10
- set x [wapp-param SCRIPT_FILENAME]
11
- set dir [file dir $x]
12
- set fn [file tail $x]
13
- return $dir/-$fn.db
14
-}
15
-
16
-# Verify permission to use chat. Return true if not authorized.
17
-# Return false if the Fossil user is allowed to access chat.
18
-#
19
-proc not-authorized {} {
20
- set cap [wapp-param FOSSIL_CAPABILITIES]
21
- return [expr {![string match *i* $cap]}]
22
-}
23
-
24
-# The default page.
25
-# Load the initial chat screen.
26
-#
27
-proc wapp-default {} {
28
- wapp-content-security-policy off
29
- wapp-trim {
30
- <div class="fossil-doc" data-title="Chat">
31
- }
32
- if {[not-authorized]} {
33
- wapp-trim {
34
- <h1>Not authorized</h1>
35
- <p>You must have privileges to use this chatroom</p>
36
- </div>
37
- }
38
- return
39
- }
40
- set scriptFile [wapp-param SCRIPT_FILENAME]
41
- set cgiFn [file tail $scriptFile]
42
- wapp-trim {
43
- <form accept-encoding="utf-8" id="chat-form">
44
- <div id='chat-input-area'>
45
- <div id='chat-input-line'>
46
- <input type="text" name="msg" id="sbox" placeholder="Type message here.">
47
- <input type="submit" value="Send">
48
- </div>
49
- <div id='chat-input-file'>
50
- <span>File:</span>
51
- <input type="file" name="file">
52
- </div>
53
- </div>
54
- </form>
55
- <hr>
56
- <span id='message-inject-point'><!--
57
- new chat messages get inserted immediately after this element
58
- --></span>
59
-
60
- </div><!-- .fossil-doc -->
61
- <hr>
62
- <p>
63
- <a href="%string($cgiFn)/env">CGI environment</a> |
64
- <a href="%string($cgiFn)/self">Wapp script</a>
65
- <style>
66
-\#dialog {
67
- width: 97%;
68
-}
69
-\#chat-input-area {
70
- width: 100%;
71
- display: flex;
72
- flex-direction: column;
73
-}
74
-\#chat-input-line {
75
- display: flex;
76
- flex-direction: row;
77
- margin-bottom: 1em;
78
- align-items: center;
79
-}
80
-\#chat-input-line > input[type=submit] {
81
- flex: 1 5 auto;
82
- max-width: 6em;
83
-}
84
-\#chat-input-line > input[type=text] {
85
- flex: 5 1 auto;
86
-}
87
-\#chat-input-file {
88
- display: flex;
89
- flex-direction: row;
90
- align-items: center;
91
-}
92
-\#chat-input-file > input {
93
- flex: 1 0 auto;
94
-}
95
-span.at-name { /* for @USERNAME references */
96
- text-decoration: underline;
97
- font-weight: bold;
98
-}
99
-/* A wrapper for a single single message (one row of the UI) */
100
-.message-row {
101
- margin-bottom: 0.5em;
102
- border: none;
103
- display: flex;
104
- flex-direction: row;
105
- justify-content: flex-start;
106
- /*border: 1px solid rgba(0,0,0,0.2);
107
- border-radius: 0.25em;
108
- box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);*/
109
- border: none;
110
-}
111
-/* Rows for the current user have the .user-is-me CSS class
112
- and get right-aligned. */
113
-.message-row.user-is-me {
114
- justify-content: flex-end;
115
- /*background-color: #d2dde1;*/
116
-}
117
-/* The content area of a message (the body element of a FIELDSET) */
118
-.message-content {
119
- display: inline-block;
120
- border-radius: 0.25em;
121
- border: 1px solid rgba(0,0,0,0.2);
122
- box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);
123
- padding: 0.25em 1em;
124
- margin-top: -0.75em;
125
-}
126
-.message-row.user-is-me .message-content {
127
- background-color: #d2dde1;
128
-}
129
-/* User name for the post (a LEGEND element) */
130
-.message-row .message-user {
131
- background: inherit;
132
- border-radius: 0.25em 0.25em 0 0;
133
- padding: 0 0.5em;
134
- /*text-align: left; Firefox requires the 'align' attribute */
135
- margin-left: 0.25em;
136
- padding: 0 0.5em 0em 0.5em;
137
- margin-bottom: 0.4em;
138
- background-color: #d2dde1;
139
-}
140
-/* Reposition "my" posts to the right */
141
-.message-row.user-is-me .message-user {
142
- /*text-align: right; Firefox requires the 'align' attribute */
143
- margin-left: 0;
144
- margin-right: 0.25em;
145
-}
146
-</style>
147
- }
148
- set nonce [wapp-param FOSSIL_NONCE]
149
- set submiturl [wapp-param SCRIPT_NAME]/send
150
- set pollurl [wapp-param SCRIPT_NAME]/poll
151
- set downloadurl [wapp-param SCRIPT_NAME]/download
152
- set me [wapp-param FOSSIL_USER]
153
- wapp-trim {
154
-<script nonce="%string($nonce)">
155
-(function(){
156
- const form = document.querySelector('#chat-form');
157
- let mxMsg = 0;
158
- let _me = "%string($me)";
159
- form.addEventListener('submit',(e)=>{
160
- e.preventDefault();
161
- if( form.msg.value.length>0 || form.file.value.length>0 ){
162
- fetch("%string($submiturl)",{
163
- method: 'POST',
164
- body: new FormData(form)
165
- });
166
- }
167
- form.msg.value = "";
168
- form.file.value = "";
169
- form.msg.focus();
170
- });
171
- const rxUrl = /\\b(?:https?|ftp):\\/\\/\[a-z0-9-+&@\#\\/%?=~_|!:,.;]*\[a-z0-9-+&@\#\\/%=~_|]/gim;
172
- const rxAtName = /@\\w+/gmi;
173
- // ^^^ achtung, extra backslashes needed for the outer TCL.
174
- const textNode = (T)=>document.createTextNode(T);
175
-
176
- // Converts a message string to a message-containing DOM element
177
- // and returns that element, which may contain child elements.
178
- // If 2nd arg is passed, it must be a DOM element to which all
179
- // child elements are appended.
180
- const messageToDOM = function f(str, tgtElem){
181
- "use strict";
182
- if(!f.rxUrl){
183
- f.rxUrl = rxUrl;
184
- f.rxAt = rxAtName;
185
- f.rxNS = /\\S/;
186
- f.ce = (T)=>document.createElement(T);
187
- f.ct = (T)=>document.createTextNode(T);
188
- f.replaceUrls = function ff(sub, offset, whole){
189
- if(offset > ff.prevStart){
190
- f.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' ');
191
- }
192
- const a = f.ce('a');
193
- a.setAttribute('href',sub);
194
- a.setAttribute('target','_blank');
195
- a.appendChild(f.ct(sub));
196
- f.accum.push(a);
197
- ff.prevStart = offset + sub.length + 1;
198
- };
199
- f.replaceAtName = function ff(sub, offset,whole){
200
- if(offset > ff.prevStart){
201
- ff.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' ');
202
- }else if(offset && f.rxNS.test(whole[offset-1])){
203
- // Sigh: https://stackoverflow.com/questions/52655367
204
- ff.accum.push(sub);
205
- return;
206
- }
207
- const e = f.ce('span');
208
- e.classList.add('at-name');
209
- e.appendChild(f.ct(sub));
210
- ff.accum.push(e);
211
- ff.prevStart = offset + sub.length + 1;
212
- };
213
- }
214
- f.accum = []; // accumulate strings and DOM elements here.
215
- f.rxUrl.lastIndex = f.replaceUrls.prevStart = 0; // reset regex cursor
216
- str.replace(f.rxUrl, f.replaceUrls);
217
- // Push remaining non-URL part of the string to the queue...
218
- if(f.replaceUrls.prevStart < str.length){
219
- f.accum.push((f.replaceUrls.prevStart?' ':'')+str.substring(f.replaceUrls.prevStart));
220
- }
221
- // Pass 2: process @NAME references...
222
- // TODO: only match NAME if it's the name of a currently participating
223
- // user. Add a second class if NAME == current user, and style that one
224
- // differently so that people can more easily see when they're spoken to.
225
- const accum2 = f.replaceAtName.accum = [];
226
- //console.debug("f.accum =",f.accum);
227
- f.accum.forEach(function(v){
228
- //console.debug("v =",v);
229
- if('string'===typeof v){
230
- f.rxAt.lastIndex = f.replaceAtName.prevStart = 0;
231
- v.replace(f.rxAt, f.replaceAtName);
232
- if(f.replaceAtName.prevStart < v.length){
233
- accum2.push((f.replaceAtName.prevStart?' ':'')+v.substring(f.replaceAtName.prevStart));
234
- }
235
- }else{
236
- accum2.push(v);
237
- }
238
- //console.debug("accum2 =",accum2);
239
- });
240
- delete f.accum;
241
- //console.debug("accum2 =",accum2);
242
- const span = tgtElem || f.ce('span');
243
- accum2.forEach(function(e){
244
- if('string'===typeof e) e = f.ct(e);
245
- span.appendChild(e);
246
- });
247
- //console.debug("span =",span.innerHTML);
248
- return span;
249
- }/*end messageToDOM()*/;
250
- /* Injects element e as a new row in the chat, at the top of the list */
251
- const injectMessage = function f(e){
252
- if(!f.injectPoint){
253
- f.injectPoint = document.querySelector('#message-inject-point');
254
- }
255
- if(f.injectPoint.nextSibling){
256
- f.injectPoint.parentNode.insertBefore(e, f.injectPoint.nextSibling);
257
- }else{
258
- f.injectPoint.parentNode.appendChild(e);
259
- }
260
- };
261
- /** Returns the local time string of Date object d, defaulting
262
- to the current time. */
263
- const localTimeString = function ff(d){
264
- if(!ff.pad){
265
- ff.pad = (x)=>(''+x).length>1 ? x : '0'+x;
266
- }
267
- d || (d = new Date());
268
- return [
269
- d.getFullYear(),'-',ff.pad(d.getMonth()+1/*sigh*/),
270
- '-',ff.pad(d.getDate()),
271
- ' ',ff.pad(d.getHours()),':',ff.pad(d.getMinutes()),
272
- ':',ff.pad(d.getSeconds())
273
- ].join('');
274
- };
275
- function newcontent(jx){
276
- var i;
277
- for(i=0; i<jx.msgs.length; ++i){
278
- let m = jx.msgs[i];
279
- let row = document.createElement("fieldset");
280
- if( m.msgid>mxMsg ) mxMsg = m.msgid;
281
- row.classList.add('message-row');
282
- injectMessage(row);
283
- const eWho = document.createElement('legend');
284
- eWho.setAttribute('align', (m.xfrom===_me ? 'right' : 'left'));
285
- row.appendChild(eWho);
286
- eWho.classList.add('message-user');
287
- let whoName;
288
- if( m.xfrom===_me ){
289
- whoName = 'me';
290
- row.classList.add('user-is-me');
291
- }else{
292
- whoName = m.xfrom;
293
- }
294
- eWho.append(textNode(
295
- whoName+' @ '+
296
- localTimeString(new Date(Date.parse(m.mtime+".000Z"))))
297
- );
298
- let span = document.createElement("div");
299
- span.classList.add('message-content');
300
- row.appendChild(span);
301
- if( m.fsize>0 ){
302
- if( m.fmime && m.fmime.startsWith("image/") ){
303
- let img = document.createElement("img");
304
- img.src = "%string($downloadurl)/" + m.msgid;
305
- span.appendChild(img);
306
- }else{
307
- let a = document.createElement("a");
308
- let txt = "(" + m.fname + " " + m.fsize + " bytes)";
309
- a.href = "%string($downloadurl)/" + m.msgid;
310
- a.appendChild(document.createTextNode(txt));
311
- span.appendChild(a);
312
- }
313
- let br = document.createElement("br");
314
- br.style.clear = "both";
315
- span.appendChild(br);
316
- }
317
- if(m.xmsg){
318
- messageToDOM(m.xmsg, span);
319
- }
320
- span.classList.add('chat-message');
321
- if( m.xfrom!=_me ){
322
- span.classList.add('chat-mx');
323
- }else{
324
- span.classList.add('chat-ms');
325
- }
326
- }
327
- }
328
- async function poll(){
329
- if(poll.running) return;
330
- poll.running = true;
331
- fetch("%string($pollurl)/" + mxMsg)
332
- .then(x=>x.json())
333
- .then(y=>newcontent(y))
334
- .finally(()=>poll.running=false)
335
- }
336
- setInterval(poll, 1000);
337
-})();</script>
338
- }
339
-
340
- # Make sure the chat database exists
341
- sqlite3 db [chat-db-name]
342
- if {[db one {PRAGMA journal_mode}]!="wal"} {
343
- db eval {PRAGMA journal_mode=WAL}
344
- }
345
- db eval {
346
- CREATE TABLE IF NOT EXISTS chat(
347
- msgid INTEGER PRIMARY KEY AUTOINCREMENT,
348
- mtime JULIANDAY,
349
- xfrom TEXT,
350
- xto TEXT,
351
- xmsg TEXT,
352
- file BLOB,
353
- fname TEXT,
354
- fmime TEXT
355
- );
356
- CREATE TABLE IF NOT EXISTS ustat(
357
- uname TEXT PRIMARY KEY,
358
- mtime JULIANDAY, -- Last interaction
359
- seen INT, -- Last message seen
360
- logout JULIANDAY
361
- ) WITHOUT ROWID;
362
- }
363
- db close
364
-}
365
-
366
-# Show the CGI environment. Used for testing only.
367
-#
368
-proc wapp-page-env {} {
369
- wapp-trim {
370
- <div class="fossil-doc" data-title="Chat CGI Environment">
371
- <pre>%html([wapp-debug-env])</pre>
372
- </div>
373
- }
374
-}
375
-
376
-# Log the CGI environment into the "-logfile.txt" file in the same
377
-# directory as the script. Used for testing and development only.
378
-#
379
-proc logenv {} {
380
- set fn [file dir [wapp-param SCRIPT_FILENAME]]/-logfile.txt
381
- set out [open $fn a]
382
- puts $out {************************************************************}
383
- puts $out [wapp-debug-env]
384
- close $out
385
-}
386
-
387
-# A no-op page. Used for testing and development only.
388
-#
389
-proc noop-page {} {
390
- wapp-trim {
391
- <div class="fossil-doc" data-title="No-op"><h1>No-Op</h1></div>
392
- }
393
-}
394
-
395
-# Accept a new post via XHR.
396
-# No reply expected.
397
-#
398
-proc wapp-page-send {} {
399
- if {[not-authorized]} return
400
- set user [wapp-param FOSSIL_USER]
401
- set fcontent [wapp-param file.content]
402
- set fname [wapp-param file.filename]
403
- set fmime [wapp-param file.mimetype]
404
- set msg [wapp-param msg]
405
- sqlite3 db [chat-db-name]
406
- db eval BEGIN
407
- if {$fcontent!=""} {
408
- db eval {
409
- INSERT INTO chat(mtime,xfrom,xmsg,file,fname,fmime)
410
- VALUES(julianday('now'),$user,@msg,@fcontent,$fname,$fmime)
411
- }
412
- } else {
413
- db eval {
414
- INSERT INTO chat(mtime,xfrom,xmsg)
415
- VALUES(julianday('now'),$user,@msg)
416
- }
417
- }
418
- db eval {
419
- INSERT INTO ustat(uname,mtime,seen) VALUES($user,julianday('now'),0)
420
- ON CONFLICT(uname) DO UPDATE set mtime=julianday('now')
421
- }
422
- db eval COMMIT
423
- db close
424
-}
425
-
426
-# Request updates.
427
-# Delay the response until something changes (as this system works
428
-# using the Hanging-GET or Long-Poll style of server-push).
429
-# The result is javascript describing the new content.
430
-#
431
-# Call is like this: /poll/N
432
-# Where N is the last message received so far. The reply stalls
433
-# until newer messages are available.
434
-#
435
-proc wapp-page-poll {} {
436
- if {[not-authorized]} return
437
- wapp-mimetype text/json
438
- set msglist {}
439
- sqlite3 db [chat-db-name]
440
- set id 0
441
- scan [wapp-param PATH_TAIL] %d id
442
- while {1} {
443
- set datavers [db one {PRAGMA data_version}]
444
- db eval {SELECT msgid, datetime(mtime) AS dx, xfrom, CAST(xmsg AS text) mx,
445
- length(file) AS lx, fname, fmime
446
- FROM chat
447
- WHERE msgid>$id
448
- ORDER BY msgid} {
449
- set quname [string map {\" \\\"} $xfrom]
450
- set qmsg [string map {\" \\\"} $mx]
451
- if {$lx==""} {set lx 0}
452
- set qfname [string map {\" \\\"} $fname]
453
- lappend msglist "\173\"msgid\":$msgid,\"mtime\":\"$dx\",\
454
- \"xfrom\":\"$quname\",\
455
- \"xmsg\":\"$qmsg\",\"fsize\":$lx,\
456
- \"fname\":\"$qfname\",\"fmime\":\"$fmime\"\175"
457
- }
458
- if {[llength $msglist]>0} {
459
- wapp-unsafe "\173\042msgs\042:\133[join $msglist ,]\135\175"
460
- db close
461
- return
462
- }
463
- after 2000
464
- while {[db one {PRAGMA data_version}]==$datavers} {after 2000}
465
- }
466
-}
467
-
468
-# Show the text of this script.
469
-#
470
-proc wapp-page-self {} {
471
- wapp-trim {
472
- <div class="fossil-doc" data-title="Wapp Script for Chat">
473
- }
474
- set fd [open [wapp-param SCRIPT_FILENAME] rb]
475
- set script [read $fd]
476
- wapp-trim {
477
- <pre>%html($script)</pre>
478
- }
479
- wapp-trim {
480
- </div>
481
- }
482
-}
483
-
484
-# Download the file associated with a message.
485
-#
486
-# Call like this: /download/N
487
-# Where N is the message id.
488
-#
489
-proc wapp-page-download {} {
490
- if {[not-authorized]} {
491
- wapp-trim {
492
- <h1>Not authorized</h1>
493
- <p>You must have privileges to use this chatroom</p>
494
- </div>
495
- }
496
- return
497
- }
498
- set id 0
499
- scan [wapp-param PATH_TAIL] %d id
500
- sqlite3 db [chat-db-name]
501
- db eval {SELECT fname, fmime, file FROM chat WHERE msgid=$id} {
502
- wapp-mimetype $fmime
503
- wapp $file
504
- }
505
- db close
506
-}
507
-
508
-
509
-wapp-start $argv
--- a/tools/chat.tcl
+++ b/tools/chat.tcl
@@ -1,509 +0,0 @@
1 #!/usr/bin/wapptclsh
2 #
3 # A chat program designed to run using the extcgi mechanism of Fossil.
4 #
5 encoding system utf-8
6
7 # The name of the chat database file
8 #
9 proc chat-db-name {} {
10 set x [wapp-param SCRIPT_FILENAME]
11 set dir [file dir $x]
12 set fn [file tail $x]
13 return $dir/-$fn.db
14 }
15
16 # Verify permission to use chat. Return true if not authorized.
17 # Return false if the Fossil user is allowed to access chat.
18 #
19 proc not-authorized {} {
20 set cap [wapp-param FOSSIL_CAPABILITIES]
21 return [expr {![string match *i* $cap]}]
22 }
23
24 # The default page.
25 # Load the initial chat screen.
26 #
27 proc wapp-default {} {
28 wapp-content-security-policy off
29 wapp-trim {
30 <div class="fossil-doc" data-title="Chat">
31 }
32 if {[not-authorized]} {
33 wapp-trim {
34 <h1>Not authorized</h1>
35 <p>You must have privileges to use this chatroom</p>
36 </div>
37 }
38 return
39 }
40 set scriptFile [wapp-param SCRIPT_FILENAME]
41 set cgiFn [file tail $scriptFile]
42 wapp-trim {
43 <form accept-encoding="utf-8" id="chat-form">
44 <div id='chat-input-area'>
45 <div id='chat-input-line'>
46 <input type="text" name="msg" id="sbox" placeholder="Type message here.">
47 <input type="submit" value="Send">
48 </div>
49 <div id='chat-input-file'>
50 <span>File:</span>
51 <input type="file" name="file">
52 </div>
53 </div>
54 </form>
55 <hr>
56 <span id='message-inject-point'><!--
57 new chat messages get inserted immediately after this element
58 --></span>
59
60 </div><!-- .fossil-doc -->
61 <hr>
62 <p>
63 <a href="%string($cgiFn)/env">CGI environment</a> |
64 <a href="%string($cgiFn)/self">Wapp script</a>
65 <style>
66 \#dialog {
67 width: 97%;
68 }
69 \#chat-input-area {
70 width: 100%;
71 display: flex;
72 flex-direction: column;
73 }
74 \#chat-input-line {
75 display: flex;
76 flex-direction: row;
77 margin-bottom: 1em;
78 align-items: center;
79 }
80 \#chat-input-line > input[type=submit] {
81 flex: 1 5 auto;
82 max-width: 6em;
83 }
84 \#chat-input-line > input[type=text] {
85 flex: 5 1 auto;
86 }
87 \#chat-input-file {
88 display: flex;
89 flex-direction: row;
90 align-items: center;
91 }
92 \#chat-input-file > input {
93 flex: 1 0 auto;
94 }
95 span.at-name { /* for @USERNAME references */
96 text-decoration: underline;
97 font-weight: bold;
98 }
99 /* A wrapper for a single single message (one row of the UI) */
100 .message-row {
101 margin-bottom: 0.5em;
102 border: none;
103 display: flex;
104 flex-direction: row;
105 justify-content: flex-start;
106 /*border: 1px solid rgba(0,0,0,0.2);
107 border-radius: 0.25em;
108 box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);*/
109 border: none;
110 }
111 /* Rows for the current user have the .user-is-me CSS class
112 and get right-aligned. */
113 .message-row.user-is-me {
114 justify-content: flex-end;
115 /*background-color: #d2dde1;*/
116 }
117 /* The content area of a message (the body element of a FIELDSET) */
118 .message-content {
119 display: inline-block;
120 border-radius: 0.25em;
121 border: 1px solid rgba(0,0,0,0.2);
122 box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);
123 padding: 0.25em 1em;
124 margin-top: -0.75em;
125 }
126 .message-row.user-is-me .message-content {
127 background-color: #d2dde1;
128 }
129 /* User name for the post (a LEGEND element) */
130 .message-row .message-user {
131 background: inherit;
132 border-radius: 0.25em 0.25em 0 0;
133 padding: 0 0.5em;
134 /*text-align: left; Firefox requires the 'align' attribute */
135 margin-left: 0.25em;
136 padding: 0 0.5em 0em 0.5em;
137 margin-bottom: 0.4em;
138 background-color: #d2dde1;
139 }
140 /* Reposition "my" posts to the right */
141 .message-row.user-is-me .message-user {
142 /*text-align: right; Firefox requires the 'align' attribute */
143 margin-left: 0;
144 margin-right: 0.25em;
145 }
146 </style>
147 }
148 set nonce [wapp-param FOSSIL_NONCE]
149 set submiturl [wapp-param SCRIPT_NAME]/send
150 set pollurl [wapp-param SCRIPT_NAME]/poll
151 set downloadurl [wapp-param SCRIPT_NAME]/download
152 set me [wapp-param FOSSIL_USER]
153 wapp-trim {
154 <script nonce="%string($nonce)">
155 (function(){
156 const form = document.querySelector('#chat-form');
157 let mxMsg = 0;
158 let _me = "%string($me)";
159 form.addEventListener('submit',(e)=>{
160 e.preventDefault();
161 if( form.msg.value.length>0 || form.file.value.length>0 ){
162 fetch("%string($submiturl)",{
163 method: 'POST',
164 body: new FormData(form)
165 });
166 }
167 form.msg.value = "";
168 form.file.value = "";
169 form.msg.focus();
170 });
171 const rxUrl = /\\b(?:https?|ftp):\\/\\/\[a-z0-9-+&@\#\\/%?=~_|!:,.;]*\[a-z0-9-+&@\#\\/%=~_|]/gim;
172 const rxAtName = /@\\w+/gmi;
173 // ^^^ achtung, extra backslashes needed for the outer TCL.
174 const textNode = (T)=>document.createTextNode(T);
175
176 // Converts a message string to a message-containing DOM element
177 // and returns that element, which may contain child elements.
178 // If 2nd arg is passed, it must be a DOM element to which all
179 // child elements are appended.
180 const messageToDOM = function f(str, tgtElem){
181 "use strict";
182 if(!f.rxUrl){
183 f.rxUrl = rxUrl;
184 f.rxAt = rxAtName;
185 f.rxNS = /\\S/;
186 f.ce = (T)=>document.createElement(T);
187 f.ct = (T)=>document.createTextNode(T);
188 f.replaceUrls = function ff(sub, offset, whole){
189 if(offset > ff.prevStart){
190 f.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' ');
191 }
192 const a = f.ce('a');
193 a.setAttribute('href',sub);
194 a.setAttribute('target','_blank');
195 a.appendChild(f.ct(sub));
196 f.accum.push(a);
197 ff.prevStart = offset + sub.length + 1;
198 };
199 f.replaceAtName = function ff(sub, offset,whole){
200 if(offset > ff.prevStart){
201 ff.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' ');
202 }else if(offset && f.rxNS.test(whole[offset-1])){
203 // Sigh: https://stackoverflow.com/questions/52655367
204 ff.accum.push(sub);
205 return;
206 }
207 const e = f.ce('span');
208 e.classList.add('at-name');
209 e.appendChild(f.ct(sub));
210 ff.accum.push(e);
211 ff.prevStart = offset + sub.length + 1;
212 };
213 }
214 f.accum = []; // accumulate strings and DOM elements here.
215 f.rxUrl.lastIndex = f.replaceUrls.prevStart = 0; // reset regex cursor
216 str.replace(f.rxUrl, f.replaceUrls);
217 // Push remaining non-URL part of the string to the queue...
218 if(f.replaceUrls.prevStart < str.length){
219 f.accum.push((f.replaceUrls.prevStart?' ':'')+str.substring(f.replaceUrls.prevStart));
220 }
221 // Pass 2: process @NAME references...
222 // TODO: only match NAME if it's the name of a currently participating
223 // user. Add a second class if NAME == current user, and style that one
224 // differently so that people can more easily see when they're spoken to.
225 const accum2 = f.replaceAtName.accum = [];
226 //console.debug("f.accum =",f.accum);
227 f.accum.forEach(function(v){
228 //console.debug("v =",v);
229 if('string'===typeof v){
230 f.rxAt.lastIndex = f.replaceAtName.prevStart = 0;
231 v.replace(f.rxAt, f.replaceAtName);
232 if(f.replaceAtName.prevStart < v.length){
233 accum2.push((f.replaceAtName.prevStart?' ':'')+v.substring(f.replaceAtName.prevStart));
234 }
235 }else{
236 accum2.push(v);
237 }
238 //console.debug("accum2 =",accum2);
239 });
240 delete f.accum;
241 //console.debug("accum2 =",accum2);
242 const span = tgtElem || f.ce('span');
243 accum2.forEach(function(e){
244 if('string'===typeof e) e = f.ct(e);
245 span.appendChild(e);
246 });
247 //console.debug("span =",span.innerHTML);
248 return span;
249 }/*end messageToDOM()*/;
250 /* Injects element e as a new row in the chat, at the top of the list */
251 const injectMessage = function f(e){
252 if(!f.injectPoint){
253 f.injectPoint = document.querySelector('#message-inject-point');
254 }
255 if(f.injectPoint.nextSibling){
256 f.injectPoint.parentNode.insertBefore(e, f.injectPoint.nextSibling);
257 }else{
258 f.injectPoint.parentNode.appendChild(e);
259 }
260 };
261 /** Returns the local time string of Date object d, defaulting
262 to the current time. */
263 const localTimeString = function ff(d){
264 if(!ff.pad){
265 ff.pad = (x)=>(''+x).length>1 ? x : '0'+x;
266 }
267 d || (d = new Date());
268 return [
269 d.getFullYear(),'-',ff.pad(d.getMonth()+1/*sigh*/),
270 '-',ff.pad(d.getDate()),
271 ' ',ff.pad(d.getHours()),':',ff.pad(d.getMinutes()),
272 ':',ff.pad(d.getSeconds())
273 ].join('');
274 };
275 function newcontent(jx){
276 var i;
277 for(i=0; i<jx.msgs.length; ++i){
278 let m = jx.msgs[i];
279 let row = document.createElement("fieldset");
280 if( m.msgid>mxMsg ) mxMsg = m.msgid;
281 row.classList.add('message-row');
282 injectMessage(row);
283 const eWho = document.createElement('legend');
284 eWho.setAttribute('align', (m.xfrom===_me ? 'right' : 'left'));
285 row.appendChild(eWho);
286 eWho.classList.add('message-user');
287 let whoName;
288 if( m.xfrom===_me ){
289 whoName = 'me';
290 row.classList.add('user-is-me');
291 }else{
292 whoName = m.xfrom;
293 }
294 eWho.append(textNode(
295 whoName+' @ '+
296 localTimeString(new Date(Date.parse(m.mtime+".000Z"))))
297 );
298 let span = document.createElement("div");
299 span.classList.add('message-content');
300 row.appendChild(span);
301 if( m.fsize>0 ){
302 if( m.fmime && m.fmime.startsWith("image/") ){
303 let img = document.createElement("img");
304 img.src = "%string($downloadurl)/" + m.msgid;
305 span.appendChild(img);
306 }else{
307 let a = document.createElement("a");
308 let txt = "(" + m.fname + " " + m.fsize + " bytes)";
309 a.href = "%string($downloadurl)/" + m.msgid;
310 a.appendChild(document.createTextNode(txt));
311 span.appendChild(a);
312 }
313 let br = document.createElement("br");
314 br.style.clear = "both";
315 span.appendChild(br);
316 }
317 if(m.xmsg){
318 messageToDOM(m.xmsg, span);
319 }
320 span.classList.add('chat-message');
321 if( m.xfrom!=_me ){
322 span.classList.add('chat-mx');
323 }else{
324 span.classList.add('chat-ms');
325 }
326 }
327 }
328 async function poll(){
329 if(poll.running) return;
330 poll.running = true;
331 fetch("%string($pollurl)/" + mxMsg)
332 .then(x=>x.json())
333 .then(y=>newcontent(y))
334 .finally(()=>poll.running=false)
335 }
336 setInterval(poll, 1000);
337 })();</script>
338 }
339
340 # Make sure the chat database exists
341 sqlite3 db [chat-db-name]
342 if {[db one {PRAGMA journal_mode}]!="wal"} {
343 db eval {PRAGMA journal_mode=WAL}
344 }
345 db eval {
346 CREATE TABLE IF NOT EXISTS chat(
347 msgid INTEGER PRIMARY KEY AUTOINCREMENT,
348 mtime JULIANDAY,
349 xfrom TEXT,
350 xto TEXT,
351 xmsg TEXT,
352 file BLOB,
353 fname TEXT,
354 fmime TEXT
355 );
356 CREATE TABLE IF NOT EXISTS ustat(
357 uname TEXT PRIMARY KEY,
358 mtime JULIANDAY, -- Last interaction
359 seen INT, -- Last message seen
360 logout JULIANDAY
361 ) WITHOUT ROWID;
362 }
363 db close
364 }
365
366 # Show the CGI environment. Used for testing only.
367 #
368 proc wapp-page-env {} {
369 wapp-trim {
370 <div class="fossil-doc" data-title="Chat CGI Environment">
371 <pre>%html([wapp-debug-env])</pre>
372 </div>
373 }
374 }
375
376 # Log the CGI environment into the "-logfile.txt" file in the same
377 # directory as the script. Used for testing and development only.
378 #
379 proc logenv {} {
380 set fn [file dir [wapp-param SCRIPT_FILENAME]]/-logfile.txt
381 set out [open $fn a]
382 puts $out {************************************************************}
383 puts $out [wapp-debug-env]
384 close $out
385 }
386
387 # A no-op page. Used for testing and development only.
388 #
389 proc noop-page {} {
390 wapp-trim {
391 <div class="fossil-doc" data-title="No-op"><h1>No-Op</h1></div>
392 }
393 }
394
395 # Accept a new post via XHR.
396 # No reply expected.
397 #
398 proc wapp-page-send {} {
399 if {[not-authorized]} return
400 set user [wapp-param FOSSIL_USER]
401 set fcontent [wapp-param file.content]
402 set fname [wapp-param file.filename]
403 set fmime [wapp-param file.mimetype]
404 set msg [wapp-param msg]
405 sqlite3 db [chat-db-name]
406 db eval BEGIN
407 if {$fcontent!=""} {
408 db eval {
409 INSERT INTO chat(mtime,xfrom,xmsg,file,fname,fmime)
410 VALUES(julianday('now'),$user,@msg,@fcontent,$fname,$fmime)
411 }
412 } else {
413 db eval {
414 INSERT INTO chat(mtime,xfrom,xmsg)
415 VALUES(julianday('now'),$user,@msg)
416 }
417 }
418 db eval {
419 INSERT INTO ustat(uname,mtime,seen) VALUES($user,julianday('now'),0)
420 ON CONFLICT(uname) DO UPDATE set mtime=julianday('now')
421 }
422 db eval COMMIT
423 db close
424 }
425
426 # Request updates.
427 # Delay the response until something changes (as this system works
428 # using the Hanging-GET or Long-Poll style of server-push).
429 # The result is javascript describing the new content.
430 #
431 # Call is like this: /poll/N
432 # Where N is the last message received so far. The reply stalls
433 # until newer messages are available.
434 #
435 proc wapp-page-poll {} {
436 if {[not-authorized]} return
437 wapp-mimetype text/json
438 set msglist {}
439 sqlite3 db [chat-db-name]
440 set id 0
441 scan [wapp-param PATH_TAIL] %d id
442 while {1} {
443 set datavers [db one {PRAGMA data_version}]
444 db eval {SELECT msgid, datetime(mtime) AS dx, xfrom, CAST(xmsg AS text) mx,
445 length(file) AS lx, fname, fmime
446 FROM chat
447 WHERE msgid>$id
448 ORDER BY msgid} {
449 set quname [string map {\" \\\"} $xfrom]
450 set qmsg [string map {\" \\\"} $mx]
451 if {$lx==""} {set lx 0}
452 set qfname [string map {\" \\\"} $fname]
453 lappend msglist "\173\"msgid\":$msgid,\"mtime\":\"$dx\",\
454 \"xfrom\":\"$quname\",\
455 \"xmsg\":\"$qmsg\",\"fsize\":$lx,\
456 \"fname\":\"$qfname\",\"fmime\":\"$fmime\"\175"
457 }
458 if {[llength $msglist]>0} {
459 wapp-unsafe "\173\042msgs\042:\133[join $msglist ,]\135\175"
460 db close
461 return
462 }
463 after 2000
464 while {[db one {PRAGMA data_version}]==$datavers} {after 2000}
465 }
466 }
467
468 # Show the text of this script.
469 #
470 proc wapp-page-self {} {
471 wapp-trim {
472 <div class="fossil-doc" data-title="Wapp Script for Chat">
473 }
474 set fd [open [wapp-param SCRIPT_FILENAME] rb]
475 set script [read $fd]
476 wapp-trim {
477 <pre>%html($script)</pre>
478 }
479 wapp-trim {
480 </div>
481 }
482 }
483
484 # Download the file associated with a message.
485 #
486 # Call like this: /download/N
487 # Where N is the message id.
488 #
489 proc wapp-page-download {} {
490 if {[not-authorized]} {
491 wapp-trim {
492 <h1>Not authorized</h1>
493 <p>You must have privileges to use this chatroom</p>
494 </div>
495 }
496 return
497 }
498 set id 0
499 scan [wapp-param PATH_TAIL] %d id
500 sqlite3 db [chat-db-name]
501 db eval {SELECT fname, fmime, file FROM chat WHERE msgid=$id} {
502 wapp-mimetype $fmime
503 wapp $file
504 }
505 db close
506 }
507
508
509 wapp-start $argv
--- a/tools/chat.tcl
+++ b/tools/chat.tcl
@@ -1,509 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
D tools/fossil_chat.tcl
-257
--- a/tools/fossil_chat.tcl
+++ b/tools/fossil_chat.tcl
@@ -1,257 +0,0 @@
1
-#!/home/drh/bin/tobe
2
-#
3
-# Simple chat client for Tcl/Tk.
4
-#
5
-package require Tk
6
-
7
-set SERVERHOST fossil-scm.hwaci.com
8
-# set SERVERHOST 127.0.0.1
9
-#set SERVERHOST 64.5.53.192
10
-set SERVERPORT 8615
11
-
12
-# set to correct values if you have to use a proxy
13
-set PROXYHOST {}
14
-set PROXYPORT {}
15
-
16
-# Setup the user interface
17
-wm title . Fossil-Chat
18
-wm iconname . [wm title .]
19
-
20
-menu .mb -type menubar
21
-if {$tcl_platform(platform)=="unix" && $tcl_platform(os)!="Darwin"} {
22
- pack .mb -side top -fill x
23
-} else {
24
- . config -menu .mb
25
-}
26
-.mb add cascade -label File -underline 0 -menu .mb.file
27
-menu .mb.file -tearoff 0
28
-.mb.file add command -label Send -command send_message
29
-.mb.file add command -label {Remove older messages} -command cleanup_record
30
-.mb.file add separator
31
-.mb.file add command -label {Exit} -command exit
32
-
33
-frame .who
34
-pack .who -side right -anchor n -fill y
35
-label .who.title -text {Users: }
36
-pack .who.title -side top -anchor nw
37
-label .who.list -anchor w -justify left -text {}
38
-pack .who.list -side top -anchor nw -expand 1 -padx 5
39
-label .who.time -text {} -justify right
40
-proc update_time {} {
41
- after 1000 update_time
42
- set now [clock seconds]
43
- set time [clock format $now -format %H:%M -gmt 1]
44
- .who.time config -text "UTC: $time"
45
-}
46
-update_time
47
-pack .who.time -side bottom -anchor sw
48
-
49
-frame .input
50
-pack .input -side bottom -fill x
51
-text .input.t -bd 1 -relief sunken -bg white -fg black -width 60 -height 3 \
52
- -wrap word -yscrollcommand [list .input.sb set] -takefocus 1
53
-bind .input.t <Key-Return> {send_message; break}
54
-pack .input.t -side left -fill both -expand 1
55
-scrollbar .input.sb -orient vertical -command [list .input.t yview]
56
-pack .input.sb -side left -fill y
57
-
58
-frame .msg
59
-pack .msg -side top -fill both -expand 1
60
-text .msg.t -bd 1 -relief sunken -bg white -fg black -width 60 -height 20 \
61
- -wrap word -yscrollcommand [list .msg.sb set] -takefocus 0
62
-bindtags .msg.t [list .msg.t . all]
63
-.msg.t tag config error -foreground red
64
-.msg.t tag config meta -foreground forestgreen
65
-.msg.t tag config norm -foreground black
66
-pack .msg.t -side left -fill both -expand 1
67
-scrollbar .msg.sb -orient vertical -command [list .msg.t yview]
68
-pack .msg.sb -side left -fill y
69
-
70
-update
71
-
72
-# Send periodic messages to keep the TCP/IP link up
73
-#
74
-proc keep_alive {} {
75
- global TIMER SOCKET
76
- catch {after cancel $TIMER}
77
- set TIMER [after 300000 keep_alive]
78
- catch {puts $SOCKET noop; flush $SOCKET}
79
-}
80
-
81
-# Connect to the server
82
-proc connect {} {
83
- global SOCKET tcl_platform
84
- catch {close $SOCKET}
85
- if {[catch {
86
- if {$::PROXYHOST ne {}} {
87
- set SOCKET [socket $::PROXYHOST $::PROXYPORT]
88
- puts $SOCKET "CONNECT $::SERVERHOST:$::SERVERPORT HTTP/1.1"
89
- puts $SOCKET "Host: $::SERVERHOST:$::SERVERPORT"
90
- puts $SOCKET ""
91
- } else {
92
- set SOCKET [socket $::SERVERHOST $::SERVERPORT]
93
- }
94
- fconfigure $SOCKET -translation binary -blocking 0
95
- puts $SOCKET [list login $tcl_platform(user) fact,fuzz]
96
- flush $SOCKET
97
- fileevent $SOCKET readable handle_input
98
- keep_alive
99
- } errmsg]} {
100
- if {[tk_messageBox -icon error -type yesno -parent . -message \
101
- "Unable to connect to server. $errmsg.\n\nTry again?"]=="yes"} {
102
- after 100 connect
103
- }
104
- }
105
-}
106
-connect
107
-
108
-# Send the message text contained in the .input.t widget to the server.
109
-#
110
-proc send_message {} {
111
- set txt [.input.t get 1.0 end]
112
- .input.t delete 1.0 end
113
- regsub -all "\[ \t\n\f\r\]+" [string trim $txt] { } txt
114
- if {$txt==""} return
115
- global SOCKET
116
- puts $SOCKET [list message $txt]
117
- flush $SOCKET
118
-}
119
-
120
-.mb add cascade -label "Transfer" -underline 0 -menu .mb.files
121
-menu .mb.files -tearoff 0
122
-.mb.files add command -label "Send file..." -command send_file
123
-.mb.files add command -label "Delete files" -command delete_files \
124
- -state disabled
125
-.mb.files add separator
126
-
127
-# Encode a string (possibly containing binary and \000 characters) into
128
-# single line of text.
129
-#
130
-proc encode {txt} {
131
- return [string map [list % %25 + %2b " " + \n %0a \t %09 \000 %00] $txt]
132
-}
133
-
134
-# Undo the work of encode. Convert an encoded string back into its original
135
-# form.
136
-#
137
-proc decode {txt} {
138
- return [string map [list %00 \000 %09 \t %0a \n + " " %2b + %25 %] $txt]
139
-}
140
-
141
-# Delete all of the downloaded files we are currently holding.
142
-#
143
-proc delete_files {} {
144
- global FILES
145
- .mb.files delete 3 end
146
- array unset FILES
147
- .mb.files entryconfigure 1 -state disabled
148
-}
149
-
150
-# Prompt the user to select a file from the disk. Then send that
151
-# file to all chat participants.
152
-#
153
-proc send_file {} {
154
- global SOCKET
155
- set openfile [tk_getOpenFile]
156
- if {$openfile==""} return
157
- set f [open $openfile]
158
- fconfigure $f -translation binary
159
- set data [read $f]
160
- close $f
161
- puts $SOCKET [list file [file tail $openfile] [encode $data]]
162
- flush $SOCKET
163
- set time [clock format [clock seconds] -format {%H:%M} -gmt 1]
164
- .msg.t insert end "\[$time\] sent file [file tail $openfile]\
165
- - [string length $data] bytes\n" meta
166
- .msg.t see end
167
-}
168
-
169
-# Save the named file to the disk.
170
-#
171
- proc save_file {filename} {
172
- global FILES
173
- set savefile [tk_getSaveFile -initialfile $filename]
174
- if {$savefile==""} return
175
- set f [open $savefile w]
176
- fconfigure $f -translation binary
177
- puts -nonewline $f [decode $FILES($filename)]
178
- close $f
179
-}
180
-
181
-# Handle a "file" message from the chat server.
182
-#
183
-proc handle_file {from filename data} {
184
- global FILES
185
- foreach prior [array names FILES] {
186
- if {$filename==$prior} break
187
- }
188
- if {![info exists prior] || $filename!=$prior} {
189
- .mb.files add command -label "Save \"$filename\"" \
190
- -command [list save_file $filename]
191
- }
192
- set FILES($filename) $data
193
- .mb.files entryconfigure 1 -state active
194
- set time [clock format [clock seconds] -format {%H:%M} -gmt 1]
195
- .msg.t insert end "\[$time $from\] " meta "File: \"$filename\"\n" norm
196
- .msg.t see end
197
-}
198
-
199
-# Handle input from the server
200
-#
201
-proc handle_input {} {
202
- global SOCKET
203
- if {[eof $SOCKET]} {
204
- disconnect
205
- return
206
- }
207
- set line [gets $SOCKET]
208
- if {$line==""} return
209
- set cmd [lindex $line 0]
210
- if {$cmd=="userlist"} {
211
- set ulist {}
212
- foreach u [lrange $line 1 end] {
213
- append ulist $u\n
214
- }
215
- .who.list config -text [string trim $ulist]
216
- } elseif {$cmd=="message"} {
217
- set time [clock format [clock seconds] -format {%H:%M} -gmt 1]
218
- set from [lindex $line 1]
219
- .msg.t insert end "\[$time $from\] " meta [lindex $line 2]\n norm
220
- .msg.t see end
221
- bell
222
- wm deiconify .
223
- update
224
- raise .
225
- } elseif {$cmd=="noop"} {
226
- # do nothing
227
- } elseif {$cmd=="meta"} {
228
- set now [clock seconds]
229
- set time [clock format $now -format {%H:%M} -gmt 1]
230
- .msg.t insert end "\[$time\] [lindex $line 1]\n" meta
231
- .msg.t see end
232
- } elseif {$cmd=="file"} {
233
- if {[info commands handle_file]=="handle_file"} {
234
- handle_file [lindex $line 1] [lindex $line 2] [lindex $line 3]
235
- }
236
- }
237
-}
238
-
239
-# Handle a broken socket connection
240
-#
241
-proc disconnect {} {
242
- global SOCKET
243
- close $SOCKET
244
- set q [tk_messageBox -icon error -type yesno -parent . -message \
245
- "TCP/IP link lost. Try to reconnet?"]
246
- if {$q=="yes"} {
247
- connect
248
- } else {
249
- exit
250
- }
251
-}
252
-
253
-# Remove all but the most recent 100 message from the message log
254
-#
255
-proc cleanup_record {} {
256
- .msg.t delete 1.0 {end -100 lines}
257
-}
--- a/tools/fossil_chat.tcl
+++ b/tools/fossil_chat.tcl
@@ -1,257 +0,0 @@
1 #!/home/drh/bin/tobe
2 #
3 # Simple chat client for Tcl/Tk.
4 #
5 package require Tk
6
7 set SERVERHOST fossil-scm.hwaci.com
8 # set SERVERHOST 127.0.0.1
9 #set SERVERHOST 64.5.53.192
10 set SERVERPORT 8615
11
12 # set to correct values if you have to use a proxy
13 set PROXYHOST {}
14 set PROXYPORT {}
15
16 # Setup the user interface
17 wm title . Fossil-Chat
18 wm iconname . [wm title .]
19
20 menu .mb -type menubar
21 if {$tcl_platform(platform)=="unix" && $tcl_platform(os)!="Darwin"} {
22 pack .mb -side top -fill x
23 } else {
24 . config -menu .mb
25 }
26 .mb add cascade -label File -underline 0 -menu .mb.file
27 menu .mb.file -tearoff 0
28 .mb.file add command -label Send -command send_message
29 .mb.file add command -label {Remove older messages} -command cleanup_record
30 .mb.file add separator
31 .mb.file add command -label {Exit} -command exit
32
33 frame .who
34 pack .who -side right -anchor n -fill y
35 label .who.title -text {Users: }
36 pack .who.title -side top -anchor nw
37 label .who.list -anchor w -justify left -text {}
38 pack .who.list -side top -anchor nw -expand 1 -padx 5
39 label .who.time -text {} -justify right
40 proc update_time {} {
41 after 1000 update_time
42 set now [clock seconds]
43 set time [clock format $now -format %H:%M -gmt 1]
44 .who.time config -text "UTC: $time"
45 }
46 update_time
47 pack .who.time -side bottom -anchor sw
48
49 frame .input
50 pack .input -side bottom -fill x
51 text .input.t -bd 1 -relief sunken -bg white -fg black -width 60 -height 3 \
52 -wrap word -yscrollcommand [list .input.sb set] -takefocus 1
53 bind .input.t <Key-Return> {send_message; break}
54 pack .input.t -side left -fill both -expand 1
55 scrollbar .input.sb -orient vertical -command [list .input.t yview]
56 pack .input.sb -side left -fill y
57
58 frame .msg
59 pack .msg -side top -fill both -expand 1
60 text .msg.t -bd 1 -relief sunken -bg white -fg black -width 60 -height 20 \
61 -wrap word -yscrollcommand [list .msg.sb set] -takefocus 0
62 bindtags .msg.t [list .msg.t . all]
63 .msg.t tag config error -foreground red
64 .msg.t tag config meta -foreground forestgreen
65 .msg.t tag config norm -foreground black
66 pack .msg.t -side left -fill both -expand 1
67 scrollbar .msg.sb -orient vertical -command [list .msg.t yview]
68 pack .msg.sb -side left -fill y
69
70 update
71
72 # Send periodic messages to keep the TCP/IP link up
73 #
74 proc keep_alive {} {
75 global TIMER SOCKET
76 catch {after cancel $TIMER}
77 set TIMER [after 300000 keep_alive]
78 catch {puts $SOCKET noop; flush $SOCKET}
79 }
80
81 # Connect to the server
82 proc connect {} {
83 global SOCKET tcl_platform
84 catch {close $SOCKET}
85 if {[catch {
86 if {$::PROXYHOST ne {}} {
87 set SOCKET [socket $::PROXYHOST $::PROXYPORT]
88 puts $SOCKET "CONNECT $::SERVERHOST:$::SERVERPORT HTTP/1.1"
89 puts $SOCKET "Host: $::SERVERHOST:$::SERVERPORT"
90 puts $SOCKET ""
91 } else {
92 set SOCKET [socket $::SERVERHOST $::SERVERPORT]
93 }
94 fconfigure $SOCKET -translation binary -blocking 0
95 puts $SOCKET [list login $tcl_platform(user) fact,fuzz]
96 flush $SOCKET
97 fileevent $SOCKET readable handle_input
98 keep_alive
99 } errmsg]} {
100 if {[tk_messageBox -icon error -type yesno -parent . -message \
101 "Unable to connect to server. $errmsg.\n\nTry again?"]=="yes"} {
102 after 100 connect
103 }
104 }
105 }
106 connect
107
108 # Send the message text contained in the .input.t widget to the server.
109 #
110 proc send_message {} {
111 set txt [.input.t get 1.0 end]
112 .input.t delete 1.0 end
113 regsub -all "\[ \t\n\f\r\]+" [string trim $txt] { } txt
114 if {$txt==""} return
115 global SOCKET
116 puts $SOCKET [list message $txt]
117 flush $SOCKET
118 }
119
120 .mb add cascade -label "Transfer" -underline 0 -menu .mb.files
121 menu .mb.files -tearoff 0
122 .mb.files add command -label "Send file..." -command send_file
123 .mb.files add command -label "Delete files" -command delete_files \
124 -state disabled
125 .mb.files add separator
126
127 # Encode a string (possibly containing binary and \000 characters) into
128 # single line of text.
129 #
130 proc encode {txt} {
131 return [string map [list % %25 + %2b " " + \n %0a \t %09 \000 %00] $txt]
132 }
133
134 # Undo the work of encode. Convert an encoded string back into its original
135 # form.
136 #
137 proc decode {txt} {
138 return [string map [list %00 \000 %09 \t %0a \n + " " %2b + %25 %] $txt]
139 }
140
141 # Delete all of the downloaded files we are currently holding.
142 #
143 proc delete_files {} {
144 global FILES
145 .mb.files delete 3 end
146 array unset FILES
147 .mb.files entryconfigure 1 -state disabled
148 }
149
150 # Prompt the user to select a file from the disk. Then send that
151 # file to all chat participants.
152 #
153 proc send_file {} {
154 global SOCKET
155 set openfile [tk_getOpenFile]
156 if {$openfile==""} return
157 set f [open $openfile]
158 fconfigure $f -translation binary
159 set data [read $f]
160 close $f
161 puts $SOCKET [list file [file tail $openfile] [encode $data]]
162 flush $SOCKET
163 set time [clock format [clock seconds] -format {%H:%M} -gmt 1]
164 .msg.t insert end "\[$time\] sent file [file tail $openfile]\
165 - [string length $data] bytes\n" meta
166 .msg.t see end
167 }
168
169 # Save the named file to the disk.
170 #
171 proc save_file {filename} {
172 global FILES
173 set savefile [tk_getSaveFile -initialfile $filename]
174 if {$savefile==""} return
175 set f [open $savefile w]
176 fconfigure $f -translation binary
177 puts -nonewline $f [decode $FILES($filename)]
178 close $f
179 }
180
181 # Handle a "file" message from the chat server.
182 #
183 proc handle_file {from filename data} {
184 global FILES
185 foreach prior [array names FILES] {
186 if {$filename==$prior} break
187 }
188 if {![info exists prior] || $filename!=$prior} {
189 .mb.files add command -label "Save \"$filename\"" \
190 -command [list save_file $filename]
191 }
192 set FILES($filename) $data
193 .mb.files entryconfigure 1 -state active
194 set time [clock format [clock seconds] -format {%H:%M} -gmt 1]
195 .msg.t insert end "\[$time $from\] " meta "File: \"$filename\"\n" norm
196 .msg.t see end
197 }
198
199 # Handle input from the server
200 #
201 proc handle_input {} {
202 global SOCKET
203 if {[eof $SOCKET]} {
204 disconnect
205 return
206 }
207 set line [gets $SOCKET]
208 if {$line==""} return
209 set cmd [lindex $line 0]
210 if {$cmd=="userlist"} {
211 set ulist {}
212 foreach u [lrange $line 1 end] {
213 append ulist $u\n
214 }
215 .who.list config -text [string trim $ulist]
216 } elseif {$cmd=="message"} {
217 set time [clock format [clock seconds] -format {%H:%M} -gmt 1]
218 set from [lindex $line 1]
219 .msg.t insert end "\[$time $from\] " meta [lindex $line 2]\n norm
220 .msg.t see end
221 bell
222 wm deiconify .
223 update
224 raise .
225 } elseif {$cmd=="noop"} {
226 # do nothing
227 } elseif {$cmd=="meta"} {
228 set now [clock seconds]
229 set time [clock format $now -format {%H:%M} -gmt 1]
230 .msg.t insert end "\[$time\] [lindex $line 1]\n" meta
231 .msg.t see end
232 } elseif {$cmd=="file"} {
233 if {[info commands handle_file]=="handle_file"} {
234 handle_file [lindex $line 1] [lindex $line 2] [lindex $line 3]
235 }
236 }
237 }
238
239 # Handle a broken socket connection
240 #
241 proc disconnect {} {
242 global SOCKET
243 close $SOCKET
244 set q [tk_messageBox -icon error -type yesno -parent . -message \
245 "TCP/IP link lost. Try to reconnet?"]
246 if {$q=="yes"} {
247 connect
248 } else {
249 exit
250 }
251 }
252
253 # Remove all but the most recent 100 message from the message log
254 #
255 proc cleanup_record {} {
256 .msg.t delete 1.0 {end -100 lines}
257 }
--- a/tools/fossil_chat.tcl
+++ b/tools/fossil_chat.tcl
@@ -1,257 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button