wusenet

Check-in [b81b20ee38]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Initial checkin, git source control
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | descendants | master | trunk
Files: files | file ages | folders
SHA3-256:b81b20ee38829ada2d3482b7e0c0b7c4557c91bce329b427d896184e7acafb31
User & Date: ajv-899-334-8894@vsta.org 2015-04-06 17:17:24
Context
2015-04-07
00:54
Start coding up NNTP connection and initial web screens check-in: 9c312f1ad1 user: ajv-899-334-8894@vsta.org tags: master, trunk
2015-04-06
17:17
Initial checkin, git source control check-in: b81b20ee38 user: ajv-899-334-8894@vsta.org tags: master, trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace

Added .gitignore.





>
>
1
2
data
*.pyc

Added README.md.

























>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
Wusenet - WWW interface to Usenet

This is a standalone, embedded WWW server which provides
a web interface to view and post Usenet articles.  On the
backend, it interfaces to an NNTP server to pull articles
and push posted ones.

It is multi-threaded, so web clients do not interfere
with each other.  A separate, dedicated thread handles
pulling of new articles and pushing of posted articles.
All clients share the same article state, reducing load
on the NNTP server.

Added config.py.























































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#
# config.py
#	Handle loading of config file
#

# Accept input line, return line as lexed
#  into words
def lex(lnum, l):
    words = []
    curword = ""
    inquote = False
    while l:
	# Pull next char from input line
	c = l[0]
	l = l[1:]

	# Looking for beginning of next word
	if not curword:
	    # Whitespace between words
	    if c.isspace():
		continue

	    # Beginning of quoted string
	    if c == '"':
		inquote = True
	    else:
		curword += c

	# Quoted text
	elif inquote:
	    if c == '"':
		inquote = False
		words.append(curword)
		curword = ""
	    else:
		curword += c

	# End of current word
	elif c.isspace():
	    words.append(curword)
	    curword = ""

	# Another char in current word
	else:
	    curword += c

    # End of line without closing quote
    if inquote:
	raise Exception, "Unterminated quote at %d" % (lnum,)

    # Last word on line
    if curword:
	words.append(curword)

    return(tuple(words))

# Load config file into dict
def load_cfg(cfg_file):

    # Configuration, as a dict
    f = open(cfg_file, "r")
    res = { "notify": [], "interface": [] }
    curdict = None

    lnum = 0
    for l in f:
	lnum += 1

	# Note if it's an indented line (and thus annotation
	#  applying to the current element)
	indented = l[0].isspace()

	# Comments & blank lines
	l = l.strip()
	if not l:
	    continue
	if l.startswith("#"):
	    continue

	# Basic format is:
	#  keyword [args...]
	words = lex(lnum, l)
	op = words[0]
	words = words[1:]

	# Add annotation to this object
	if indented:
	    if curdict is None:
		raise Exception, \
		 "%s line %d: unexpected indented annotation" % \
		 (cfg_file, lnum)

	    # Single datum is present directly, otherwise record
	    #  the whole list
	    if len(words) == 1:
		val = words[0]
	    else:
		val = words

	    # Multiples possible
	    if op in ("head", "tail"):
		if op in curdict:
		    curdict[op].append(val)
		else:
		    curdict[op] = [val]

	    # Single config item
	    elif op in curdict:
		raise Exception, \
		 "%s line %d: %s already configured" % \
		 (cfg_file, lnum, op)
	    else:
		curdict[op] = val

	# New object
	else:
	    curdict = None

	    # These are single-argument config parameters which
	    #  can be supplied only once
	    if op in ("history", "nslot", "nstate", "nalert",
		    "org"):
		if len(words) != 1:
		    raise Exception, \
		     "%s line %d: trailing text in %s annotation" % \
		     (cfg_file, lnum, op)
		if op in res:
		    raise Exception, \
		     "%s line %d: %s already configured" % \
		     (cfg_file, lnum, op)

	    # These can be specified multiple times, and
	    #  accumulate under res[op] as (type, sub-config{})
	    if op in ("notify", "interface"):
		curdict = {}
		res[op].append( (words[0], curdict) )

	    elif op in ("org",):
		res[op] = words[0]
 
	    # Numeric arguments
	    elif op in ("history", "nslot", "nstate", "nalert"):
		if not words[0].isdigit():
		    raise Exception, \
		     "%s line %d: bad number %s" % \
		     (cfg_file, lnum, words[0])
		res[op] = int(words[0])

	    else:
		raise Exception, \
		 "%s line %d: unknown section type %s " % \
		 (cfg_file, lnum, op)

    # Here's our finished dict of config stuff
    return res

Added etc/accounts.













>
>
>
>
>
>
1
2
3
4
5
6
#
# Usenet access accounts
#

# Username	Password
vandys		xyzzy

Added etc/config.





















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
# Wusenet config
#
# This config file includes login information for the NNTP
#  server, so keep it private on multi-user systems.
#

# Our NNTP server
server news.eternal-september.org
user vandys
password tvecqkjrb

# Our web interface
port 8081

# Groups for which we maintain state
groups rec.games.pbm alt.ascii-art alt.sys.pdp10 alt.sys.pdp11
groups sci.military.moderated ba.mountain-folk comp.lang.hermes
groups comp.infosystems.gopher alt.sys.pdp8 sci.agriculture.poultry
groups ba.transportation comp.distributed rec.games.mud.lp
groups comp.lang.dylan rec.games.board.marketplace comp.os.qnx
groups comp.mail.uucp comp.arch alt.music.mods comp.os.rsts
groups alt.lang.teco comp.sys.cdc comp.sys.m6809 comp.sys.dec
groups alt.coffee alt.os.development comp.ai.nat-lang rec.games.board
groups alt.hackers alt.ascii-art.animation rec.games.empire
groups rec.humor.funny alt.os.multics alt.ham-radio.packet

Added get.py.































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
#
# get.py
#	Mixin to implement HTML GET operations
#
# Structure of paths in the server:
#  /
#	Main index, currently subscribed groups with articles
#	Button for config, kill at bottom
#  /config
#	Name, signature
#	List of groups with "subscribe" selectable
#	"Update" at bottom (also cancel)
#  /kill
#	Clickable delete, radio from/subject, then pattern to kill
#  /group.name
#	Fetches articles for group.name, NART oldest not yet read
#  /group.name/<index>
#	Reads article for group.name at NNTP server <index>
#	Button for reformat, telescope quoting, follow-up
#
# 
import os, copy

# Number of articles to display at one time
NART = 50

# These are file extensions which should have their bits served
#  directly, literally.
Litfiles = (".mp3", ".jpg", ".gif", ".js", ".wav", ".txt")

