Newer
Older
#! /usr/bin/env python3
from collections import namedtuple
import json
import asyncio
import random
import uuid
Joseph Walton-Rivers
committed
import os
Joseph Walton-Rivers
committed
# FIXME: should support _FILE for secrets
JWT_SECRET = os.environ['ALLOC_JWT_SECRET']
STAFF_PASSWORD = os.environ['ALLOC_STAFF_PW']
GUEST = 0
STUDENT = 1
STAFF = 2
def allocateVariableGroups(students, idealTeamSize=4, minTeamSize=2):
"""Allocate students to groups, with an ideal team size.
If there are students left over, and there are at least minStudents they get
to be a team. Else, the 'remainder' students are distrubted across teams.
"""
gid = 0
currGroup = []
for student in students:
currGroup.append( student )
groups.append( currGroup )
currGroup = []
gid += 1
# last team
if currGroup:
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
if minTeamSize <= len(currGroup) or len(groups) == 0:
# viable team (or no other choice)
groups.append( currGroup )
else:
# redistribute across teams
groupAlloc = 0
while currGroup:
groups[groupAlloc].append( currGroup.pop() )
groupAlloc = (groupAlloc + 1) % len(groups)
return groups
def allocateFixedGroups(students, numGroups, minSize = 1):
"""Allocate students assuming a fixed number of groups.
Students are allocated fairly amoung groups, remainders
will be added to the first n groups. If there are not
enouph students for numGroups, then the maximum number
of groups to still satify numGroups is used.
"""
# figure out how many groups we can have
maxPossibleGroups = len(students) // minSize
numGroups = min( numGroups, maxPossibleGroups )
if numGroups == 0:
numGroups = 1 # we're having a bad day
groups = []
for i in range(0, numGroups):
groups.append( [] )
random.shuffle( students )
nextGroup = 0
while students:
groups[nextGroup].append( students.pop() )
nextGroup = (nextGroup + 1) % numGroups
ALLOCATIONS = {
'members': allocateVariableGroups,
'group': allocateFixedGroups
}
async def sendToStaff( topic, data ):
for staff in get_staff():
await clients[staff].send_topic( topic, data )
realName = message['data']['name']
realName = re.sub(r'[^\w -]', '', realName)
if 'password' in message['data']:
if message['data']['password'] == STAFF_PASSWORD:
client.role = STAFF
else:
client.role = GUEST
else:
client.role = STUDENT
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
async def forceAllocate(client, message):
if client.role != STAFF:
return
data = message["data"]
# Allow different group sizes
size = 4
if 'idealSize' in data:
size = int(data['idealSize'])
minSize = 2
if 'minSize' in data:
minSize = int(data['minSize'])
method = allocateFixedGroups
if 'method' in data and data['method'] in ALLOCATIONS:
method = ALLOCATIONS[ data['method'] ]
summary = {
'size': size,
'groups': []
}
# Perform group allocation
ids = [ x for x in clients.keys() if clients[x].role == STUDENT ]
groups = method( ids, size, minSize )
# send out group notifications
members = [ str( clients[x].name ) for x in group ]
data = {
'groupID': idx,
'members': members
}
summary['groups'].append( data )
for student in group:
await clients[ student ].send_topic( 'allocate', data )
async def resumeToken(client, message):
try:
ticket = jwt.decode( message['data'], JWT_SECRET, algorithms=["HS256"] )
resumed_token = uuid.UUID("{"+ticket['uuid']+"}")
del clients[ client.uuid ]
print( "client {} used jwt to reclaim id {}", client.uuid, resumed_token )
# resume session
client.uuid = resumed_token
clients[ client.uuid ] = client
if resumed_token in names:
session = names[ resumed_token ]
client.name = session[0]
client.role = session[1]
await client.send_topic('changeName', { 'name': client.name, 'role': client.role })
if client.role == STAFF:
students = [ {'uuid': str(x.uuid), 'name': x.name} for x in clients.values() if x.role == STUDENT ]
await client.send_topic('students', students)
except jwt.exceptions.DecodeError:
pass
async def forceRename(client, message):
if client.role != STAFF:
return
realName = message['data']['name']
realName = re.sub(r'[^\w -]', '', realName)
client_token = uuid.UUID("{"+message['data']['uuid']+"}")
await clients[ client_token ].rename( realName )
'token': resumeToken,
# staff options
}
class Client:
def __init__(self, socket):
self.uuid = uuid.uuid4()
self.name = None
async def rename(self, new_name):
self.name = new_name
names[ self.uuid ] = ( new_name, self.role )
await self.send_topic('changeName', {'name': new_name, 'role': self.role })
async def recv_topic(self, data):
print("[<<]", data)
msg_type = data['type']
if msg_type in protocol:
await protocol[ msg_type ]( self, data )
def get_students():
return [ x for x in clients.keys() if clients[x].role == STUDENT ]
def get_staff():
return [ x for x in clients.keys() if clients[x].role == STAFF ]
async def handleClient(websocket):
client = Client(websocket)
clients[ client.uuid ] = client
auth_token = jwt.encode( {'uuid': str(client.uuid)}, JWT_SECRET, algorithm="HS256" )
await client.send_topic("token", auth_token)
async for message in websocket:
data = json.loads( message )
msg_type = data['type']
if msg_type in protocol:
del clients[ client.uuid ]
async def main():
async with serve(handleClient, "0.0.0.0", 8081):