wusenet

Check-in [0974799a2a]
Login

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

Overview
Comment:Start shedding some of the old TIS code. Start coding up HTML GET support for main group list and article display. Code up first pass at organizing articles by threading.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | master | trunk
Files: files | file ages | folders
SHA3-256:0974799a2aa8df85eff621584adae3f98eca38775923cc0fda4b2b595faa3ba5
User & Date: ajv-899-334-8894@vsta.org 2015-04-08 21:05:13
Context
2015-04-12
20:27
Continue cooking up article loading/caching Leaf check-in: a208a798e8 user: ajv-899-334-8894@vsta.org tags: master, trunk
2015-04-08
21:05
Start shedding some of the old TIS code. Start coding up HTML GET support for main group list and article display. Code up first pass at organizing articles by threading. check-in: 0974799a2a user: ajv-899-334-8894@vsta.org tags: master, trunk
2015-04-07
00:57
Pre-github check-in: 526aa10093 user: ajv-899-334-8894@vsta.org tags: master, trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to get.py.

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
..
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
...
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
#	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
# 


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

class GET_Mixin(object):

    # "/"; groups list
    def send_top(self):
	server = self.server
	nntp = server.nntp

	buf = '<html><head><title>Your groups - %s</title></head><body>\n' % \
	    (self.user,)


	# Enumerate user's groups.  We'll get updates from NNTP
	#  as needed.
	user = server.users[self.user]
	for gname,cur in user.groups.iteritems():
	    group = nntp.group(gname)
	    if group is None:
		continue
	    if group.latest < cur:
		cur = 0
	    count = group.latest - cur
................................................................................
	# TBD, subscription management
	# buf += '<hr>'

	buf += '</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")
................................................................................
	    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:







>










>

<
>



<







 







|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







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
..
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
...
242
243
244
245
246
247
248
































































































































































































































249
250
251
252
253
254
255
#	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 pdb

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

class GET_Mixin(object):

    # "/"; groups list
    def send_top(self):
	server = self.server
	nntp = server.nntp
	user = self.user
	buf = '<html><head><title>Your groups - %s</title></head><body>\n' % \

	    (user.name,)

	# Enumerate user's groups.  We'll get updates from NNTP
	#  as needed.

	for gname,cur in user.groups.iteritems():
	    group = nntp.group(gname)
	    if group is None:
		continue
	    if group.latest < cur:
		cur = 0
	    count = group.latest - cur
................................................................................
	# TBD, subscription management
	# buf += '<hr>'

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

    # Collate articles for display
    # Returns [ (artidx, indent), ... ]
    # The articles can be presented in the supplied order;
    #  when an article follows up another, it follows that
    #  article and is indented under it.
    # Out input is the dict {artidx: {headers}, ...}
    # We use message-id and references to order them.
    def collate(self, arts):
	# Map msgid to artidx
	idmap = {}
	#  ...and then map artid to msgid
	artmap = {}
	malformed = []
	for artidx,headers in arts.iteritems():
	    # Message-ID
	    msgid = headers.get("message-id")
	    if msgid is None:
		malformed.append(artidx)
		continue
	    idmap[msgid] = artidx
	    artmap[artidx] = msgid

	# Malformed will just get tacked on the end
	for artidx in malformed:
	    del arts[artidx]

	# Now map out references, skipping ones which don't
	#  matter (they don't reference an article we're doing
	#  to display, so their subordination is irrelevant)
	depmap = {}
	for artidx,headers in arts.iteritems():
	    # References
	    refs = headers.get("references")
	    if not refs:
		continue
	    refs = set(ref for ref in refs.split() if (ref in idmap))
	    if refs:
		depmap[msgid] = refs

	# If anything comes out circular, just break one
	#  of the links
	circs = []
	for msgid,deps in depmap.iteritems():
	    for dep in deps:
		if dep not in depmap:
		    continue
		deps2 = depmap.get(dep)
		if msgid in deps2:
		    circs.append( (dep, msgid) )
	for msgid,dep in circs:
	    depmap[msgid].remove(dep)

	# Order things by subject as a default presentation
	#  order.
	tbd = [ artidx for artidx in arts.iterkeys() ]
	tbd.sort(key=lambda artidx:
	    arts[artidx].get("subject", "ZZZZ"))

	# Keep bringing in results until we've incorporated
	#  all the articles.
	res = []
	rooted = set()
	while tbd:
	    # Scan each cand to see if it only references
	    #  things satisfied by articles already in the
	    #  result.
	    # Vacuously, root articles will satisfy this.  Thus,
	    #  an article with no References:, or References:
	    #  only to articles not present here, will be included.
	    for artidx in tuple(tbd):
		msgid = artmap[artidx]

		# No deps, so indent 0 and use subject
		if msgid not in depmap:
		    res.append( (artidx, 0) )
		    rooted.add(msgid)
		    tbd.remove(artidx)
		    continue

		# See if we only use things already in the
		#  rooted set.
		deps = depmap[msgid]
		if not all(dep in rooted for dep in deps):
		    continue

		# We are satisfied by things in rooted[]
		# Find the deepest nested one, and we'll insert
		#  beneath it.
		best = bestdepth = None
		for idx,(artidx2,depth2) in enumerate(res):
		    if artmap[artidx] in deps:
			if (bestdepth is None) or (depth2 > bestdepth):
			    best = idx
			    bestdepth = depth2
		assert best is not None
		res.insert(best+1, (artidx,bestdepth+1))
		rooted.add(msgid)
		tbd.remove(artidx)
	
	# All subordinate articles are inserted in a suitable
	#  place in res[]
	return tuple(res)

    # Send articles from specific group
    def send_group(self):

	# Which group?
	gname = self.path
	server = self.server
	user = self.user
	seenidx = user.groups.get(gname)
	if seenidx is None:
	    self.send_error(404, "File not found")
	g = server.groups.get(gname)
	if g is None:
	    self.send_error(404, "File not found")

	# Update the group
	g.poll()

	# Identify up to NART articles to read next
	nseen = 0
	arts = {}
	for artidx in g.newarts(seenidx):
	    if nseen >= NART:
		break
	    headers = g.headers(artidx)
	    if headers is not None:
		arts[artidx] = headers

	# Order their presentation
	ordered = self.collate(arts)

	# Now present them, threaded
	buf = '<html><head><title>%s - %d articles of %d</title>' % \
	    (len(arts), g.last-seenidx)
	buf += '<body>\n'
	for artidx,indent in ordered:
	    head = headers[artidx]
	    subj = head.get("subject", "(no subject)")
	    buf += '<a href="/%s/%d">%s%s</a><br>' % \
		(gname, artidx, "&nbsp;" * indent, subj)

	# Here's your article list
	buf += '</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")