# These are HTML source files
HTMLfiles = (".htm", ".html")

# Factor out constructing the HTML5 element to play an MP3
def mp3player(path, id=None):

    # Optional ID for overall element
    if id:
	idstr = ' id="%s"' % (id,)
    else:
	idstr = ''

    # Player
    res = """<span%s>Play: <audio controls>
<source src="%s" type="audio/mpeg" />
<span>No player</span>
</audio></span>""" % (idstr, path)

    return res

class GET_Mixin(object):

    # Top level of TIS; current status & available states
    def send_top(self):
	tis = self.server
	buf = '<html><title>TIS Management - %s</title><body>\n' % \
	    (tis.config["org"],)
	buf += '<form action="." method="post"><table border=1>\n'
	for idx,state in enumerate(tis.states):
	    # Each State gets two table rows; the first lets us
	    #  choose the current state, the second gives us
	    #  our buttons to edit/clear/etc.
	    atcur = (idx == tis.curstate)

	    # First row, radio checkbox and title
	    buf += '<tr>\n'
	    buf += ' <td><input type="radio" name="stateset"'
	    buf += ' value=%d' % (idx,)
	    if atcur:
		buf += ' checked'
	    if (not atcur) and (not state.configured()):
		buf += ' disabled'
	    buf += '></td>\n'
	    buf += ' <td><h3>State#%d - %s</h3></td>' % \
		(idx, state.name or "(empty)")
	    buf += ' <td><i>alert level %d</i></td>\n' % \
		(state.get_alert(),)
	    buf += '</tr>\n'

	    # Second row, edit/clear (first column blank)
	    buf += '<tr>\n'
	    buf += ' <td> </td>\n'
	    buf += ' <td>\n  <a href="state%d/">edit</a>\n' % (idx,)
	    buf += ' <input type="submit"'
	    buf += '  name="clear%d" value="clear"' % (idx,)
	    if atcur:
		buf += ' disabled'
	    buf += '>\n'
	    buf += '</td></tr>\n'

	    # Separators
	    for x in xrange(2):
		buf += '<tr class="greenbar"><td colspan=3 ' \
		    'class="separator"></td></tr>\n'

	buf += '</table>\n'

	# Apply state change
	buf += '<input type="submit" name="state" ' \
	    'value="Change State"><br>\n'
	buf += 'Revert in <input type="number" step="0.01" ' \
	    'name="sexpires" value="4" size=3> hours\n'
	buf += 'Lights off in <input type="number" step="0.01" ' \
	    'name="lexpires" value="4" size=3> hours\n'

	buf += '</form></body></html>\n'
	buf = self.send_result(buf)
	return buf

    # Edit a given state
    def send_state(self, idx):
	tis = self.server
	state = tis.states[idx]
	statename = "state%d" % (idx,)
	buf = '<html><title>TIS Management - %s ' \
		'- Edit %s</title><body>\n' % \
	    (tis.config["org"],statename)
	buf += '<form action="/state%d" method="post">\n' % \
	    (idx,)
	buf += '<hr>\nOverall alert description:<br>\n'
	buf += '<input type="text" name="description" ' \
	    'value="%s" size=70>\n' % (state.name,)
	buf += '<input type="submit" name="set-description" ' \
	    'value="Set description">\n'
	buf += ' <a href="/state%d.text">View Overall</a>\n' % \
	    (idx,)
	buf += ' <a href="/">Go back</a><br>'
	buf += mp3player("/state%d.mp3?ver=%d" % (idx, state.version))
	buf += '<hr>\n'
	for sidx,slot in enumerate(state.slots):
	    isTop = (sidx == 0)
	    isBottom = (sidx == len(state.slots)-1)

	    # Whole-Slot operations
	    buf += '%d - ' % (sidx+1,)
	    buf += '<input type="image" name="up-%s" ' \
		'alt="Move up" src="/imgs/small_%s.jpg"%s>\n' % \
		(slot.uuid,
		 "white" if isTop else "up",
		 " disabled" if isTop else "")
	    buf += '<input type="image" name="down-%s" ' \
		'alt="Move down" src="/imgs/small_%s.jpg"%s>\n' % \
		(slot.uuid,
		 "white" if isBottom else "down",
		 " disabled" if isBottom else "")
	    buf += '<input type="submit" name="change-%s" ' \
		'value="clear">\n' % (slot.uuid,)
	    buf += '<a href="/state%d/edit%s">edit</a>' % \
		(idx, slot.uuid,)


	    # Slot alert/label/name
	    buf += '<i>Alert%d</i><br>' % (slot.alert,)
	    buf += '<input type="text" name="text-%s" size=70 ' \
		'value="%s" readonly>\n' % (slot.uuid, slot.title)

	    # If they have enough privilege, let them post Slot's
	    #  into the Library
	    if slot.configured() and (self.priv > 0):
		buf += '<a href="/lib?save=%d&state=%d">' \
		    'Save to Library</a>' % \
		    (sidx,state.idx)

	    # End of stuff for this Slot's line
	    buf += '<br><hr>\n'

	buf += '</form></body></html>\n'
	buf = self.send_result(buf)
	return buf

    # Edit a specific Slot's textual portions
    def edit_slot_text(self, editidx, sidx, slot):
	tis = self.server
	state = slot.state
	statename = "state%d" % (state.idx,)

	# We label with integer slot index, because that's what
	#  they saw on the screen.
	buf = '<html><title>TIS Management - %s ' \
		'- Edit text of %s slot %d</title><body>\n' % \
	    (tis.config["org"], statename, sidx+1)
	buf += '<form action="/edit%d/text" method="post">\n' % \
	    (editidx,)

	# Slot alert level & name
	buf += '<hr>\n'
	buf += '<select name="alert">\n'
	for x in xrange(tis.config["nalert"]):
	    buf += ' <option value="%d"%s>Alert %d</option>\n' % \
		(x, (' selected' if (x == slot.alert) else ''), x)
	buf += '</select>\n'
	buf += '<hr>Overall slot description:<b>\n'
	buf += '<input type="text" name="title" value="%s" size=80>\n' \
	    % (slot.title,)

	# Slot text
	buf += '<hr>Transcript of slot audio:<hr>\n'
	buf += '<textarea name="text" cols=80 rows=20>\n'
	for l in slot.text.split("\n"):
	    buf += '%s\n' % (l.strip(),)
	buf += '</textarea>\n'

	# Control buttons
	buf += '<hr>\n'
	buf += '<input type="submit" name="submit" ' \
	    'value="Save Text, Record Audio">\n'
	buf += '<a href="/state%d">Cancel</a>' % (state.idx,)
	buf += '  <a href="/lib?load=%d">Load from Library</a>' % \
	    (editidx,)

	# Audio file controls
	buf += '<br><hr>\n'
	buf += mp3player("/edit%d/msg.mp3?ver=%d" %
	    (editidx, slot.version))

	# There you go.
	buf += '</form></body></html>\n'
	buf = self.send_result(buf)

	return buf

    # Try to serve the named file
    def send_files(self, fn):
	# Hanky-panky?
	if ".." in fn:
	    self.send_error(404, "File not found")
	    return None

	# Make sure we know its MIME type
	isbin = True
	if fn.endswith(".js"):
	    mtyp = "application/javascript"
	    isbin = False
	elif fn.endswith(".jpg"):
	    mtyp = "image/jpeg"
	elif fn.endswith(".png"):
	    mtyp = "image/png"
	elif fn.endswith(".svg"):
	    mtyp = "image/svg+xml"
	elif fn.endswith(".gif"):
	    mtyp = "image/gif"
	elif fn.endswith(".wav"):
	    mtyp = "audio/x-wav"
	elif fn.endswith(".mp3"):
	    mtyp = "audio/mpeg"
	elif fn.endswith(".html"):
	    mtyp = "text/html"
	    isbin = False
	elif fn.endswith(".htm"):
	    mtyp = "text/html"
	    isbin = False
	elif fn.endswith(".txt"):
	    mtyp = "text/plain"
	    isbin = False
	else:
	    # Unknown file type
	    self.send_error(404, "File not found")
	    return None

	try:
	    f = open(fn, "r")
	except:
	    self.send_error(404, "File not found")
	    return None
	buf = f.read()
	f.close()
	buf = self.send_result(buf, mtyp, binary=isbin)
	return buf

    # Send a file within the Slot being edited
    # Typically, this would be the mp3 file
    def send_edit_item(self, editidx, slot, item):
	if item != "msg.mp3":
	    # Eventually we'll likely have access to MIME attachments
	    #  related to the slot.
	    # TBD.
	    self.send_error(404, "File not found")
	    return None

	# Common code to deliver files
	return self.send_files("data/edit/%d/%s" % (editidx, item))

    # Begin editing of a Slot
    #
    # We rotor around edit indices, and just make the
    #  assumption that the rotation is big enough to
    #  never collide with a previous edit.
    def clone_slot(self, sidx, slot):
	tis = self.server

	# Get next edit index
	editidx = tis.curedit
	tis.curedit += 1
	if tis.curedit >= len(tis.edits):
	    tis.curedit = 0

	# Clear storage
	os.system("rm -rf data/edit/%d" % (editidx,))

	# Clone Slot contents in filesystem
	os.system("cp -r data/state%d/slot%d data/edit/%d" %
	    (slot.state.idx, sidx, editidx))

	# Clone Slot state in memory (shallow copy)
	edit = tis.edits[editidx] = copy.copy(slot)

	# Send the browser to the edit screen
	return self.gen_redir("/edit%d/text" % (editidx,))

    # After updating the text, they get a chance to record new
    #  audio while looking at that text:
    # /editX/audio
    def edit_slot_audio(self, editidx, sidx, slot):

	# We label with integer slot index, because that's what
	#  they saw on the screen.
	tis = self.server
	state = slot.state
	statename = "state%d" % (state.idx+1,)
	buf = '<html><title>TIS Management - %s ' \
		'- Edit audio of %s slot %d</title><body>\n' % \
	    (tis.config["org"], statename, state.slotidx(slot)+1)
	buf += '<form action="/BadEdit" method="post">\n'

	# Slot name
	buf += '<hr>Overall slot description:<b>\n'
	buf += '<input type="text" name="title" ' \
		'value="%s" size=80 readonly>\n' \
	    % (slot.title,)

	# Slot text
	buf += '<hr>Transcript of slot audio:<hr>\n'
	buf += '<textarea name="text" cols=80 rows=20 readonly>\n'
	for l in slot.text.split("\n"):
	    buf += '%s\n' % (l.strip(),)
	buf += '</textarea>\n'

	# Audio recorder/player
	statepath = "/state%d" % (state.idx, )
	mp3path = "/edit%d/msg.mp3?ver=%d" % (editidx, slot.version)
	buf += \
