diff --git a/.gitignore b/.gitignore index ae2d4a0..684c562 100644 --- a/.gitignore +++ b/.gitignore @@ -133,5 +133,5 @@ dmypy.json # jetbrains folder .idea/ qbBuzzer.iml -.env test/ + diff --git a/src/app.py b/src/app.py index 3906df4..d4d4211 100644 --- a/src/app.py +++ b/src/app.py @@ -2,43 +2,38 @@ from flask import * from random import randint as rint from src.config import Config from src.host import HostForm -from src.sec import gencode, dohash +from src.join import JoinForm +from src.sec import gencode, dohash, whitelist from logging.config import dictConfig -import logging -from flask_socketio import SocketIO +from flask_socketio import SocketIO, emit, join_room, leave_room import json -dictConfig({ - 'version': 1, - 'formatters': {'default': { - 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', - }}, - 'handlers': {'wsgi': { - 'class': 'logging.StreamHandler', - 'stream': 'ext://flask.logging.wsgi_errors_stream', - 'formatter': 'default' - }}, - 'root': { - 'level': 'INFO', - 'handlers': ['wsgi'] - } -}) +# Loading logging preferences +with open("logger.json", "r") as f: + dconf = json.load(f) +# Establishing logger +dictConfig(dconf) + +# Loading flask app app = Flask(__name__) app.logger.info("Flask app loaded at " + __name__) app.config.from_object(Config) -app.logger.setLevel(logging.DEBUG) - +# Version allows css / js to load instead of taking hours to update even on run smh version = rint(0, 300000000) + +# games games = {} +# home @app.route('/') def home(): return render_template('index.html', version=str(version)) +# creating a room for players to join @app.route("/host", methods=["GET", "POST"]) def host(): form = HostForm() @@ -46,7 +41,7 @@ def host(): hostcode = gencode(64) hash = dohash(hostcode) resp = redirect(url_for("play", hash=hash)) - resp.set_cookie("_gid", str(hostcode), httponly=True, secure=True) + resp.set_cookie("_gid", str(hostcode)) with open("src/templates/games.json", "r") as f: tmp = json.load(f) games[hash] = tmp @@ -55,7 +50,6 @@ def host(): games[hash]["bonus"] = form.bonus.data games[hash]["power"] = form.power.data games[hash]["negs"] = form.negs.data - games[hash]["teams"] = form.teams.data app.logger.debug("Game host at %s", hostcode) app.logger.info("Game created at %s", hash) return resp @@ -63,32 +57,136 @@ def host(): return render_template('host.html', title="Host Game", version=str(version), form=form, default=default) +# Inside the room itself @app.route("/play/") def play(hash): if hash in games.keys(): - return render_template('play.html', version=str(version)) + if dohash(request.cookies.get('_gid')) == hash: + return render_template('gamehost.html', version=str(version), gamecode=hash) + else: + if "name" in request.cookies: + name = request.cookies.get("name") + return render_template('play.html', version=str(version), gamecode=hash, username=name) + else: + return render_template('please.html', version=str(version)) else: abort(404) -@app.route("/join") +@app.route("/kick") +def kick(): + return render_template('kick.html', version=str(version)) + + +# When players attempt to join a room +@app.route("/join", methods=["GET", "POST"]) def join(): - return "In progress!" + form = JoinForm() + if form.validate_on_submit(): + wlist = whitelist() + if not all([a in wlist for a in form.name.data]): + return render_template('badname.html', title='Join Game', version=str(version)) + hash = form.roomcode.data + if hash in games.keys(): + if form.name.data in games[hash]["players"].keys(): + return render_template('nametaken.html', title='Join Game', version=str(version)) + games[hash]["players"][form.name.data] = 0 + resp = redirect(url_for("play", hash=hash)) + resp.set_cookie("_gid", "") + resp.set_cookie("name", form.name.data) + return resp + else: + return render_template('gamenotfound.html', title="Join Game", version=str(version)) + return render_template('join.html', title="Join Game", version=str(version), form=form) +# If someone visits somethin stupid @app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 +# Connecting socketio for all socketio functions below socketio = SocketIO(app) +# | +# V + + @socketio.on('json') def handle_json(json): print('received message: ' + str(json)) + emit('json', json) +# Checks when a player / host joins the room +@socketio.on('join') +def on_join(data): + room = data['room'] + username = "" + if "username" in data.keys(): + username = data['username'] + else: + gid = data["_gid"] + if dohash(gid) == room: + username = "host" + join_room(str(room)) + emit('player_join_event', games[room]["players"], room=room) + + +# When the host sends data to server +@socketio.on('host') +def host_msg(data): + room = data["room"] + gid = data["_gid"] + if dohash(gid) != room: # Check if the host is really the host + return + msg = data["data"] + if "lock" in msg.keys(): + pass # lock buzzers + elif "kick" in msg.keys(): + msg["url"] = url_for('kick') + username = msg["kick"] + del games[room]["players"][username] + emit("player_kick_event", msg, room=room) + elif "tossup" in msg.keys(): + pass # give player points + elif "bonus" in msg.keys(): + pass # give player points + elif "power" in msg.keys(): + pass # give player points + elif "negs" in msg.keys(): + pass # give player points + elif "amount" in msg.keys(): + pass # give player points + + +# When the player buzzes +@socketio.on('buzz') +def buzz(data): + room = data["room"] + emit("buzz", data, room=room) # Just send it back + + +# When a player / host leaves +@socketio.on('leave') +def on_leave(data): + print("player leave") + room = data['room'] + username = "" + if "username" in data.keys(): + username = data['username'] + else: + gid = data["_gid"] + if dohash(gid) == room: + username = "host" + del games[room]["players"][username] + leave_room(room) + emit('player_leave_event', {"player": username}, room=room) + + +# Run the thing lol if __name__ == "__main__": - socketio.run(app) + socketio.run(app, host="0.0.0.0", port=25565) # app.run(host="127.0.0.1", port=25565) diff --git a/src/host.py b/src/host.py index d0ebf4b..2785535 100644 --- a/src/host.py +++ b/src/host.py @@ -1,10 +1,9 @@ from flask_wtf import FlaskForm -from wtforms import IntegerField, SubmitField, BooleanField +from wtforms import IntegerField, SubmitField from wtforms.validators import DataRequired class HostForm(FlaskForm): tossup = IntegerField('Tossup Points', validators=[DataRequired()]) bonus = IntegerField('Bonus Points', validators=[DataRequired()]) power = IntegerField('Power Points', validators=[DataRequired()]) negs = IntegerField('Neg Points [Positive]', validators=[DataRequired()]) - teams = BooleanField('Teams? ') create = SubmitField('Create Room') \ No newline at end of file diff --git a/src/join.py b/src/join.py index ba0f6b4..731e613 100644 --- a/src/join.py +++ b/src/join.py @@ -1,6 +1,7 @@ from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, BooleanField +from wtforms import StringField, SubmitField from wtforms.validators import DataRequired class JoinForm(FlaskForm): roomcode = StringField('Room Code', validators=[DataRequired()]) + name = StringField('Name', validators=[DataRequired()]) create = SubmitField('Join Room') \ No newline at end of file diff --git a/src/logger.json b/src/logger.json new file mode 100644 index 0000000..51382b8 --- /dev/null +++ b/src/logger.json @@ -0,0 +1,15 @@ +{ + "version": 1, + "formatters": {"default": { + "format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s" + }}, + "handlers": {"wsgi": { + "class": "logging.StreamHandler", + "stream": "ext://flask.logging.wsgi_errors_stream", + "formatter": "default" + }}, + "root": { + "level": "DEBUG", + "handlers": ["wsgi"] + } +} \ No newline at end of file diff --git a/src/sec.py b/src/sec.py index 1bfb7c5..fd5564f 100644 --- a/src/sec.py +++ b/src/sec.py @@ -20,6 +20,10 @@ def sha3256(val): return ho.digest() +def whitelist(): + return list(ascii_letters + digits) + + def hexdigest(val): return hexlify(val).decode('utf-8') diff --git a/src/static/buzz.mp3 b/src/static/buzz.mp3 new file mode 100644 index 0000000..cc4b024 Binary files /dev/null and b/src/static/buzz.mp3 differ diff --git a/src/static/game.js b/src/static/game.js index 9f392de..dc2b6ae 100644 --- a/src/static/game.js +++ b/src/static/game.js @@ -1,7 +1,74 @@ var socket = io(); -test = function() { - console.log("h"); - socket.emit('json', {data: 'I\'m connected!'}); + +code = document.getElementById("code").value; +name = document.getElementById("name").value; +kickurl = document.getElementById("kick").value; + +function removeAllChildren(e) { + var child = e.lastElementChild; + while (child) { + e.removeChild(child); + child = e.lastElementChild; + } +} + +socket.on("connect", function() { + // when client first connects to a game + socket.emit("join", {"username": name, "room": code, "_gid": ""}); +}) + +socket.on("player_join_event", function(data) { + div = document.getElementById("sidebar"); + removeAllChildren(div); + if (Object.keys(data).length == 0) { + var keepSidebarOpen = document.createElement("input"); + keepSidebarOpen.setAttribute("type", "hidden"); + div.appendChild(keepSidebarOpen); + return + } + var keys = Object.keys(data); + for (var key in keys) { + var playerDisplay = document.createElement("div"); + playerDisplay.setAttribute("id", keys[key]); + playerDisplay.setAttribute("class", "playertag"); + var playerLink = document.createElement("p"); + playerLink.appendChild(document.createTextNode(keys[key])); + playerDisplay.appendChild(playerLink); + playerDisplay.appendChild(document.createElement("br")); + div.appendChild(playerDisplay); + } +}); + +socket.on("player_leave_event", function(data) { + var element = document.getElementById(data["player"]); + element.parentNode.removeChild(element); +}); + +socket.on("player_kick_event", function(data) { + if (data["kick"] == name) { + window.location = kickurl; + } else { + var element = document.getElementById(data["kick"]); + element.parentNode.removeChild(element); + } + +}) + +function buzz() { + // when client hits buzz + socket.emit("buzz", {"username": name, "room": code, "_gid": ""}); +} + +$(document).on('keypress', function(e) { + if (e.key === ' ' || e.key === 'Spacebar') { + e.preventDefault(); + buzz(); + } +}) + +window.onbeforeunload = function leave() { + // when client leaves + console.log("leave") + socket.emit("leave", {"username": name, "room": code, "_gid": ""}); } -// div id = game (that's where the game goes) \ No newline at end of file diff --git a/src/static/host.js b/src/static/host.js new file mode 100644 index 0000000..a87678e --- /dev/null +++ b/src/static/host.js @@ -0,0 +1,90 @@ +var socket = io(); + +code = document.getElementById("copycode").value; + +function readCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; +} + +gid = readCookie("_gid"); + +copycode = function() { + var copyText = document.getElementById("copycode") + copyText.select(); + copyText.setSelectionRange(0, 99999); + document.execCommand("copy") +} + +function send(data) { + socket.emit("host", {"room": code, "_gid": gid, "data": data}); +} + +function removeAllChildren(e) { + var child = e.lastElementChild; + while (child) { + e.removeChild(child); + child = e.lastElementChild; + } +} + +socket.on("connect", function() { + socket.emit("join", {"room": code, "_gid": gid}); +}); +window.onbeforeunload = function leave() { + socket.emit("leave", {"room": code, "_gid": gid}); +} + + +socket.on("player_join_event", function(data) { + div = document.getElementById("sidebar"); + removeAllChildren(div); + if (Object.keys(data).length == 0) { + var keepSidebarOpen = document.createElement("input"); + keepSidebarOpen.setAttribute("type", "hidden"); + div.appendChild(keepSidebarOpen); + return + } + var keys = Object.keys(data); + for (var key in keys) { + var playerDisplay = document.createElement("div"); + playerDisplay.setAttribute("id", keys[key]); + playerDisplay.setAttribute("class", "playertag"); + var playerLink = document.createElement("p"); + playerLink.appendChild(document.createTextNode(keys[key])); + var removeLink = document.createElement("a"); + removeLink.setAttribute("onclick", "removePlayer('" + keys[key] + "');"); + removeLink.appendChild(document.createTextNode("Kick")); + playerDisplay.appendChild(playerLink); + playerDisplay.appendChild(removeLink); + playerDisplay.appendChild(document.createElement("br")); + div.appendChild(playerDisplay); + } +}); + +socket.on("player_leave_event", function(data) { + var element = document.getElementById(data["player"]); + element.parentNode.removeChild(element); +}); + +socket.on("player_kick_event", function(data) { + var element = document.getElementById(data["kick"]); + element.parentNode.removeChild(element); + +}) + +function removePlayer(playername) { + send({"kick": playername}); +} + +socket.on("buzz", function(message) { + var audio = new Audio('/static/buzz.mp3'); + audio.play(); +}) + diff --git a/src/static/index.css b/src/static/index.css index 7570866..e75e556 100644 --- a/src/static/index.css +++ b/src/static/index.css @@ -92,6 +92,63 @@ body { margin-left: 5px; } +.copycode { + text-align: center; +} + +.form { + text-align: center; +} + +.form input { + text-align: center; +} + +.sidebar { + height: 100%; /* Full-height: remove this if you want "auto" height */ + font-size: 25px; + position: fixed; /* Fixed Sidebar (stay in place on scroll) */ + z-index: 1; /* Stay on top */ + top: 0; /* Stay at the top */ + left: 0; + background-color: #e2e27c; + padding-top: 20px; + padding-left: 10px; + padding-right: 20px; +} + +/* The navigation menu links */ +.sidebar a { + padding: 6px 8px 6px 8px; + text-decoration: none; + font-size: 14px; + color: #ea2a2d; + display: inline; +} + +.sidebar p { + padding: 6px 8px 6px 6px; + text-decoration: none; + font-size: 25px; + color: #818181; + display: inline; +} + +/* When you mouse over the navigation links, change their color */ +.sidebar a:hover { + color: #f1f1f1; +} + +/* Style page content */ +.game { + padding: 0px 10px; +} + +/* On smaller screens, where height is less than 450px, change the style of the sidebar (less padding and a smaller font size) */ +@media screen and (max-height: 450px) { + .sidebar {padding-top: 15px;} + .sidebar a {font-size: 18px;} +} @font-face { font-family: 'Abberancy-Regular'; diff --git a/src/templates/badname.html b/src/templates/badname.html new file mode 100644 index 0000000..7fcaba0 --- /dev/null +++ b/src/templates/badname.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block content %} +
+ Back +
+
+