................................................................................
	    self.send_error(404, "File not found")
	    return None
	buf = f.read()
	f.close()
	buf = self.send_result(buf, mtyp, binary=isbin)
	return buf

































































































































































































































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

	# Options?
	if "?" in p:

Changes to handler.py.

98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117

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

	# Record username of this connection
	self.user = tup[0]

	# Ok
	return True

    # Send header
    # Also canonicalize to DOS-style line endings (ew)
    def send_result(self, buf,







|
|



|







98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117

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

	# Record username of this connection
	self.user = user

	# Ok
	return True

    # Send header
    # Also canonicalize to DOS-style line endings (ew)
    def send_result(self, buf,

Changes to main.py.

42
43
44
45
46
47
48

49
50
51
52
53



54
55
56
57
58
59
60
..
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
	#  thus ends up being the listener for HTTP
	#  clients.  Threads will spin off as clients
	#  arrive.
	h = self.http = http.HTTP(self, c["port"])
	h.run()

    # Authenticate a user

    def authenticate(self, user, pw):

	# Already seen; use our in-memory copy
	if user in self.users:
	    return self.users[user].pass == pw




	# Scan our filesystem table
	f = open("etc/accounts", "r")
	for l in f:
	    l = l.strip()
	    if l.startswith("#"):
		continue
................................................................................
		continue

	    # Verify password against account
	    if l[1] != pw:
		break

	    # Good password; load in user
	    self.users[user] = user.User(user, pw)
	    f.close()
	    return True

	# Unknown user account
	f.close()
	return False

if __name__ == "__main__":
    t = Wusenet()
    t.run()







>




|
>
>
>







 







|

|



|




42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
..
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
	#  thus ends up being the listener for HTTP
	#  clients.  Threads will spin off as clients
	#  arrive.
	h = self.http = http.HTTP(self, c["port"])
	h.run()

    # Authenticate a user
    # Return User instance, or None
    def authenticate(self, user, pw):

	# Already seen; use our in-memory copy
	if user in self.users:
	    u = self.users[user]
	    if u.pass == pw:
		return u
	    return None

	# Scan our filesystem table
	f = open("etc/accounts", "r")
	for l in f:
	    l = l.strip()
	    if l.startswith("#"):
		continue
................................................................................
		continue

	    # Verify password against account
	    if l[1] != pw:
		break

	    # Good password; load in user
	    u = self.users[user] = user.User(user, pw)
	    f.close()
	    return u

	# Unknown user account
	f.close()
	return None

if __name__ == "__main__":
    t = Wusenet()
    t.run()

Changes to user.py.

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Subscription and articles read are pushed back to the filesystem.
# State is also kept for which message ID's have been seen (so postings
#  to multiple groups are seen once), but that is only held until
#  server restart.
#
class User(object):
    def __init__(self, user, pass):
	self.user = user
	self.pass = pass
	self.seen = set()
	self.load_groups()
	# TBD: killfile, signature

    # Load record of groups subscribed to, along with
    #  which article seen in that group







|







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Subscription and articles read are pushed back to the filesystem.
# State is also kept for which message ID's have been seen (so postings
#  to multiple groups are seen once), but that is only held until
#  server restart.
#
class User(object):
    def __init__(self, user, pass):
	self.name = user
	self.pass = pass
	self.seen = set()
	self.load_groups()
	# TBD: killfile, signature

    # Load record of groups subscribed to, along with
    #  which article seen in that group