"""
<canvas id="peaklev" width="1024" height="32"></canvas><br>
<div id="showStatus"></div>
<script src="/js/rec.js">Javascript disabled</script>
<button id="bdoRec" onclick="return doRec();">Record</button>
<button id="bdoStop" onclick="return doStop();" disabled>Stop</button>
"""
	buf += mp3player(mp3path, id="bdoPlay")
	buf += \
"""
<button id="bdoSave" onclick="return doSave();" disabled>Save</button>
"""

	# If we have an existing mp3 at hand, offer to just
	#  keep it.
	gotmp3 = os.access("data/edit/%d/msg.mp3" % (editidx,), os.R_OK)
	buf += '<button id="bdoKeep" ' \
	    'onclick="return doKeep();"%s>' \
	    'Keep old audio</button>\n' % \
	    (("" if gotmp3 else " disabled"),)

	# Or we could just forget it?
	buf += '<a href="%s">Cancel</a>\n' % (statepath,)

	# There you go.
	buf += '</form></body></html>\n'
	buf = self.send_result(buf)

	return buf

    # View of State's text, including head and tail content
    def view_state_text(self, idx):
	tis = self.server
	state = tis.states[idx]
	statename = "state%d" % (idx,)
	buf = '<html><title>TIS Management - %s ' \
		'- View %s</title><body>\n' % \
	    (tis.config["org"], statename)

	# State's description
	buf += 'Overall alert description: '
	buf += '<pre>%s</pre><hr>\n' % (state.name,)

	# Navigation
	buf += '<a href="/state%d">Go back</a><hr>\n' % (idx,)

	# Text of State, as rolled up
	try:
	    fd = open("data/state%d.text" % (idx,), "r")
	    buf += '<pre>'
	    for l in fd:
		buf += l
	    fd.close()
	    buf += '</pre>\n'
	except:
	    buf += "<i>No alert text found</i>\n"

	buf += '</body></html>\n'
	buf = self.send_result(buf)
	return buf

    # Get /lib Slot selection; this routine handles both
    #  choosing a Slot to copy into an /editX position, as
    #  well as saving a /stateX/slotY's contents into a
    #  library Slot.
    def get_lib(self):
	tis = self.server

	# Decode load/save operation
	slots = idx = op = None
	if "load" in self.vals:
	    # For loading /lib -> /edit, we just need the
	    #  index for which Edit Slot we're filling.
	    op = "load"
	    sidx = self.vals[op]
	    slots = tis.edits
	    tofrom = "Load from"
	    returl = "/edit%s/text" % (sidx,)
	    postargs = "load=%s" % (sidx,)

	elif "save" in self.vals:
	    op = "save"
	    sidx = self.vals[op]

	    # For saving, we also need to pull out the
	    #  /stateX from which we are copying
	    sstateidx = self.vals.get("state", "")
	    if not sstateidx.isdigit():
		self.send_error(404, "File not found")
		return None
	    stateidx = int(sstateidx)
	    if (stateidx < 0) or (stateidx >= len(tis.states)):
		self.send_error(404, "File not found")
		return None

	    # Now pass on the Slot's for this State, and
	    #  where we go when returning
	    slots = tis.states[stateidx].slots
	    tofrom = "Save to"
	    returl = "/state%d" % (stateidx,)
	    postargs = "save=%s&state=%d" % (sidx,stateidx)

	else:
	    op = None
	if op is None:
	    self.send_error(404, "File not found")
	    return None

	# Permission
	if (op == "save") and (self.priv < 1):
	    self.send_error(403, "Permission denied")
	    return None

	# Verify Slot index
	if not sidx.isdigit():
	    self.send_error(404, "File not found")
	    return None
	idx = int(sidx)
	if (idx < 0) or (idx >= len(slots)):
	    self.send_error(404, "File not found")
	    return None
	slot = slots[idx]
	if slot is None:
	    self.send_error(404, "File not found")
	    return None

	# Header, remind them why they're here
	buf = '<html><head><title>TIS Management - %s ' \
		'- Library</title><head><body>\n' % \
	    (tis.config["org"],)
	buf = '%s which Library slot:<br>\n' % (tofrom,)
	buf += '<form action="/lib?%s" method="post">\n' % \
	    (postargs,)

	# Library Slot's
	for slotidx,slot in enumerate(tis.lib.slots):
	    buf += '<input type="submit" name="use-%d" ' \
		'value="choose">' % (slotidx,)
	    buf += ' %2d - ' % (slotidx+1,)
	    buf += '<i>Alert%d</i>  ' % \
		(slot.alert,)
	    buf += '<input type="text" name="text-%d" size=60 ' \
		'value="%s" readonly><br>\n' % \
		(slotidx, slot.title or '(empty)')

	# Forget it
	buf += '<a href="%s">Go back</a>\n' % (returl,)

	buf += '</form></body></html>\n'
	buf = self.send_result(buf)
	return buf

    # Common code for GET/HEAD
    def handle_get(self):
	p = self.path
	print "TIS GET/HEAD", p

	# Options?
	if "?" in p:
	    idx = p.rindex("?")
	    if self.parseKV(p[idx+1:]) is None:
		return None
	    self.path = p = p[:idx]

	# Burst path
	pp = p.strip("/").split("/")

	# Top level
	if (not pp) or ((len(pp) == 1) and not pp[0]):
	    return self.send_top()

	# RESTful API
	if pp[0] == "rest":
	    return self.get_rest(pp[1:])

	# First path component is "state"?
	# /stateX [/slotY]
	tis = self.server
	if pp[0].startswith("state"):
	    # stateX
	    stid = pp[0][5:]

	    # /stateX.mp3?
	    isMP3 = stid.endswith(".mp3")
	    if isMP3:
		if len(pp) != 1:
		    # /stateX.mp3/more... wtf?
		    self.send_error(404, "File not found")
		    return None
		stid = stid[:-4]

	    # /stateX.text?
	    isTXT = stid.endswith(".text")
	    if isTXT:
		if len(pp) != 1:
		    # /stateX.text/more... wtf?
		    self.send_error(404, "File not found")
		    return None
		stid = stid[:-5]

	    # state<X>
	    if not stid.isdigit():
		self.send_error(404, "File not found")
		return None
	    idx = int(stid)
	    if (idx < 0) or (idx >= len(tis.states)):
		# But X is an illegal index
		self.send_error(404, "File not found")
		return None

	    # /stateX.mp3 -> send audio
	    if isMP3:
		return self.send_files("data/state%d.mp3" % (idx,))

	    # /stateX.text -> send rollup of State's text
	    if isTXT:
		return self.view_state_text(idx)

	    # /stateX
	    # Editing state itself
	    if len(pp) == 1:
		return self.send_state(idx)

	    # Editing Slot?
	    # /state/edit<slot-UUID>
	    state = tis.states[idx]
	    if pp[1].startswith("edit"):
		# Edit URL:
		#  edit<slot UUID>
		#
		# This kicks off editing of a Slot, cloning
		#  the content so all the editing can be completed
		#  before any of it is seen (i.e., atomicity).
		#
		sid = pp[1][4:]

		# Look up Slot UUID
		for sidx,slot in enumerate(state.slots):
		    if slot.uuid == sid:
			break
		else:
		    self.send_error(404, "File not found")
		    return None

		# Just looking at the Slot itself
		if len(pp) == 2:
		    # Kick off clone/edit of the Slot; implement TIS exclusion
		    #  so all of the filesystem ops are atomic.
		    with tis.exclusion:
			buf = self.clone_slot(sidx, slot)
		    return buf

	    # Malformed path, or trying to look at slot in
	    #  way not supported
	    self.send_error(404, "File not found")
	    return None

	# An /editX reference?
	# This is the cloned content of a Slot
	if pp[0].startswith("edit"):

	    # If all digits, we're working WRT an existing
	    #  edit.
	    sid = pp[0][4:]
	    if (not sid.isdigit()) or (len(pp) != 2):
		self.send_error(404, "File not found")
		return None

	    # Sanity check
	    editidx = int(sid)
	    if (editidx < 0) or (editidx >= len(tis.edits)):
		self.send_error(404, "File not found")
		return None

	    # Get tis.edits[], a copy of a Slot
	    slot = tis.edits[editidx]
	    if slot is None:
		self.send_error(404, "File not found")
		return None

	    # Dig up original Slot's index
	    state = slot.state
	    for sidx,_slot in enumerate(state.slots):
		if _slot.uuid == slot.uuid:
		    break
	    else:
		# Shouldn't happen
		self.send_error(404, "File not found")
		return None

	    # Referencing cloned Slot (most often this is
	    #  the redirect after cloning)
	    if pp[1] == "text":
		return self.edit_slot_text(editidx, sidx, slot)

	    # Cloned Slot's text edited, now going to record
	    #  audio.
	    if pp[1] == "audio":
		return self.edit_slot_audio(editidx, sidx, slot)

	    # Referencing cloned Slot item; usually playing the
	    #  mp3 audio, but others could be added...
	    if any(pp[1].endswith(suffix) for suffix in Litfiles):
		return self.send_edit_item(editidx, slot, pp[1])

	    # Malformed path
	    self.send_error(404, "File not found")
	    return None

	# /js, /imgs, /latest:
	# Subdirs with just literal content
	if pp[0] in ("js", "imgs", "latest"):
	    if any(pp[-1].endswith(suffix) for suffix in Litfiles):
		fname = os.path.join(*pp)
		return self.send_files(fname)

	# /html
	# Literal HTML source
	if pp[0] in ("html", ):
	    if any(pp[-1].endswith(suffix) for suffix in HTMLfiles):
		return self.send_files(os.path.join(*pp))

	# /lib; interface to Slot library
	if pp[0] == "lib":
	    return self.get_lib()

	# Bad path
	self.send_error(404, "File not found")
	return None

