commit 9cd9873e207b5efbe021e5d413ef54da0ed5cc7a Author: Serge Bazanski Date: Sat Apr 13 17:09:11 2019 +0200 code dump from kkc19 diff --git a/client.py b/client.py new file mode 100644 index 0000000..aecb3b5 --- /dev/null +++ b/client.py @@ -0,0 +1,23 @@ +import asyncio +import socketio + + +sio = socketio.AsyncClient() + +@sio.on('state') +def sio_state(state): + print(state) + +@sio.on('identified') +def sio_identified(msg): + if msg.get('identified') != True: + print('COULD NOT IDENTIFY') + return + print('Identified.') + +async def main(): + cl = await sio.connect('http://127.0.0.1:5000') + await sio.emit('admin', {'password': 'changeme', 'set_role': {'who': '716a784c', 'what': 'HOST'}}) + await asyncio.sleep(2) + +asyncio.run(main()) diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..6c7a762 --- /dev/null +++ b/default.nix @@ -0,0 +1,20 @@ +with import {}; + + +stdenv.mkDerivation rec { + name = "venv"; + env = buildEnv { name = name; paths = buildInputs; }; + buildInputs = [ + python37 + ] ++ (with python37Packages; [ + virtualenv + pip + flask + redis + flask-socketio + eventlet + ecdsa + python-jose + aiohttp + ]); +} diff --git a/example-round.js b/example-round.js new file mode 100644 index 0000000..cf0738a --- /dev/null +++ b/example-round.js @@ -0,0 +1,55 @@ +{ + "type": "simple", + "categories": [ + { + "name": "foo", + "questions": [ + {"clue": "foo1", "answer": "foo1!"}, + {"clue": "foo2", "answer": "foo2!"}, + {"clue": "foo3", "answer": "foo3!"}, + {"clue": "foo4", "answer": "foo4!"}, + {"clue": "foo5", "answer": "foo5!"} + ] + }, + { + "name": "bar", + "questions": [ + {"clue": "bar1", "answer": "bar1!"}, + {"clue": "bar2", "answer": "bar2!"}, + {"clue": "bar3", "answer": "bar3!"}, + {"clue": "bar4", "answer": "bar4!"}, + {"clue": "bar5", "answer": "bar5!"} + ] + }, + { + "name": "baz", + "questions": [ + {"clue": "baz1", "answer": "baz1!"}, + {"clue": "baz2", "answer": "baz2!"}, + {"clue": "baz3", "answer": "baz3!"}, + {"clue": "baz4", "answer": "baz4!"}, + {"clue": "baz5", "answer": "baz5!"} + ] + }, + { + "name": "barfoo", + "questions": [ + {"clue": "barfoo1", "answer": "barfoo1!"}, + {"clue": "barfoo2", "answer": "barfoo2!"}, + {"clue": "barfoo3", "answer": "barfoo3!"}, + {"clue": "barfoo4", "answer": "barfoo4!"}, + {"clue": "barfoo5", "answer": "barfoo5!"} + ] + }, + { + "name": "barbaz", + "questions": [ + {"clue": "barbaz1", "answer": "barbaz1!"}, + {"clue": "barbaz2", "answer": "barbaz2!"}, + {"clue": "barbaz3", "answer": "barbaz3!"}, + {"clue": "barbaz4", "answer": "barbaz4!"}, + {"clue": "barbaz5", "answer": "barbaz5!"} + ] + } + ] +} diff --git a/jeopardy.py b/jeopardy.py new file mode 100644 index 0000000..85dfba1 --- /dev/null +++ b/jeopardy.py @@ -0,0 +1,468 @@ +import secrets +import os +import logging +import json +import ecdsa +import binascii + +from jose.jwk import ECKey +from flask import Flask, render_template, g, session +from flask_socketio import SocketIO, emit + + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'secret!' +app.config['ADMIN_PASSWORD'] = 'changeme' +socketio = SocketIO(app) +log = logging.getLogger() +log.setLevel(logging.DEBUG) +log.addHandler(logging.StreamHandler()) + +log.info("starting up...") + + +class Board: + def __init__(self, type): + self.type = type + self.categories = [] + + def add_category(self, c): + self.categories.append(c) + + def serialize(self): + res = {'categories': [c.serialize() for c in self.categories]} + return res + + def copy(self): + r = Board(self.type) + for c in self.categories: + r.add_category(c.copy()) + return r + + @classmethod + def unserialize(cls, j): + r = cls(j) + print('UNSERIALIZING ROUND', j) + + if 'categories' not in j: + log.error("No categories") + return + + for cid, c in enumerate(j['categories']): + if 'name' not in c: + log.error("No category name") + if 'questions' not in c or len(c['questions']) != 5: + log.error("Invalid question count") + + cat = Category(c['name']) + r.add_category(cat) + + questions = c['questions'] + for qid, q in enumerate(questions): + if 'clue' not in q: + log.error("No clue in question") + if 'answer' not in q: + log.error("No answer in question") + + value = (qid+1)*100 + ques = Question(value, q['clue'], q['answer'], c['name'], qid, cid) + cat.add_question(ques) + + return r + +class Category: + def __init__(self, name): + self.name = name + self.questions = [] + + def add_question(self, q): + self.questions.append(q) + + def serialize(self): + return {'name': self.name, 'questions': [q.serialize() for q in self.questions]} + + def copy(self): + c = Category(self.name) + for q in self.questions: + c.add_question(q.copy()) + return c + +class Question: + def __init__(self, value, clue, answer, category, qid, cid, answered=False): + self.value = value + self.clue = clue + self.answer = answer + self.category = category + self.answered = answered + self.qid = qid + self.cid = cid + + def serialize(self): + return { + 'value': self.value, + 'clue': self.clue, + 'answer': self.answer, + 'category': self.category, + 'answered': self.answered, + } + + def copy(self): + return Question(self.value, self.clue, self.answer, self.category, self.qid, self.cid, self.answered) + +class Compaction: + def __init__(self): + self.ct = 0 + self.board = None + self.roles = {} + self.identity = {} + self.contestants = {} + self.question = None + + @property + def buzzing(self): + for _, c in self.contestants.items(): + if c.buzzing: + return c.identity + return None + + def feed(self, entry): + log.info("Compacting {}".format(entry)) + if entry['event'] == 'new round': + self.board = entry['round'].copy() + log.info("LOG ENTRY, new round") + elif entry['event'] == 'role set': + who, what = entry['identity'], entry['what'] + self.roles[who] = what + if what == 'CONTESTANT' and who not in self.contestants: + self.contestants[who] = Contestant(who, who[:8]) + elif who in self.contestants: + del self.contestants[who] + log.info("LOG ENTRY, role set, {} is {}".format(who, what)) + elif entry['event'] == 'pretty set': + self.identity[entry['pretty']] = entry['identity'] + log.info("LOG ENTRY, pretty set, {} is {}".format(entry['pretty'], entry['identity'])) + elif entry['event'] == 'question': + cid = entry['cid'] + qid = entry['qid'] + log.info("LOG ENTRY, question activated, {}/{}".format(cid, qid)) + self.question = self.board.categories[cid].questions[qid].copy() + self.question.losers = set() + for _, c in self.contestants.items(): + c.buzzing = False + elif entry['event'] == 'buzz': + if not self.question: + self.ct += 1 + return + log.info("LOG ENTRY, buzz") + who = entry['identity'] + if not self.buzzing and who not in self.question.losers: + self.contestants[who].buzzing = True + elif entry['event'] == 'answered': + if not self.question: + self.ct += 1 + return + log.info("LOG ENTRY, answer") + ok = entry['ok'] + value = self.question.value + who = self.buzzing + if not who: + self.ct += 1 + return + self.contestants[who].buzzing = False + + if who in self.question.losers: + self.ct += 1 + return + if ok : + self.contestants[who].points += value + self.board.categories[self.question.cid].questions[self.question.qid].answered = True + self.question = None + self.ct += 1 + return + + self.contestants[who].points -= value + self.question.losers.add(who) + elif entry['event'] == 'give up': + if not self.question: + self.ct += 1 + return + self.board.categories[self.question.cid].questions[self.question.qid].answered = True + self.question = None + elif entry['event'] == 'set name': + who = entry['identity'] + name = entry['name'] + self.contestants[who].name = name + else: + log.error("Invalid log entry type {}".format(entry['event'])) + self.ct += 1 + + def serialize(self): + return { + 'board': self.board.serialize() if self.board else None, + 'roles': dict(self.roles), + 'identity': dict(self.identity), + 'contestants': [c.serialize() for (_, c) in self.contestants.items()], + 'question': self.question.serialize() if self.question else None, + 'losers': list(self.question.losers) if self.question else [], + } + + +class Contestant: + def __init__(self, identity, name): + self.identity = identity + self.name = name + self.points = 0 + self.buzzing = False + + def serialize(self): + return { + 'name': self.name, + 'points': self.points, + 'buzzing': self.buzzing, + 'identity': self.identity, + } + + +class Game: + def __init__(self): + self.state = 'IDLE' + self.log = [] + self._compaction = None + + def start_round(self, path): + with open(path) as f: + j = json.load(f) + r = Board.unserialize(j) + self.log.append({'event': 'new round', 'round': r}) + self.save() + + def activate_question(self, qid, cid): + if self.compacted.board.categories[cid].questions[qid].answered: + return + self.log.append({'event': 'question', 'qid': qid, 'cid': cid}) + self.save() + + def set_role(self, pretty, what): + self.log.append({'event': 'role set', 'identity': self.identity[pretty], 'what': what}) + self.save() + + def set_pretty(self, pretty, identity): + self.log.append({'event': 'pretty set', 'pretty': pretty, 'identity': identity}) + self.save() + + def answer(self, ok): + self.log.append({'event': 'answered', 'ok': ok}) + self.save() + + def give_up(self): + self.log.append({'event': 'give up'}) + self.save() + + def set_name(self, who, name): + self.log.append({'event': 'set name', 'identity': who, 'name': name}) + + @property + def roles(self): + return self.compacted.roles + + @property + def identity(self): + return self.compacted.identity + + @property + def compacted(self): + if self._compaction is None: + self._compaction = Compaction() + + if self._compaction.ct == len(self.log): + return self._compaction + + missing = self.log[self._compaction.ct:] + for m in missing: + self._compaction.feed(m) + + return self._compaction + + def save(self): + with open('jeopardy.dat.new', 'wb') as f: + for l in self.log: + if l['event'] == 'new round': + l = dict(l) + l['round'] = l['round'].serialize() + f.write(binascii.hexlify(json.dumps(l).encode())) + f.write(b'\n') + os.rename('jeopardy.dat.new', 'jeopardy.dat') + + def load(self): + if not os.path.exists('jeopardy.dat'): + log.info("New game...") + return + log.info("Loading game...") + with open('jeopardy.dat', 'r') as f: + for line in f: + line = line.strip() + entry = json.loads(binascii.unhexlify(line)) + print('RESTORING', entry) + if entry['event'] == 'new round': + entry['round'] = Board.unserialize(entry['round']) + self.log.append(entry) + + self._compaction = None + + def client_state(self, identity, admin): + role = self.roles.get(identity, 'SPECTATOR') + return { + 'role': role, + 'admin': admin, + 'state': self.state, + 'compacted': self.compacted_filter(self.compacted.serialize(), role, admin), + 'ct': self.compacted.ct, + } + + def compacted_filter(self, d, role, admin): + if admin: + return d + if role in ('SPECTATOR', 'CONTESTANT'): + del d['roles'] + del d['identity'] + if d['question']: + del d['question']['answer'] + # filter out answers + if d['board'] is not None: + for i, c in enumerate(d['board']['categories']): + for j, q in enumerate(c['questions']): + del d['board']['categories'][i]['questions'][j]['answer'] + + return d + + def buzz(self, identity): + if self.compacted.question is None: + return + for _, c in self.compacted.contestants.items(): + if c.buzzing: + return + self.log.append({'event': 'buzz', 'identity': identity}) + + +app.game = Game() +app.game.load() +#app.game.start_round('questions/final.js') +#app.game.save() + + +def emit_state(): + identity = session['identity'] + admin = session['admin'] + emit('state', app.game.client_state(identity, admin)) + +@app.route('/') +def view_index(): + return render_template("index.html") + + +@socketio.on('connect') +def sio_connect(): + log.info("Client connected") + session['identity'] = None + session['admin'] = False + session['nonce'] = secrets.token_hex(64) + emit_state() + emit('nonce', session['nonce']) + +@socketio.on('ping') +def sio_ping(): + emit_state() + +@socketio.on('proof') +def sio_proof(data): + log.info("Client attempting identity proof") + if data.get('nonce', '') != session['nonce']: + emit('identified', {'identified': False}) + return + + # ,___, + #jwk = data.get('jwk', {}) + #k = ECKey(jwk, 'ES384') + #proof = binascii.unhexlify(data.get('proof')) + #if not k.verify(session['nonce'], proof): + # emit('identified', {'identified': False}) + # return + + identity = data.get('identity') + session['identity'] = identity + app.game.set_pretty(identity[:8], identity) + emit('identified', {'identified': True}) + + emit_state() + +@socketio.on('admin') +def sio_admin(data): + log.info("Admin connecting...") + if data.get('password') != app.config['ADMIN_PASSWORD']: + emit('identified', {'identifier': True}) + return + + emit('identified', {'identified': True}) + session['admin'] = True + + if data.get('set_role') != None: + who = data['set_role'].get('who') + what = data['set_role'].get('what') + app.game.set_role(who, what) + + emit_state() + +@socketio.on('host-question') +def sio_host_question(data): + if app.game.roles[session['identity']] != 'HOST': + return + + cid = data.get('category') + qid = data.get('question') + if cid not in (0, 1, 2, 3, 4): + return + if qid not in (0, 1, 2, 3, 4): + return + app.game.activate_question(qid, cid) + +@socketio.on('host-set-role') +def sio_host_contestant(data): + if app.game.roles[session['identity']] != 'HOST': + return + if not data.get('who'): + log.info("Who?") + return + if data.get('what') not in ('SPECTATOR', 'CONTESTANT'): + log.info("What?") + return + app.game.set_role(data['who'], data['what']) + +@socketio.on('buzz') +def sio_buz(): + role = app.game.roles.get(session['identity']) + if role != 'CONTESTANT': + return + app.game.buzz(session['identity']) + +@socketio.on('answer') +def sio_answer(data): + if app.game.roles[session['identity']] != 'HOST': + return + + ok = data.get('ok', False) + app.game.answer(ok) + +@socketio.on('give up') +def sio_give_up(): + if app.game.roles[session['identity']] != 'HOST': + return + app.game.give_up() + +@socketio.on('set name') +def sio_set_name(msg): + if app.game.roles[session['identity']] != 'HOST': + return + app.game.set_name(msg['identity'], msg['name']) + +if __name__ == '__main__': + socketio.run(app, debug=True, host='0.0.0.0') diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..ee2af6c Binary files /dev/null and b/static/favicon.png differ diff --git a/static/main.css b/static/main.css new file mode 100644 index 0000000..91be12f --- /dev/null +++ b/static/main.css @@ -0,0 +1,189 @@ +body { + background-color: black; + color: white; + font-size: 100%; + font-family: sans-serif; + overflow: hidden; +} + +div#error { + position: absolute; + top: 50%; + left: 50%; + width: 40rem; + margin-left: -20rem; + margin-top: -1.5rem; + background-color: red; + font-size: 1.5rem; + text-align: center; + z-index: 100; + padding: 0.8rem; + border-radius: 1rem; + + display: none; +} + +div#status { + height: 1.2rem; + font-size: 0.7rem; + position: absolute; + top: 100%; + left: 0; + margin-top: -1.2rem; + width: 100%; + z-index: 50; + text-align: right; +} + +div#status p { + padding: 0; + padding-right: 0.8rem; + margin: 0; + display: inline; +} + +div#board { + display: flex; + flex-direction: row; + justify-content: center; +} + +div#board div.category { + margin: 0.25rem; +} + +div#board div.panel, div#board div.header { + background-color: #0d0b70; + width: 15rem; + height: 4rem; + margin-bottom: 0.5rem; + text-align: center; + border-radius: 0.25rem; + + display: flex; + flex-direction: column; + justify-content: center; +} + +div#board div.header { + font-size: 1.5rem; + margin-bottom: 1.5rem; + height: 8rem; +} + +div#board div.panel { + font-size: 2rem; +} + +div#board div.solved { + background-color: #000010; + color: #202020; +} + +div#board div.narrow { + width: 10rem; + height: 5rem; +} + +div#host { + width: 30rem; + background-color: red; + padding: 0.5rem; + margin-left: 1rem; +} + +div#contestants { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; +} + +div.contestant { + width: 18rem; + height: 8rem; + background-color: #1d1b90; + border-radius: 0.5rem; + margin-left: 1.5rem; + margin-right: 1.5rem; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; +} + +div.contestant h1 { + margin: 0; + padding: 0; +} + +div.contestant h2 { + margin: 0; + padding: 0; +} + +div.contestant h3 { + font-size: 1rem; + margin: 0; + padding: 0; +} + +div.buzz { + border: 4px solid red; +} + +div#question { + position: absolute; + top: 50%; + left: 50%; + width: 80rem; + height: 40rem; + margin-left: -40rem; + margin-top: -20rem; + background-color: #1d1b90; + border: 4px solid black; + font-size: 1.5rem; + text-align: center; + z-index: 90; + border-radius: 1rem; + display: none; + + flex-direction: column; + justify-content: space-between; +} + +div#question div.header { + font-size: 3rem; + margin-top: 1rem; +} + +div#question div.inner { +} + +div#question div.inner img { + width: auto; + height: 30rem; +} + +div.buzz { + background-color: #901d1b; + height: 5rem; +} + +div.choice { + display: flex; + flex-direction: row; + justify-content: center; +} + +div.ack { + background-color: green; + width: 10rem; + height: 2em; +} + +div.nack { + background-color: red; + width: 10rem; + height: 2em; +} diff --git a/static/main.js b/static/main.js new file mode 100644 index 0000000..f2711a1 --- /dev/null +++ b/static/main.js @@ -0,0 +1,442 @@ +"use strict"; + +let Identity = function() { + this.key = null; + this.pretty = null; +}; + +Identity.prototype.start = async function() { + const lskey = "hacker-jeopardy-key-6"; + const algo = { + name: "ECDSA", + namedCurve: "P-384", + }; + const uses = ["sign", "verify"]; + + let existing = localStorage.getItem(lskey); + if (existing == null) { + console.log("Generating identity..."); + const pair = await crypto.subtle.generateKey(algo, true, uses); + this.pubkey = pair.publicKey; + this.privkey = pair.privateKey; + this.identity = (await this.hexsign("hacker-jeopardy-whoami")); + const ls = { + "pubkey": await crypto.subtle.exportKey("jwk", this.pubkey), + "privkey": await crypto.subtle.exportKey("jwk", this.privkey), + "identity": this.identity, + }; + this.jwk_pubkey = ls.pubkey; + localStorage.setItem(lskey, JSON.stringify(ls)); + } else { + console.log("Loading identity..."); + const ls = JSON.parse(existing); + this.privkey = await crypto.subtle.importKey("jwk", ls.privkey, algo, false, ["sign"]); + this.pubkey = await crypto.subtle.importKey("jwk", ls.pubkey, algo, false, ["verify"]); + this.jwk_pubkey = ls.pubkey; + this.identity = ls.identity; + } + + this.pretty = this.identity.substr(0, 8); + console.log("Pretty identity: %s", this.pretty); +}; + +Identity.prototype.hexsign = async function(nonce) { + const ugh = (new TextEncoder()).encode(nonce); + const ugnh = await crypto.subtle.sign({ + name: "ECDSA", + hash: {name: "SHA-384"}, + }, this.privkey, ugh); + const ungh = Array.from(new Uint8Array(ugnh)); + const argh = ungh.map((b) => { return b.toString(16).padStart(2, "0"); }).join(""); + console.log("signed encoded", argh); + return argh; +}; + +const ClientState = Object.freeze({ + CONNECTING: Symbol("CONNECTING"), + SYNCING: Symbol("SYNCING"), + SYNCED: Symbol("SYNCED"), + BACKOFF: Symbol("BACKOFF"), +}); + +let Game = function() { + this.clientState = ClientState.CONNECTING; + this.role = 'SPECTATOR'; + this.error = null; + this.interval = null; + this.serverState = null; + this.identified = false; + this.ct = null; + this.rct = null; +}; + +Game.prototype.run = async function() { + let self = this; + + this.identity = new Identity(); + await this.identity.start(); + + console.log("Opening socket..."); + this.socket = io.connect('https://jeopardy.rhw.q3k.org'); + this.socket.on('connect', function() { self.socketConnected(); }); + this.socket.on('disconnect', function() { self.socketDisconnected(); }); + this.socket.on('state', function(msg) { self.socketState(msg); }); + this.socket.on('nonce', function(nonce) { self.socketNonce(nonce); }); + this.socket.on('identified', function(msg) { self.socketIdentified(msg); }); + + if (this.timeout == null) { + this.interval = setInterval(function() { self.tick(); }, 1000); + } + + this.lastStateUpdate = null; + this.serverState = null; +}; + +Game.prototype.mutate = function(newState) { + if (this.clientState != newState) { + console.log("Client %s -> %s", this.clientState, newState); + } + this.clientState = newState; + this.render(); +}; + +Game.prototype.fatal = function(msg) { + console.error("FATAL in %s: %s", this.clientState, msg); + this.error = msg; + this.mutate(ClientState.BACKOFF); + //setTimeout(() => this.backoffDone(), 3000); +}; + +Game.prototype.backoffDone = function() { + this.socket.close(); +} + +Game.prototype.socketConnected = function() { + console.log("Socket connected"); + if (this.clientState != ClientState.CONNECTING) { + this.fatal("Unexpected 'connect'"); + } + this.error = null; + this.mutate(ClientState.SYNCING); +}; + +Game.prototype.socketDisconnected = function() { + console.log("Socket disconnected"); + this.identified = false; + this.mutate(ClientState.CONNECTING); + this.socket.open(); + this.error = "Disconnected"; +}; + +Game.prototype.socketState = function(msg) { + this.serverState = msg.compacted; + this.ct = msg.ct; + this.role = msg.role; + this.lastStateUpdate = Date.now(); + this.mutate(ClientState.SYNCED); + + if (this.role == 'HOST') { + for (let [pretty, identity] of Object.entries(msg.compacted.identity)) { + let role = msg.compacted.roles[identity]; + if (role == undefined || role == null) { + continue; + } + } + } +}; + +Game.prototype.socketNonce = async function(nonce) { + console.log("Received nonce, sending identity proof."); + const proof = await this.identity.hexsign(nonce); + this.socket.emit('proof', {'identity': this.identity.identity, 'nonce': nonce, 'jwk': this.identity.jwk_pubkey, 'proof': proof}); +}; + +Game.prototype.socketIdentified = async function(msg) { + console.log("Identification result: ", msg); + if (msg.identified != true) { + this.fatal("Identification failed!") + return; + } + + this.identified = true; + this.render(); +}; + +Game.prototype.tick = function() { + let self = this; + this.socket.emit('ping') + + //this.render(); + + if (this.clientState == ClientState.CONNECTING) { + if (this.socket.disconnected) { + console.log("Reconnecting..."); + this.socket.open(); + } + } + + if (this.clientState != ClientState.SYNCED) { + return; + } +}; + +Game.prototype.render = function() { + if (this.rct == this.ct) { + return + } + this.rct = this.ct; + if (this.error != null) { + let err = document.querySelector("#error"); + err.style.display = "block"; + err.textContent = "Error: " + this.error; + } else { + let err = document.querySelector("#error"); + err.style.display = "none"; + } + + let status = document.querySelector("#status p"); + status.textContent = "State: " + this.clientState.description; + status.textContent += ", identity: " + this.identity.pretty; + status.textContent += ", role: " + this.role; + status.textContent += "."; + + this.renderGame(); +}; + +Game.prototype.renderGame = function() { + if (this.serverState == null) { + return; + } + + this.renderBoard(); + this.renderContestants(); + this.renderQuestion(); +}; + +Game.prototype.renderBoard = function() { + let host = (this.role == 'HOST'); + + let b = document.createElement("div"); + b.id = "board"; + + + if (this.serverState.board == null) { + document.querySelector("#board").replaceWith(b); + return; + } + + let desc = this.serverState.board; + + desc.categories.forEach((category, cid) => { + let c = document.createElement("div"); + c.className = "category"; + b.appendChild(c); + + let h = document.createElement("div"); + h.className = "header"; + if (host) { + h.className += " narrow"; + }; + h.textContent = category.name; + c.appendChild(h); + + category.questions.forEach((question, qid) => { + let p = document.createElement("div"); + p.className = "panel"; + if (question.answered) { + p.className += " solved"; + } + if (host) { + p.className += " narrow"; + }; + + if (this.role == "HOST") { + p.onclick = (() => { + this.socket.emit("host-question", {"category": cid, "question": qid}) + }); + } + + let t = document.createElement("div"); + t.className = "text"; + t.textContent = "" + question.value + " SOG"; + p.appendChild(t); + c.appendChild(p); + }); + }); + + if (host) { + let h = document.createElement("div"); + h.id = "host"; + h.textContent = "Host menu"; + b.appendChild(h); + + let u = document.createElement("ul"); + h.appendChild(u); + + for (let [pretty, identifier] of Object.entries(this.serverState.identity)) { + let role = this.serverState.roles[identifier]; + if (role == undefined) { + role = "SPECTATOR"; + } + let li = document.createElement("li"); + li.textContent = pretty + " " + role + " "; + u.appendChild(li); + let setRole = (who, what) => { + this.socket.emit('host-set-role', {'who': pretty, 'what': what}); + }; + if (role == "SPECTATOR") { + let a = document.createElement("a"); + a.textContent = "contestant"; + a.href = "#"; + a.onclick = () => { setRole(pretty, 'CONTESTANT'); }; + li.appendChild(a); + } + if (role == "CONTESTANT") { + let a = document.createElement("a"); + a.textContent = "spectator"; + a.href = "#"; + a.onclick = () => { setRole(pretty, 'SPECTATOR'); }; + li.appendChild(a); + + let b = document.createElement("a"); + b.textContent = "set name"; + b.href = "#"; + b.onclick = () => { this.socket.emit('set name', {'identity': identifier, 'name': prompt("name?")}); }; + li.appendChild(b); + } + } + } + + document.querySelector("#board").replaceWith(b); +} + +Game.prototype.renderContestants = function() { + let contestants = document.createElement("div"); + contestants.id = "contestants"; + this.serverState.contestants.forEach((c) => { + let name = c.name; + let cont = document.createElement("div"); + cont.className = "contestant"; + if (c.buzz) { + cont.className += " buzz"; + } + let h1 = document.createElement("h1"); + h1.textContent = name; + let h2 = document.createElement("h2"); + h2.textContent = c.points + " SOG"; + cont.appendChild(h1); + if (c.identity == this.identity.identity) { + let h3 = document.createElement("h3"); + h3.textContent = "(that's you!)"; + cont.appendChild(h3); + } + cont.appendChild(h2); + contestants.appendChild(cont); + }); + + document.querySelector("#contestants").replaceWith(contestants); +}; + +Game.prototype.renderQuestion = function() { + let question = document.createElement("div"); + question.id = "question"; + let q = this.serverState.question; + if (q == undefined || q == null) { + document.querySelector("#question").replaceWith(question); + return; + } + + let contestant = (this.role == "CONTESTANT"); + let host = (this.role == "HOST"); + let buzzed = false; + let buzzing = null; + this.serverState.contestants.forEach((c) => { + if (c.buzzing) { + buzzed = true; + buzzing = c; + } + }); + let loser = false; + this.serverState.losers.forEach((c) => { + if (c == this.identity.identity) { + loser = true; + }; + }); + + question.style = "display: flex;"; + let header = document.createElement("div"); + header.className = "header"; + header.textContent = q.category + " - " + q.value; + question.appendChild(header); + + let inner = document.createElement("div"); + inner.className = "inner"; + inner.innerHTML = q.clue; + question.appendChild(inner); + + if (!buzzed && !loser && contestant) { + let b = document.createElement("div"); + b.className = "buzz"; + b.textContent = "BUZZ!"; + b.onclick = () => { + this.socket.emit("buzz"); + }; + question.append(b); + } else if (buzzed && host) { + let choice = document.createElement("div"); + choice.className = "choice"; + choice.textContent = "buzzing: " + buzzing.name; + question.append(choice); + + let ack = document.createElement("div"); + ack.className = "ack"; + ack.textContent = "OK"; + ack.onclick = () => { + this.socket.emit("answer", {'ok': true}); + }; + + let nack = document.createElement("div"); + nack.className = "nack"; + nack.textContent = "WRONG"; + nack.onclick = () => { + this.socket.emit("answer", {'ok': false}); + }; + + + choice.appendChild(ack); + choice.appendChild(nack); + } else if (host) { + let choice = document.createElement("div"); + choice.className = "choice"; + choice.textContent = "losers: " + this.serverState.losers.map((e) => { return e.substr(0, 8); }).join(", ") + ", answer: " + this.serverState.question.answer; + question.append(choice); + + let giveup = document.createElement("div"); + giveup.className = "nack"; + giveup.textContent = "CANCEL QUESTION"; + giveup.onclick = () => { + this.socket.emit("give up"); + }; + choice.appendChild(giveup); + } else { + question.appendChild(document.createElement("div")); + }; + document.querySelector("#question").replaceWith(question); +}; + +window.onload = function() { + let nojs = document.querySelector("div.nojs"); + nojs.parentNode.removeChild(nojs); + + let run = async () => { + let g = new Game(); + await g.run(); + g.render(); + } + run().catch((e) => { + console.log(e); + console.log(e.stack); + console.log(e.message); + console.log(e.name); + }); +}; diff --git a/static/socket.io.min.js b/static/socket.io.min.js new file mode 100644 index 0000000..b622e1b --- /dev/null +++ b/static/socket.io.min.js @@ -0,0 +1,3 @@ +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;"undefined"!=typeof window?e=window:"undefined"!=typeof global?e=global:"undefined"!=typeof self&&(e=self),e.io=t()}}(function(){var t;return function e(t,n,r){function o(s,a){if(!n[s]){if(!t[s]){var c="function"==typeof require&&require;if(!a&&c)return c(s,!0);if(i)return i(s,!0);throw new Error("Cannot find module '"+s+"'")}var p=n[s]={exports:{}};t[s][0].call(p.exports,function(e){var n=t[s][1][e];return o(n?n:e)},p,p.exports,e,t,n,r)}return n[s].exports}for(var i="function"==typeof require&&require,s=0;s0&&!this.encoding){var t=this.packetBuffer.shift();this.packet(t)}},n.prototype.cleanup=function(){for(var t;t=this.subs.shift();)t.destroy();this.packetBuffer=[],this.encoding=!1,this.decoder.destroy()},n.prototype.close=n.prototype.disconnect=function(){this.skipReconnect=!0,this.backoff.reset(),this.readyState="closed",this.engine&&this.engine.close()},n.prototype.onclose=function(t){p("close"),this.cleanup(),this.backoff.reset(),this.readyState="closed",this.emit("close",t),this._reconnection&&!this.skipReconnect&&this.reconnect()},n.prototype.reconnect=function(){if(this.reconnecting||this.skipReconnect)return this;var t=this;if(this.backoff.attempts>=this._reconnectionAttempts)p("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var e=this.backoff.duration();p("will wait %dms before reconnect attempt",e),this.reconnecting=!0;var n=setTimeout(function(){t.skipReconnect||(p("attempting reconnect"),t.emitAll("reconnect_attempt",t.backoff.attempts),t.emitAll("reconnecting",t.backoff.attempts),t.skipReconnect||t.open(function(e){e?(p("reconnect attempt error"),t.reconnecting=!1,t.reconnect(),t.emitAll("reconnect_error",e.data)):(p("reconnect success"),t.onreconnect())}))},e);this.subs.push({destroy:function(){clearTimeout(n)}})}},n.prototype.onreconnect=function(){var t=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",t)}},{"./on":4,"./socket":5,"./url":6,backo2:7,"component-bind":8,"component-emitter":9,debug:10,"engine.io-client":11,indexof:42,"object-component":43,"socket.io-parser":46}],4:[function(t,e){function n(t,e,n){return t.on(e,n),{destroy:function(){t.removeListener(e,n)}}}e.exports=n},{}],5:[function(t,e,n){function r(t,e){this.io=t,this.nsp=e,this.json=this,this.ids=0,this.acks={},this.io.autoConnect&&this.open(),this.receiveBuffer=[],this.sendBuffer=[],this.connected=!1,this.disconnected=!0}var o=t("socket.io-parser"),i=t("component-emitter"),s=t("to-array"),a=t("./on"),c=t("component-bind"),p=t("debug")("socket.io-client:socket"),u=t("has-binary");e.exports=n=r;var f={connect:1,connect_error:1,connect_timeout:1,disconnect:1,error:1,reconnect:1,reconnect_attempt:1,reconnect_failed:1,reconnect_error:1,reconnecting:1},h=i.prototype.emit;i(r.prototype),r.prototype.subEvents=function(){if(!this.subs){var t=this.io;this.subs=[a(t,"open",c(this,"onopen")),a(t,"packet",c(this,"onpacket")),a(t,"close",c(this,"onclose"))]}},r.prototype.open=r.prototype.connect=function(){return this.connected?this:(this.subEvents(),this.io.open(),"open"==this.io.readyState&&this.onopen(),this)},r.prototype.send=function(){var t=s(arguments);return t.unshift("message"),this.emit.apply(this,t),this},r.prototype.emit=function(t){if(f.hasOwnProperty(t))return h.apply(this,arguments),this;var e=s(arguments),n=o.EVENT;u(e)&&(n=o.BINARY_EVENT);var r={type:n,data:e};return"function"==typeof e[e.length-1]&&(p("emitting packet with ack id %d",this.ids),this.acks[this.ids]=e.pop(),r.id=this.ids++),this.connected?this.packet(r):this.sendBuffer.push(r),this},r.prototype.packet=function(t){t.nsp=this.nsp,this.io.packet(t)},r.prototype.onopen=function(){p("transport is open - connecting"),"/"!=this.nsp&&this.packet({type:o.CONNECT})},r.prototype.onclose=function(t){p("close (%s)",t),this.connected=!1,this.disconnected=!0,delete this.id,this.emit("disconnect",t)},r.prototype.onpacket=function(t){if(t.nsp==this.nsp)switch(t.type){case o.CONNECT:this.onconnect();break;case o.EVENT:this.onevent(t);break;case o.BINARY_EVENT:this.onevent(t);break;case o.ACK:this.onack(t);break;case o.BINARY_ACK:this.onack(t);break;case o.DISCONNECT:this.ondisconnect();break;case o.ERROR:this.emit("error",t.data)}},r.prototype.onevent=function(t){var e=t.data||[];p("emitting event %j",e),null!=t.id&&(p("attaching ack callback to event"),e.push(this.ack(t.id))),this.connected?h.apply(this,e):this.receiveBuffer.push(e)},r.prototype.ack=function(t){var e=this,n=!1;return function(){if(!n){n=!0;var r=s(arguments);p("sending ack %j",r);var i=u(r)?o.BINARY_ACK:o.ACK;e.packet({type:i,id:t,data:r})}}},r.prototype.onack=function(t){p("calling ack %s with %j",t.id,t.data);var e=this.acks[t.id];e.apply(this,t.data),delete this.acks[t.id]},r.prototype.onconnect=function(){this.connected=!0,this.disconnected=!1,this.emit("connect"),this.emitBuffered()},r.prototype.emitBuffered=function(){var t;for(t=0;t0&&t.jitter<=1?t.jitter:0,this.attempts=0}e.exports=n,n.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},n.prototype.reset=function(){this.attempts=0},n.prototype.setMin=function(t){this.ms=t},n.prototype.setMax=function(t){this.max=t},n.prototype.setJitter=function(t){this.jitter=t}},{}],8:[function(t,e){var n=[].slice;e.exports=function(t,e){if("string"==typeof e&&(e=t[e]),"function"!=typeof e)throw new Error("bind() requires a function");var r=n.call(arguments,2);return function(){return e.apply(t,r.concat(n.call(arguments)))}}},{}],9:[function(t,e){function n(t){return t?r(t):void 0}function r(t){for(var e in n.prototype)t[e]=n.prototype[e];return t}e.exports=n,n.prototype.on=n.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks[t]=this._callbacks[t]||[]).push(e),this},n.prototype.once=function(t,e){function n(){r.off(t,n),e.apply(this,arguments)}var r=this;return this._callbacks=this._callbacks||{},n.fn=e,this.on(t,n),this},n.prototype.off=n.prototype.removeListener=n.prototype.removeAllListeners=n.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n=this._callbacks[t];if(!n)return this;if(1==arguments.length)return delete this._callbacks[t],this;for(var r,o=0;or;++r)n[r].apply(this,e)}return this},n.prototype.listeners=function(t){return this._callbacks=this._callbacks||{},this._callbacks[t]||[]},n.prototype.hasListeners=function(t){return!!this.listeners(t).length}},{}],10:[function(t,e){function n(t){return n.enabled(t)?function(e){e=r(e);var o=new Date,i=o-(n[t]||o);n[t]=o,e=t+" "+e+" +"+n.humanize(i),window.console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}:function(){}}function r(t){return t instanceof Error?t.stack||t.message:t}e.exports=n,n.names=[],n.skips=[],n.enable=function(t){try{localStorage.debug=t}catch(e){}for(var r=(t||"").split(/[\s,]+/),o=r.length,i=0;o>i;i++)t=r[i].replace("*",".*?"),"-"===t[0]?n.skips.push(new RegExp("^"+t.substr(1)+"$")):n.names.push(new RegExp("^"+t+"$"))},n.disable=function(){n.enable("")},n.humanize=function(t){var e=1e3,n=6e4,r=60*n;return t>=r?(t/r).toFixed(1)+"h":t>=n?(t/n).toFixed(1)+"m":t>=e?(t/e|0)+"s":t+"ms"},n.enabled=function(t){for(var e=0,r=n.skips.length;r>e;e++)if(n.skips[e].test(t))return!1;for(var e=0,r=n.names.length;r>e;e++)if(n.names[e].test(t))return!0;return!1};try{window.localStorage&&n.enable(localStorage.debug)}catch(o){}},{}],11:[function(t,e){e.exports=t("./lib/")},{"./lib/":12}],12:[function(t,e){e.exports=t("./socket"),e.exports.parser=t("engine.io-parser")},{"./socket":13,"engine.io-parser":25}],13:[function(t,e){(function(n){function r(t,e){if(!(this instanceof r))return new r(t,e);if(e=e||{},t&&"object"==typeof t&&(e=t,t=null),t&&(t=u(t),e.host=t.host,e.secure="https"==t.protocol||"wss"==t.protocol,e.port=t.port,t.query&&(e.query=t.query)),this.secure=null!=e.secure?e.secure:n.location&&"https:"==location.protocol,e.host){var o=e.host.split(":");e.hostname=o.shift(),o.length?e.port=o.pop():e.port||(e.port=this.secure?"443":"80")}this.agent=e.agent||!1,this.hostname=e.hostname||(n.location?location.hostname:"localhost"),this.port=e.port||(n.location&&location.port?location.port:this.secure?443:80),this.query=e.query||{},"string"==typeof this.query&&(this.query=h.decode(this.query)),this.upgrade=!1!==e.upgrade,this.path=(e.path||"/engine.io").replace(/\/$/,"")+"/",this.forceJSONP=!!e.forceJSONP,this.jsonp=!1!==e.jsonp,this.forceBase64=!!e.forceBase64,this.enablesXDR=!!e.enablesXDR,this.timestampParam=e.timestampParam||"t",this.timestampRequests=e.timestampRequests,this.transports=e.transports||["polling","websocket"],this.readyState="",this.writeBuffer=[],this.callbackBuffer=[],this.policyPort=e.policyPort||843,this.rememberUpgrade=e.rememberUpgrade||!1,this.binaryType=null,this.onlyBinaryUpgrades=e.onlyBinaryUpgrades,this.pfx=e.pfx||null,this.key=e.key||null,this.passphrase=e.passphrase||null,this.cert=e.cert||null,this.ca=e.ca||null,this.ciphers=e.ciphers||null,this.rejectUnauthorized=e.rejectUnauthorized||null,this.open()}function o(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}var i=t("./transports"),s=t("component-emitter"),a=t("debug")("engine.io-client:socket"),c=t("indexof"),p=t("engine.io-parser"),u=t("parseuri"),f=t("parsejson"),h=t("parseqs");e.exports=r,r.priorWebsocketSuccess=!1,s(r.prototype),r.protocol=p.protocol,r.Socket=r,r.Transport=t("./transport"),r.transports=t("./transports"),r.parser=t("engine.io-parser"),r.prototype.createTransport=function(t){a('creating transport "%s"',t);var e=o(this.query);e.EIO=p.protocol,e.transport=t,this.id&&(e.sid=this.id);var n=new i[t]({agent:this.agent,hostname:this.hostname,port:this.port,secure:this.secure,path:this.path,query:e,forceJSONP:this.forceJSONP,jsonp:this.jsonp,forceBase64:this.forceBase64,enablesXDR:this.enablesXDR,timestampRequests:this.timestampRequests,timestampParam:this.timestampParam,policyPort:this.policyPort,socket:this,pfx:this.pfx,key:this.key,passphrase:this.passphrase,cert:this.cert,ca:this.ca,ciphers:this.ciphers,rejectUnauthorized:this.rejectUnauthorized});return n},r.prototype.open=function(){var t;if(this.rememberUpgrade&&r.priorWebsocketSuccess&&-1!=this.transports.indexOf("websocket"))t="websocket";else{if(0==this.transports.length){var e=this;return void setTimeout(function(){e.emit("error","No transports available")},0)}t=this.transports[0]}this.readyState="opening";var t;try{t=this.createTransport(t)}catch(n){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t)},r.prototype.setTransport=function(t){a("setting transport %s",t.name);var e=this;this.transport&&(a("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=t,t.on("drain",function(){e.onDrain()}).on("packet",function(t){e.onPacket(t)}).on("error",function(t){e.onError(t)}).on("close",function(){e.onClose("transport close")})},r.prototype.probe=function(t){function e(){if(h.onlyBinaryUpgrades){var e=!this.supportsBinary&&h.transport.supportsBinary;f=f||e}f||(a('probe transport "%s" opened',t),u.send([{type:"ping",data:"probe"}]),u.once("packet",function(e){if(!f)if("pong"==e.type&&"probe"==e.data){if(a('probe transport "%s" pong',t),h.upgrading=!0,h.emit("upgrading",u),!u)return;r.priorWebsocketSuccess="websocket"==u.name,a('pausing current transport "%s"',h.transport.name),h.transport.pause(function(){f||"closed"!=h.readyState&&(a("changing transport and sending upgrade packet"),p(),h.setTransport(u),u.send([{type:"upgrade"}]),h.emit("upgrade",u),u=null,h.upgrading=!1,h.flush())})}else{a('probe transport "%s" failed',t);var n=new Error("probe error");n.transport=u.name,h.emit("upgradeError",n)}}))}function n(){f||(f=!0,p(),u.close(),u=null)}function o(e){var r=new Error("probe error: "+e);r.transport=u.name,n(),a('probe transport "%s" failed because of error: %s',t,e),h.emit("upgradeError",r)}function i(){o("transport closed")}function s(){o("socket closed")}function c(t){u&&t.name!=u.name&&(a('"%s" works - aborting "%s"',t.name,u.name),n())}function p(){u.removeListener("open",e),u.removeListener("error",o),u.removeListener("close",i),h.removeListener("close",s),h.removeListener("upgrading",c)}a('probing transport "%s"',t);var u=this.createTransport(t,{probe:1}),f=!1,h=this;r.priorWebsocketSuccess=!1,u.once("open",e),u.once("error",o),u.once("close",i),this.once("close",s),this.once("upgrading",c),u.open()},r.prototype.onOpen=function(){if(a("socket open"),this.readyState="open",r.priorWebsocketSuccess="websocket"==this.transport.name,this.emit("open"),this.flush(),"open"==this.readyState&&this.upgrade&&this.transport.pause){a("starting upgrade probes");for(var t=0,e=this.upgrades.length;e>t;t++)this.probe(this.upgrades[t])}},r.prototype.onPacket=function(t){if("opening"==this.readyState||"open"==this.readyState)switch(a('socket receive: type "%s", data "%s"',t.type,t.data),this.emit("packet",t),this.emit("heartbeat"),t.type){case"open":this.onHandshake(f(t.data));break;case"pong":this.setPing();break;case"error":var e=new Error("server error");e.code=t.data,this.emit("error",e);break;case"message":this.emit("data",t.data),this.emit("message",t.data)}else a('packet received with socket readyState "%s"',this.readyState)},r.prototype.onHandshake=function(t){this.emit("handshake",t),this.id=t.sid,this.transport.query.sid=t.sid,this.upgrades=this.filterUpgrades(t.upgrades),this.pingInterval=t.pingInterval,this.pingTimeout=t.pingTimeout,this.onOpen(),"closed"!=this.readyState&&(this.setPing(),this.removeListener("heartbeat",this.onHeartbeat),this.on("heartbeat",this.onHeartbeat))},r.prototype.onHeartbeat=function(t){clearTimeout(this.pingTimeoutTimer);var e=this;e.pingTimeoutTimer=setTimeout(function(){"closed"!=e.readyState&&e.onClose("ping timeout")},t||e.pingInterval+e.pingTimeout)},r.prototype.setPing=function(){var t=this;clearTimeout(t.pingIntervalTimer),t.pingIntervalTimer=setTimeout(function(){a("writing ping packet - expecting pong within %sms",t.pingTimeout),t.ping(),t.onHeartbeat(t.pingTimeout)},t.pingInterval)},r.prototype.ping=function(){this.sendPacket("ping")},r.prototype.onDrain=function(){for(var t=0;tn;n++)~c(this.transports,t[n])&&e.push(t[n]);return e}}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./transport":14,"./transports":15,"component-emitter":9,debug:22,"engine.io-parser":25,indexof:42,parsejson:34,parseqs:35,parseuri:36}],14:[function(t,e){function n(t){this.path=t.path,this.hostname=t.hostname,this.port=t.port,this.secure=t.secure,this.query=t.query,this.timestampParam=t.timestampParam,this.timestampRequests=t.timestampRequests,this.readyState="",this.agent=t.agent||!1,this.socket=t.socket,this.enablesXDR=t.enablesXDR,this.pfx=t.pfx,this.key=t.key,this.passphrase=t.passphrase,this.cert=t.cert,this.ca=t.ca,this.ciphers=t.ciphers,this.rejectUnauthorized=t.rejectUnauthorized}var r=t("engine.io-parser"),o=t("component-emitter");e.exports=n,o(n.prototype),n.timestamps=0,n.prototype.onError=function(t,e){var n=new Error(t);return n.type="TransportError",n.description=e,this.emit("error",n),this},n.prototype.open=function(){return("closed"==this.readyState||""==this.readyState)&&(this.readyState="opening",this.doOpen()),this},n.prototype.close=function(){return("opening"==this.readyState||"open"==this.readyState)&&(this.doClose(),this.onClose()),this},n.prototype.send=function(t){if("open"!=this.readyState)throw new Error("Transport not open");this.write(t)},n.prototype.onOpen=function(){this.readyState="open",this.writable=!0,this.emit("open")},n.prototype.onData=function(t){var e=r.decodePacket(t,this.socket.binaryType);this.onPacket(e)},n.prototype.onPacket=function(t){this.emit("packet",t)},n.prototype.onClose=function(){this.readyState="closed",this.emit("close")}},{"component-emitter":9,"engine.io-parser":25}],15:[function(t,e,n){(function(e){function r(t){var n,r=!1,a=!1,c=!1!==t.jsonp;if(e.location){var p="https:"==location.protocol,u=location.port;u||(u=p?443:80),r=t.hostname!=location.hostname||u!=t.port,a=t.secure!=p}if(t.xdomain=r,t.xscheme=a,n=new o(t),"open"in n&&!t.forceJSONP)return new i(t);if(!c)throw new Error("JSONP disabled");return new s(t)}var o=t("xmlhttprequest"),i=t("./polling-xhr"),s=t("./polling-jsonp"),a=t("./websocket");n.polling=r,n.websocket=a}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./polling-jsonp":16,"./polling-xhr":17,"./websocket":19,xmlhttprequest:20}],16:[function(t,e){(function(n){function r(){}function o(t){i.call(this,t),this.query=this.query||{},a||(n.___eio||(n.___eio=[]),a=n.___eio),this.index=a.length;var e=this;a.push(function(t){e.onData(t)}),this.query.j=this.index,n.document&&n.addEventListener&&n.addEventListener("beforeunload",function(){e.script&&(e.script.onerror=r)},!1)}var i=t("./polling"),s=t("component-inherit");e.exports=o;var a,c=/\n/g,p=/\\n/g;s(o,i),o.prototype.supportsBinary=!1,o.prototype.doClose=function(){this.script&&(this.script.parentNode.removeChild(this.script),this.script=null),this.form&&(this.form.parentNode.removeChild(this.form),this.form=null,this.iframe=null),i.prototype.doClose.call(this)},o.prototype.doPoll=function(){var t=this,e=document.createElement("script");this.script&&(this.script.parentNode.removeChild(this.script),this.script=null),e.async=!0,e.src=this.uri(),e.onerror=function(e){t.onError("jsonp poll error",e)};var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n),this.script=e;var r="undefined"!=typeof navigator&&/gecko/i.test(navigator.userAgent);r&&setTimeout(function(){var t=document.createElement("iframe");document.body.appendChild(t),document.body.removeChild(t)},100)},o.prototype.doWrite=function(t,e){function n(){r(),e()}function r(){if(o.iframe)try{o.form.removeChild(o.iframe)}catch(t){o.onError("jsonp polling iframe removal error",t)}try{var e='