Bad Name

+
+
+
+

Names can only be alphanumeric.

+
+
+{% endblock %} \ No newline at end of file diff --git a/src/templates/gamehost.html b/src/templates/gamehost.html new file mode 100644 index 0000000..2540558 --- /dev/null +++ b/src/templates/gamehost.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block content %} +
+

Game!

+
+ + +
+
+

Click to copy the game code:

+ +
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/src/templates/gamenotfound.html b/src/templates/gamenotfound.html new file mode 100644 index 0000000..e2caa98 --- /dev/null +++ b/src/templates/gamenotfound.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block content %} +
+ Back +
+
+

Game not found

+
+{% endblock %} \ No newline at end of file diff --git a/src/templates/games.json b/src/templates/games.json index a9f90e3..40cc82e 100644 --- a/src/templates/games.json +++ b/src/templates/games.json @@ -4,5 +4,6 @@ "bonus": 20, "power": 15, "negs": 5, - "teams": false + "teams": false, + "players": {} } \ No newline at end of file diff --git a/src/templates/host.html b/src/templates/host.html index 3575e0c..5f24cce 100644 --- a/src/templates/host.html +++ b/src/templates/host.html @@ -7,41 +7,39 @@

Host Game

-
- {{ form.csrf_token }} - {{ form.hidden_tag() }} -