Added handler.py.













































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#
# handler.py
#	Dispatch request from a web browser
#
import base64
import get, post, rest
from BaseHTTPServer import BaseHTTPRequestHandler

# Return value for a digit (hex)
def digval(c):
    if c.isdigit():
	return ord(c) - ord('0')
    c = c.lower()
    if (c < 'a') or (c > 'f'):
	return None
    return (ord(c) - ord('a')) + 10

# Remove %XX hex char values from form input string
# I just cannot effin believe this isn't somewhere already...
def unescape(s):
    res = ""
    c1 = c2 = idx = None
    for c in s:
	if idx is None:
	    if c != '%':
		res += c
	    else:
		idx = 1
	    continue
	if idx == 1:
	    c1 = digval(c)
	    idx = 2
	    continue
	assert idx == 2
	idx = None
	if c1 is None:
	    res += "?"
	else:
	    c2 = digval(c)
	    if c2 is None:
		res += "?"
	    else:
		res += chr((c1 << 4) + c2)
	c1 = c2 = None
    return res

# Web interface into TIS operations
#
# What our web infrastructure considers our "server" instance
#  var state is a pointer to the global TIS data structure.
#
class TIS_Handler(BaseHTTPRequestHandler,
    get.GET_Mixin, post.POST_Mixin, rest.REST_Mixin):

    def send_error(self, code, msg):
	print "HTTP error", code, msg

    # HTTP GET operation
    def do_GET(self):
	buf = self.handle_get()
	if buf:
	    self.wfile.write(buf)

    # HTTP HEAD; header of a GET result, no body
    def do_HEAD(self):
	buf = self.handle_get()

    # A POST; dispatch our various form inputs
    def do_POST(self):
	content_len = int(self.headers.getheader('content-length', 0))
	dbuf = self.rfile.read(content_len)
	buf = self.handle_post(dbuf)
	if buf:
	    self.wfile.write(buf)

    # Send a 401 response, indicating authentication issues
    def send401(self, msg):
	self.send_response(401)
	self.send_header('WWW-Authenticate',
	    'Basic realm="Vashon Alert"')
	self.send_header('Content-type', 'text/html')
	self.end_headers()
	self.wfile.write(msg)
	return False

    # Intercept after request is parsed; apply HTTP authentication
    def parse_request(self):

	# Make sure it's well-formed
	res = BaseHTTPRequestHandler.parse_request(self)
	if not res:
	    return False

	# Apply digest authentication?
	auth = self.headers.getheader('Authorization')
	if auth is None:
	    return self.send401("Authentication required\r\n")

	# Do we accept it?
	auth = base64.decodestring(auth[6:])
	tup = auth.split(":")
	if len(tup) != 2:
	    return self.send401("Malformed authentication response\r\n")
	tis = self.server
	priv = tis.authenticate(tup[0], tup[1])
	if priv is None:
	    return self.send401("Incorrect username or password\r\n")
	self.priv = priv

	return True

    # Send header
    # Also canonicalize to DOS-style line endings (ew)
    def send_result(self, buf,
	    mtype="text/html", cacheable=False, binary=False):

	# Rewrite to DOS line endings unless it's binary
	#  (mp3 audio, etc.)
	if not binary:
	    buf = buf.replace("\n", "\r\n")

        self.send_response(200)
        self.send_header("Content-type", mtype)
        self.send_header("Content-Length", len(buf))
	if not cacheable:
	    self.send_header("Cache-Control", "NoCache")
	else:
	    self.send_header("Last-Modified",
		time.asctime(time.localtime(self.changed)))
        self.end_headers()

	return buf

    # Generate a meta REFRESH body, send it out
    #  to the client
    def gen_redir(self, url, msg=None):
	tis = self.server
	if msg is None:
	    timeout = 0
	    msg = "Refreshing..."
	else:
	    # If we have something to say, give'em 5 seconds
	    #  to admire its wisdom.
	    timeout = 5
        buf = \