- {{ form.tossup.label }}
- {{ form.tossup(size=32, value=default[0]) }}
- {% for error in form.tossup.errors %} - [{{ error }}]
- {% endfor %} -
- {{ form.bonus.label }}
- {{ form.bonus(size=32, value=default[1]) }}
- {% for error in form.bonus.errors %} - [{{ error }}]
- {% endfor %} -
- {{ form.power.label }}
- {{ form.power(size=32, value=default[2]) }}
- {% for error in form.power.errors %} - [{{ error }}]
- {% endfor %} -
- {{ form.negs.label }}
- {{ form.negs(size=32, value=default[3]) }}
- {% for error in form.negs.errors %} - [{{ error }}]
- {% endfor %} -
- {{ form.teams() }} {{ form.teams.label }}
- {% for error in form.negs.errors %} - [{{ error }}]
- {% endfor %} -
- {{ form.create() }} -
-

-
+
+
+ {{ form.csrf_token }} +

+

+ {{ form.tossup.label }}
+ {{ form.tossup(size=32, value=default[0]) }}
+ {% for error in form.tossup.errors %} + [{{ error }}]
+ {% endfor %} +
+ {{ form.bonus.label }}
+ {{ form.bonus(size=32, value=default[1]) }}
+ {% for error in form.bonus.errors %} + [{{ error }}]
+ {% endfor %} +
+ {{ form.power.label }}
+ {{ form.power(size=32, value=default[2]) }}
+ {% for error in form.power.errors %} + [{{ error }}]
+ {% endfor %} +
+ {{ form.negs.label }}
+ {{ form.negs(size=32, value=default[3]) }}
+ {% for error in form.negs.errors %} + [{{ error }}]
+ {% endfor %} +
+ {{ form.create() }} +
+
+