"""
<html>
<head>
<title>TIS - Edit - %s</title>
<meta http-equiv="REFRESH"
content="%d;url=%s" />
</head>
<body>
%s
</body>
</html>
""" % (tis.config["org"], timeout, url, msg)

	buf = self.send_result(buf)
	return buf

    # In <url>[?key[=val][&key[=val...]]], parse key[/val] and
    #  put into self.vals{} and self.rvals{}
    # Returns the input @buf, or None if there was a problem
    def parseKV(self, buf):
	# Walk each key/val, in the format "k[=v]"
	# vals{} assembles vals[k] = v, and
	#  rvals{} assembles rvals[v] = k
	self.vals = vals = {}
	self.rvals = rvals = {}
	for kv in buf.split("&"):
	    # Split to k and v
	    tup2 = kv.split("=")
	    if len(tup2) == 1:
		k = tup2[0]
		v = True
	    elif len(tup2) == 2:
		k,v = tup2
	    else:
		# x=y=z, something like that?
		self.send_error(404, "File not found")
		return None

	    # Field name should be folded to lower case, as case
	    #  sensitivity varies by browser.
	    k = k.lower()
	    if isinstance(v, bool):
		vals[k] = v
	    else:
		# The show/artist/track can have spaces, which have
		#  turned into plus signs.
		v = v.replace("+", " ")
		vals[k] = unescape(v)
		rvals[v] = k

	# Success; @buf untouched
	return buf

Added http.py.































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#
# http.py
#	Web interface into TIS operations
#
import sys, socket, threading, struct
from handler import TIS_Handler
from server import interface