+
+
{% endblock %} \ No newline at end of file diff --git a/src/templates/join.html b/src/templates/join.html index b04a1d4..afb233c 100644 --- a/src/templates/join.html +++ b/src/templates/join.html @@ -1,47 +1,30 @@ {% extends "base.html" %} {% block content %} -
- Back -
-
-

Host Game

-
-
- {{ form.csrf_token }} - {{ form.hidden_tag() }} -

- {{ form.tossup.label }}
- {{ form.tossup(size=32, value=default[0]) }}
- {% for error in form.tossup.errors %} - [{{ error }}]
- {% endfor %} -
- {{ form.bonus.label }}
- {{ form.bonus(size=32, value=default[1]) }}
- {% for error in form.bonus.errors %} - [{{ error }}]
- {% endfor %} -
- {{ form.power.label }}
- {{ form.power(size=32, value=default[2]) }}
- {% for error in form.power.errors %} - [{{ error }}]
- {% endfor %} -
- {{ form.negs.label }}
- {{ form.negs(size=32, value=default[3]) }}
- {% for error in form.negs.errors %} - [{{ error }}]
- {% endfor %} -
- {{ form.teams() }} {{ form.teams.label }}
- {% for error in form.negs.errors %} - [{{ error }}]
- {% endfor %} -
- {{ form.create() }} -
-