# An HTTP server, including methods to manipulate TIS state
class HTTP(interface.Interface):

    # Instantiate an Interface handler of TIS operations;
    #  WWW style
    def __init__(self, tis):
	interface.Interface.__init__(self, tis)
	self.rest = self.sock = None

    # Let our superclass handle config, then open the
    #  listen port here.  Priveleged ports require root,
    #  which will be dropped before regular service is
    #  started.
    def config(self, config):
	interface.Interface.config(self, config, ("rest",))

	# Start serving our WWW clients
	s = self.sock = socket.socket(socket.AF_INET,
	    socket.SOCK_STREAM)
	s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
	s.bind( ("", self.port or 80) )
	s.listen(20)

    # Run this operation from a web browser
    def launch(self, conn, tup):
	print "HTTP client", tup
	try:
	    handler = TIS_Handler(conn, tup, self.tis)
	finally:
	    conn.close()
	sys.exit(0)

    # Process web clients; each gets their own thread
    def run(self):
	s = self.sock
	while True:
	    conn,tup = s.accept()
	    t = threading.Thread(target=self.launch, args=(conn,tup))
	    t.start()

Added main.py.









































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#
# main.py
#	Main driver for WWW-Usenet interface
#
import sys, threading, time
import config
from www import http
from nntp import NNTP

# Root of all Wusenet server state
class Wusenet(object):

    def __init__(self, cfgname, datadir):

	# Load in configuration values; our configuration is
	#  simply key/val stored in a dict.
	c = self.config = config.load_cfg(cfgname)

	# Sanity
	if not c.get("server"):
	    raise Exception, "No NNTP server configured"
	if not c.get("user"):
	    raise Exception, "No NNTP username configured"
	if not c.get("password"):
	    raise Exception, "No NNTP password configured"
	if not c.get("groups"):
	    raise Exception, "No NNTP groups configured"
	if not c.get("port"):
	    raise Exception, "No HTTP server port configured"

	# Instantiate NNTP handling
	d = self.datadir = datadir

    # Actually start running
    def run(self):
	# Configure NNTP
	n = self.nntp = NNTP(c["server"],
	    c["user"], c["password"], d)
	for g in c["groups"]:
	    n.subscribe(g)

	# Dedicate a thread to NNTP
	t = threading.Thread(target=n.run)
	t.start()

	# Configure HTTP, then run it.  Our main thread
	#  thus ends up being the listener for HTTP
	#  clients.
	h = self.http = http.HTTP(self, c["port"])
	h.run()

    # Authenticate a user
    def authenticate(self, user, pw):
	try:
	    f = open("%s/password", "r")
	    fpw = f.readline()[:-1]
	    f.close()
	    return (pw == fpw)
	except:
	    return False

if __name__ == "__main__":
    if len(sys.argv) != 3:
	sys.stderr.write("Usage is: %s <cfg-file> <datadir>\n" %
	    (sys.argv[0],))
	sys.exit(1)
    t = Wusenet(sys.argv[1], sys.argv[2])
    t.run()

Added post.py.





























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
#
# post.py
#	HTML POST/PUT handling
#
# This is just a mixin to keep the www/handler.py code from
#  getting unwieldy.
#
import os, time

class POST_Mixin(object):

    # Write @buf's contents to the file named @fn
    def putfile(self, fn, buf):
	f = open(fn, "w")
	f.write(buf)
	f.close()

    # Common code to write content to a generic Slot
    # @prefix says where its storage in the filesystem is
    #  rooted.
    def set_slot_text(self, prefix, slot,
	    title, text, alert, mp3):
	tis = self.server

	with tis.exclusion:
	    self.putfile("%s/msg.title" % (prefix,),
		title)
	    self.putfile("%s/msg.text" % (prefix,),
		text)
	    self.putfile("%s/msg.alert" % (prefix,),
		str(alert))
	    slot.title = title
	    slot.text = text
	    slot.alert = alert
	    slot.version += 1
	    self.putfile("%s/version" % (prefix,),
		str(slot.version))
	    slot.touch()
	    self.putfile("%s/mtime" % (prefix,),
		str(slot.mtime))

	    # Try and copy the mp3

	    # Open the mp3?
	    fdin = None
	    try:
		if mp3 is None:
		    fdin = None
		else:
		    fdin = open(mp3, "r")
	    except:
		pass

	    # Have input, set up output
	    fdout = None
	    if fdin is not None:
		try:
		    fdout = open("%s/msg.mp3" % (prefix,), "w")
		except:
		    pass

	    if (fdin is not None) and (fdout is not None):
		while True:
		    buf = fdin.read(8192)
		    if not buf:
			break
		    fdout.write(buf)
	    else:
		# If can't copy over mp3, leave no audio at all
		# (Note we leave it alone if we weren't even told
		#  to try and touch it in the first place.)
		if mp3 is not None:
		    try:
			os.unlink("%s/msg.mp3" % (prefix,))
		    except:
			pass

	    # Cleanup
	    if fdin is not None:
		fdin.close()
	    if fdout is not None:
		fdout.close()

    # Write new Slot content into an Edit, including
    #  updating filesystem view
    def set_edit_text(self, edit, editidx, title, text, alert, mp3):
	self.set_slot_text("data/edit/%d" % (editidx,), edit,
	    title, text, alert, mp3)

    # Write text to a Library Slot
    def set_lib_text(self, slot, slotidx, title, text, alert, mp3):
	self.set_slot_text("data/lib/slot%d" % (slotidx,), slot,
	    title, text, alert, mp3)

    # Text portion of Slot has been edited, proceeding to audio
    def post_text(self, editidx):

	# Get textual portions
	vals = self.vals
	text = vals.get("text")
	title = vals.get("title")
	if (not text) or (not title):
	    self.send_error(404, "File not found")
	    return None

	# Normalize text
	title = title.strip()
	text = "".join([ (l.strip()+"\n") for l in text.split("\n") ])

	# Get alert setting
	tis = self.server
	alert = vals.get("alert")
	if (not alert) or (not alert.isdigit()):
	    alert = 0
	else:
	    alert = int(alert)
	if (alert < 0) or (alert >= tis.config["nalert"]):
	    alert = 0

	# Sync text to template dir, and register in
	#  our edit Slot
	self.set_edit_text(tis.edits[editidx], editidx,
	    title, text, alert, None)

	# With text updated, proceed to audio update
	return self.gen_redir("/edit%d/audio" % (editidx,))

    # Place this copy of a Slot into place as the new version
    # Note we're invoked under global TIS exclusion; all of our filesystem
    #  operations in a row will not be interleaved with another client's
    def commit(self, editidx):
	tis = self.server
	edit = tis.edits[editidx]
	state = edit.state
	slotidx = state.slotidx(edit)
	os.system("rm -rf data/state%d/slot%d" % (state.idx, slotidx))
	os.system("mv data/edit/%d data/state%d/slot%d" %
	    (editidx, state.idx, slotidx))
	state.slots[slotidx] = edit
	state.rollup()

    # Handle POST's into /editX
    def post_edit(self, pp, buf):
	if len(pp) != 2:
	    self.send_error(404, "File not found")
	    return None

	# Decode edit slot index
	tis = self.server
	s = pp[0][4:]
	if not s.isdigit():
	    self.send_error(404, "File not found")
	    return None
	editidx = int(s)
	if (editidx < 0) or (editidx > len(tis.edits)):
	    self.send_error(404, "File not found")
	    return None
	edit = tis.edits[editidx]
	if edit is None:
	    self.send_error(404, "File not found")
	    return None

	# /editX/{msg.wav, old.mp3}
	if pp[1] in ("msg.wav", "old.mp3"):

	    # The buffer handed to us is a wav file which we convert
	    if pp[1] == "msg.wav":

		# Write wav data
		wavname = "data/edit/%d/msg.wav" % (editidx,)
		f = open(wavname, "w")
		f.write(buf)
		f.close()

		# Convert to MP3, 16k mono with normalized audio levels
		mp3name = "data/edit/%d/msg.mp3" % (editidx,)
		tmpname =  "data/edit/%d/tmp_msg.mp3" % (editidx,)
		os.system("sox %s -c 1 -r 16k --norm %s" %
		    (wavname, tmpname))

		# Push into place
		os.system("mv %s %s" % (tmpname, mp3name))

	    # Otherwise, we already have a msg.mp3 and we're just
	    #  going to use it (usually, it's from the library)
	    else:
		pass

	    # New revision
	    edit.version += 1
	    self.putfile("data/edit/%d/version" % (editidx,), str(edit.version))
	    edit.mtime = time.time()
	    self.putfile("data/edit/%d/mtime" % (editidx,), str(edit.mtime))

	    # They've edited the text, recorded their audio--commit the
	    #  changes and back to the overall State.
	    with tis.exclusion:
		self.commit(editidx)

	    # We've used up this Edit, so don't try
	    #  to use it again.
	    tis.edits[editidx] = None

	    # The POST code is Javascript which will just read
	    #  back the URL to use next.
	    buf = "/state%d/" % (edit.state.idx,)
	    buf = self.send_result(buf)
	    return buf

	# Parse k[=v] part of our invocation
	buf = self.parseKV(buf)
	if buf is None:
	    return None

	# "submit" type form
	if pp[1] == "text":
	    return self.post_text(editidx)

	else:
	    # Unknown submit WRT /editX
	    self.send_error(404, "File not found")
	    return None

    # Exchange two Slot's under this State
    # This runs under TIS exclusion so we can swap both our
    #  own data structures as well as the actual filesystem
    #  storage.
    def swap_slots(self, state, idx1, idx2):
	sidx = state.idx

	# Shuffle the directories
	os.system("mv data/state%d/slot%d data/state%d/tmp_slot" %
	    (sidx, idx1, sidx))
	os.system("mv data/state%d/slot%d data/state%d/slot%d" %
	    (sidx, idx2, sidx, idx1))
	os.system("mv data/state%d/tmp_slot data/state%d/slot%d" %
	    (sidx, sidx, idx2))

	# Shuffle our local data structures
	s1 = state.slots[idx1]
	s2 = state.slots[idx2]
	state.slots[idx1] = s2
	state.slots[idx2] = s1

	# They're "changed" in the sense that the State using
	#  them needs to be updated
	s1.touch()
	s2.touch()

    # Handle POST's into /stateX
    def post_state(self, pp, buf):

	# /stateX, turn X to an integer & make sure
	#  it's an OK state index
	s = pp[0][5:]
	if not s.isdigit():
	    self.send_error(404, "File not found")
	    return None
	stateidx = int(s)
	tis = self.server
	if (stateidx < 0) or (stateidx >= len(tis.states)):
	    self.send_error(404, "File not found")
	    return None
	state = tis.states[stateidx]

	# A POST into the top page; form submission (usually
	#  Slot up/down motion)
	if len(pp) == 1:
	    buf = self.parseKV(buf)
	    if buf is None:
		self.send_error(404, "File not found")
		return None

	    # See what they're asking us to do
	    for k in self.vals.iterkeys():
		if k.startswith("up-"):
		    op = "up"
		    uuid = k[3:]
		    break
		if k.startswith("down-"):
		    op = "down"
		    uuid = k[5:]
		    break
		if k.startswith("change-"):
		    op = self.vals[k]
		    uuid = k[7:]
		    break

		# Just setting the State name
		if k == "description":
		    state.name = self.vals.get(k, "")
		    state.sync()
		    return self.gen_redir("/state%d" % (stateidx,))

	    else:
		# Unknown operation
		self.send_error(404, "File not found")
		return None

	    # An image button supplies the X/Y coord at which
	    #  the click occurred... we don't care.
	    uuid = uuid.split(".")[0]

	    # Find our Slot
	    for slotidx,slot in enumerate(state.slots):
		if slot.uuid == uuid:
		    break
	    else:
		# Unknown Slot UUID
		self.send_error(404, "File not found")
		return None

	    # Shuffle Slot up/down?
	    if op == "up":
		if slotidx > 0:
		    with tis.exclusion:
			self.swap_slots(state, slotidx-1, slotidx)
			state.rollup()
	    elif op == "down":
		if slotidx < len(state.slots)-1:
		    with tis.exclusion:
			self.swap_slots(state, slotidx, slotidx+1)
			state.rollup()

	    # "change"; usually "clear"
	    elif op == "clear":
		slot.clear(os.path.join("data",
		    "state%d" % stateidx,
		    "slot%d" % slotidx))
		with tis.exclusion:
		    state.rollup()

	    else:
		raise Exception, "Bad POST decode logic?"

	    return self.gen_redir("/state%d" % (stateidx,))

	# TBD, other state operations?
	self.send_error(404, "File not found")
	return None

    # Install a Library Slot's contents into the specified Edit
    def copy_lib_edit(self, slot, slotidx, edit, editidx):
	# Copy data structure state, including updating
	#  filesystem content.
	self.set_edit_text(edit, editidx,
	    slot.title, slot.text, slot.alert,
	    "data/lib/slot%d/msg.mp3" % (slotidx))
	return self.gen_redir("/edit%d/text" % (editidx,),
	    "Library content loaded")

    # Record State's Slot contents into the library
    def copy_slot_lib(self, state, slot, slotidx, libslot, libidx):
	self.set_lib_text(libslot, libidx,
	    slot.title, slot.text, slot.alert,
	    "data/state%d/slot%d/msg.mp3" % (state.idx, slotidx))

	# We've touched the library, so quite possibly our
	#  content needs to be honored by somebody's head/tail
	#  configuration.
	tis = self.server
	tis.version += 1
	for n in tuple(tis.notify):
	    n.update(tis.curstate, tis.curstate)

	# Back out to the state
	return self.gen_redir("/state%d" % (state.idx,),
	    "Slot contents saved to Library")

    # POST'ing WRT /lib contents
    def post_lib(self, pp, buf):

	# Decode parameters
	p1 = pp[-1]
	if "?" in p1:
	    # Some move k=v's, in the POST'ed URL
	    idx = p1.index("?")
	    pp[-1] = p1[:idx]
	    buf = p1[idx+1:] + "&" + buf
	buf = self.parseKV(buf)
	if buf is None:
	    self.send_error(404, "File not found")
	    return None

	# See what they want us to do
	tis = self.server
	op = libidx = editidx = stateidx = slotidx = None
	for k,v in self.vals.iteritems():
	    # Which Library slot to access?
	    if k.startswith("use-"):
		s = k[4:]
		if (not s.isdigit()) or (libidx is not None):
		    self.send_error(404, "File not found")
		    return None
		libidx = int(s)
		if (libidx < 0) or (libidx >= len(tis.lib.slots)):
		    self.send_error(404, "File not found")
		    return None
		continue

	    # /stateX
	    if k == "state":
		if (stateidx is not None) or (not v.isdigit()):
		    self.send_error(404, "File not found")
		    return None
		stateidx = int(v)
		if (stateidx < 0) or (stateidx >= len(tis.states)):
		    self.send_error(404, "File not found")
		    return None
		continue

	    # Save /stateX/slotY into /libZ
	    if k == "save":
		if (op is not None) or (not v.isdigit()):
		    self.send_error(404, "File not found")
		    return None
		slotidx = int(v)
		if (slotidx < 0) or (slotidx >= tis.config["nslot"]):
		    self.send_error(404, "File not found")
		    return None
		op = k
		continue

	    # Operation & edit index
	    if k == "load":
		if (op is not None) or (not v.isdigit()):
		    self.send_error(404, "File not found")
		    return None
		editidx = int(v)
		if (editidx < 0) or (editidx >= len(tis.edits)):
		    self.send_error(404, "File not found")
		    return None
		edit = tis.edits[editidx]
		if edit is None:
		    self.send_error(404, "File not found")
		    return None
		op = k
		continue

	# Mangled request
	if (op is None) or (libidx is None):
	    self.send_error(404, "File not found")
	    return None

	# This library Slot
	libslot = tis.lib.slots[libidx]

	# Save requires /stateX/slotY, and no /editZ
	if op == "save":
	    if (stateidx is None) or (slotidx is None) or \
		    (editidx is not None):
		self.send_error(404, "File not found")
		return None
	    state = tis.states[stateidx]
	    slot = state.slots[slotidx]

	    # Copy State's Slot into Library
	    if (not slot.title) or (not slot.text):
		self.send_error(404, "File not found")
		return None

	    # Should've been blocked at the Form level, but
	    #  hey, the user controls the browser...
	    if self.priv < 1:
		self.send_error(403, "Permission denied")
		return None

	    return self.copy_slot_lib(state, slot, slotidx,
		libslot, libidx)

	# Load Library Slot contents into an Edit Slot
	if op == "load":
	    if (not libslot.title) or (not libslot.text):
		self.send_error(404, "File not found")
		return None
	    return self.copy_lib_edit(libslot, libidx,
		edit, editidx)


	# Shouldn't happen; op was already decoded
	raise Exception, "Unhandled op?"
	self.send_error(404, "File not found")
	return None

    # POST onto root of server
    def post_root(self, buf):

	# Decode arguments
	buf = self.parseKV(buf)
	if buf is None:
	    self.send_error(404, "File not found")
	    return None
	tis = self.server
	op = stateidx = state = None
	for k,v in self.vals.iteritems():
	    # Clearing a State
	    if k.startswith("clear"):
		if op is not None:
		    self.send_error(404, "File not found")
		    return None
		op = "clear"
		s = k[5:]
		if not s.isdigit():
		    self.send_error(404, "File not found")
		    return None
		stateidx = int(s)
		if (stateidx < 0) or (stateidx >= len(tis.states)):
		    self.send_error(404, "File not found")
		    return None
		state = tis.states[stateidx]
		continue

	    # Setting current state
	    if k == "state":
		if (op is not None) or (v != "Change State"):
		    self.send_error(404, "File not found")
		    return None
		op = "newstate"
		s = self.vals.get("stateset", "")
		if not s.isdigit():
		    self.send_error(404, "File not found")
		    return None
		stateidx = int(s)
		if (stateidx < 0) or (stateidx >= len(tis.states)):
		    self.send_error(404, "File not found")
		    return None
		continue

	# Huh?
	if op is None:
	    self.send_error(404, "File not found")
	    return None

	# Dispatch operation

	# Erase a State in its entirety
	if op == "clear":
	    # Can't clear the one in use!
	    if stateidx == tis.curstate:
		self.send_error(409, "State in use")
		return None

	    # Wipe it out
	    state.clear()

	    # Ok
	    return self.gen_redir("/", "State cleared")

	# Move to new State position
	if op == "newstate":
	    if stateidx == tis.curstate:
		self.send_error(409, "State already selected")
		return None

	    # Get state and lamp timeouts
	    try:
		sexpires = float(self.vals.get("sexpires", "0"))
	    except:
		sexpires = 0.0
	    try:
		lexpires = float(self.vals.get("lexpires", "0"))
	    except:
		lexpires = 0.0

	    # Switch states
	    tis.set_state(stateidx, sexpires, lexpires)

	    # Back to top
	    return self.gen_redir("/",
		"State %d selected" % (stateidx,))

	raise Exception, "Unimplemented op?"

    # Entry from an HTML POST, body already read
    #  and in @buf
    def handle_post(self, buf):
	p = self.path
	print "TIS POST", p

	# Burst path
	pp = p.strip("/").split("/")

	# Operation on root--clearing a State, usually
	if (len(pp) == 1) and (not pp[0]):
	    return self.post_root(buf)

	# RESTful POST
	if pp[0] == "rest":
	    return self.post_rest(pp[1:], buf)

	# POST -> /editX/<what>
	if pp[0].startswith("edit"):
	    return self.post_edit(pp, buf)

	# POST -> /stateX
	# Moving Slot's up and down
	if pp[0].startswith("state"):
	    return self.post_state(pp, buf)

	# POST -> /lib?...
	if pp[0].startswith("lib"):
	    return self.post_lib(pp, buf)

	# Unknown submission type
	self.send_error(404, "File not found")
	return None