-
+
+ Back +
+
+

Join Game

+
+
+
+ {{ form.csrf_token }} +

+ {{ form.roomcode.label }}
+ {{ form.roomcode(size=32) }}
+ {% for error in form.roomcode.errors %} + [{{ error }}]
+ {% endfor %} +
+ {{ form.name.label }}
+ {{ form.name(size=32) }}
+ {% for error in form.name.errors %} + [{{ error }}]
+ {% endfor %} +
+ {{ form.create() }} +

+
+
{% endblock %} \ No newline at end of file diff --git a/src/templates/kick.html b/src/templates/kick.html new file mode 100644 index 0000000..1297a39 --- /dev/null +++ b/src/templates/kick.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +
+ Back +
+
+

You have been kicked.

+
+{% endblock %} \ No newline at end of file diff --git a/src/templates/nametaken.html b/src/templates/nametaken.html new file mode 100644 index 0000000..e4d6075 --- /dev/null +++ b/src/templates/nametaken.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +
+ Back +
+
+

Name is already taken.

+
+{% endblock %} \ No newline at end of file diff --git a/src/templates/play.html b/src/templates/play.html index b6290ca..9b0a1d4 100644 --- a/src/templates/play.html +++ b/src/templates/play.html @@ -1,9 +1,42 @@ -{% extends "base.html" %} - -{% block content %} - - -
- + + + {% if title %} + + + + + {{ title }} - Buzzer! + + {% else %} + + Buzzer! + + + + {% endif %} + +
+

Play!

-{% endblock %} + +

+
+
+ +
+
+
+ +
+ +
+
+ + + + + + diff --git a/src/templates/please.html b/src/templates/please.html new file mode 100644 index 0000000..e645fcc --- /dev/null +++ b/src/templates/please.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block content %} +
+ Back +
+
+

Please join using the Join page.

+
+{% endblock %} \ No newline at end of file