diff --git a/anonkun.py b/anonkun.py index 6e16c7d..d358463 100644 --- a/anonkun.py +++ b/anonkun.py @@ -56,6 +56,7 @@ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 app.config['SESSION_TYPE'] = 'filesystem' app.jinja_env.trim_blocks = True app.jinja_env.lstrip_blocks = True +app.jinja_env.undefined = "StrictUndefined" Session(app) socketio.init_app(app) paranoid = Paranoid(app) diff --git a/anonkun.sql b/anonkun.sql index e5d89f9..8151b24 100644 --- a/anonkun.sql +++ b/anonkun.sql @@ -11,7 +11,7 @@ CREATE TABLE `quest_meta` ( `canon_title` VARCHAR(300) DEFAULT NULL, `ident_title` VARCHAR(300) DEFAULT NULL, `owner_id` SMALLINT UNSIGNED DEFAULT NULL, - `dice_call` SMALLINT UNSIGNED DEFAULT NULL, + `open_post_id` SMALLINT UNSIGNED DEFAULT NULL, PRIMARY KEY (`quest_id`) ) ENGINE=InnoDB CHARSET utf8mb4; @@ -24,7 +24,7 @@ CREATE TABLE `quest_data` ( PRIMARY KEY (`post_id`) ) ENGINE=InnoDB CHARSET utf8mb4; -CREATE TABLE `quest_dice_calls` ( +CREATE TABLE `dice_calls` ( `post_id` MEDIUMINT UNSIGNED NOT NULL, `dice_roll` TEXT NOT NULL, `strict` BOOLEAN DEFAULT FALSE, @@ -33,16 +33,30 @@ CREATE TABLE `quest_dice_calls` ( PRIMARY KEY (`post_id`) ) ENGINE=InnoDB CHARSET utf8mb4; -CREATE TABLE `quest_rolls` ( +CREATE TABLE `dice_rolls` ( `message_id` MEDIUMINT UNSIGNED NOT NULL, `quest_id` SMALLINT UNSIGNED NOT NULL, `post_id` MEDIUMINT UNSIGNED NOT NULL, `roll_dice` TEXT NOT NULL, `roll_results` TEXT NOT NULL, - `roll_total` SMALLINT UNSIGNED NOT NULL + `roll_total` SMALLINT UNSIGNED NOT NULL, PRIMARY KEY (`message_id`) ) ENGINE=InnoDB CHARSET utf8mb4; +CREATE TABLE `polls` ( + `post_id` MEDIUMINT UNSIGNED NOT NULL, + `multi_choice` BOOLEAN DEFAULT FALSE, + `allow_writein` BOOLEAN DEFAULT FALSE, + PRIMARY KEY (`post_id`) +) ENGINE=InnoDB CHARSET utf8mb4; + +CREATE TABLE `poll_options` ( + `post_id` MEDIUMINT UNSIGNED NOT NULL, + `quest_id` SMALLINT UNSIGNED NOT NULL, + `option_text` VARCHAR(200) NOT NULL, + `ips_voted` TEXT DEFAULT NULL +) ENGINE=InnoDB CHARSET utf8mb4; + CREATE TABLE `chat_messages` ( `message_id` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT, `room_id` MEDIUMINT UNSIGNED NOT NULL, diff --git a/database.py b/database.py index 5368ca5..adc8a81 100644 --- a/database.py +++ b/database.py @@ -214,58 +214,98 @@ def update_quest_post(post_id, new_post): (new_post, post_id)) -def set_dice_call_open(post_id, quest_id, new_call=None): - """Sets an open dice call for the given quest.""" +def set_post_open(post_id, quest_id): + """Sets an active post open for the given quest.""" _DB.execute( - "UPDATE `quest_meta` SET `dice_call` = %s WHERE `quest_id` = %s", + "UPDATE `quest_meta` SET `open_post_id` = %s WHERE `quest_id` = %s", (post_id, quest_id)) - if new_call: - dice_roll, strict, dice_challence, rolls_taken = new_call - _DB.execute( - "INSERT INTO `quest_dice_calls`" \ - + "(`post_id`, `dice_roll`, `strict`, " \ - + "`dice_challenge`, `rolls_taken`)" \ - + "VALUES (%s, %s, %s, %s, %s)", - (post_id, dice_roll, strict, dice_challence, rolls_taken)) - -def set_dice_call_closed(quest_id): +def set_post_closed(quest_id): """Closes a quest's dice call.""" _DB.execute( - "UPDATE `quest_meta` SET `dice_call` = NULL WHERE `quest_id` = %s", + "UPDATE `quest_meta` SET `open_post_id` = NULL WHERE `quest_id` = %s", (quest_id,)) +def insert_dice_call(post_id, quest_id, new_call): + """Inserts a new dice call.""" + dice_roll, strict, dice_challence, rolls_taken = new_call + _DB.execute( + "INSERT INTO `dice_calls`" \ + + "(`post_id`, `dice_roll`, `strict`,`dice_challenge`,`rolls_taken`)" \ + + "VALUES (%s, %s, %s, %s, %s)", + (post_id, dice_roll, strict, dice_challence, rolls_taken)) + + def get_dice_call(post_id): """Retrives the currently open dice call, if there is one.""" data = _DB.execute( - "SELECT * FROM `quest_dice_calls` WHERE `post_id` = %s", + "SELECT * FROM `dice_calls` WHERE `post_id` = %s", (post_id,)).fetchone() return data def insert_quest_roll(message_id, quest_id, post_id, roll_data): - """Inserts a user roll into the `quest_rolls` table.""" + """Inserts a user roll into the `dice_rolls` table.""" ins = (message_id, quest_id, post_id) + roll_data _DB.execute( - "INSERT INTO `quest_rolls`" \ + "INSERT INTO `dice_rolls`" \ + "(`message_id`, `quest_id`, `post_id`, " \ + "`roll_dice`, `roll_results`, `roll_total`)" \ + "VALUES (%s, %s, %s, %s, %s, %s)", (ins)) -def get_quest_rolls(quest_id=None, post_id=None): +def get_dice_rolls(quest_id=None, post_id=None): """Gets all rolls for the given quest.""" if quest_id: - sql = "SELECT * FROM `quest_rolls` WHERE `quest_id` = %s " + sql = "SELECT * FROM `dice_rolls` WHERE `quest_id` = %s " ins = quest_id elif post_id: - sql = "SELECT * FROM `quest_rolls` WHERE `post_id` = %s " + sql = "SELECT * FROM `dice_rolls` WHERE `post_id` = %s " ins = post_id else: return sql += "ORDER BY `message_id` ASC" data = _DB.execute(sql, (ins,)).fetchall() return data + + +def insert_poll(post_id, multi_choice, allow_writein): + """Inserts a new poll post.""" + _DB.execute( + "INSERT INTO `polls` " \ + + "(`post_id`, `multi_choice`, `allow_writein`) " \ + + "VALUES (%s, %s, %s)", + (post_id, multi_choice, allow_writein)) + + +def get_poll(post_id): + """Gets poll information.""" + data = _DB.excute( + "SELECT * FROM `polls` WHERE `post_id` = %s", (post_id,)) + return data + + +def insert_poll_option(post_id, quest_id, option_text): + """Insert a new poll option. ips_voted will be NULL to start.""" + _DB.execute( + "INSERT INTO `poll_options` " \ + + "(`post_id`, `quest_id`, `option_text`) VALUES (%s, %s, %s)", + (post_id, quest_id, option_text)) + + +def get_poll_options(quest_id=None, post_id=None): + """Gets all relevent poll options for a given quest or post.""" + sql = "SELECT * FROM `poll_options` WHERE " + if quest_id: + sql += "`quest_id` = %s" + ins = (quest_id,) + elif post_id: + sql += "`post_id` = %s" + ins = (post_id,) + else: + return + data = _DB.execute(sql, ins) + return data diff --git a/events.py b/events.py index 2fb7231..755ea5e 100644 --- a/events.py +++ b/events.py @@ -2,9 +2,11 @@ """ SocketIO events. """ +# TODO: harden the fuck up import re import time import random +import functools import bleach from flask import session @@ -15,6 +17,28 @@ import database as db socketio = SocketIO() +def qm_only(msg=""): + """ + A decorator function to protect certain endpoints so that only the + QM can access them. + """ + # TODO: better docstring, test this more thoroughly + def actual_decorator(func): + @functools.wraps(func) + def _nop(*args, **kwargs): + data = args[0] + room = data.get("room") + res = db.get_quest_meta(quest_id=room) + if not res: + return msg + if session.get("user_id") != res[3]: + return msg + return func(*args, **kwargs) + return _nop + return actual_decorator + + + @socketio.on('joined') def joined(data): """ @@ -76,7 +100,7 @@ def message(data): roll_data = (dice_roll, roll_results, roll_total) db.insert_quest_roll(message_id, room, dice_call_id, roll_data) - if len(db.get_quest_rolls(post_id=dice_call_id)) == dice_call[4]: + if len(db.get_dice_rolls(post_id=dice_call_id)) == dice_call[4]: db.set_dice_call_closed(room) emit("close_dice_call", {"post_id": dice_call_id}, room=room) @@ -123,46 +147,38 @@ def handle_dice(data): @socketio.on("new_post") +@qm_only() def new_post(data, internal=False): """ Called when the QM makes a new post. """ - room = data["room"] - res = db.get_quest_meta(quest_id=room) - if not res: - return - if session.get("user_id") != res[3]: - return - - post = data["post"] + room = data.get("room") + post = data.get("post") post = bleach.clean(post.strip()) post = post.replace("\n", "
") post = tools.handle_img(post) + date = int(time.time()) + + post_id = db.insert_quest_post(room, "text", post, date) + db.set_post_closed(room) data = {} data["post"] = [post] data["post_type"] = "text" - data["date"] = int(time.time()) - post_id = db.insert_quest_post(room, "text", post, data["date"]) - db.set_dice_call_closed(room); + data["date"] = date data["post_id"] = post_id emit("new_post", data, room=room) @socketio.on("update_post") +@qm_only() def update_post(data): """ Called when the QM edits and saves a post. """ - room = data["room"] - res = db.get_quest_meta(quest_id=room) - if not res: - return - if session.get("user_id") != res[3]: - return - # TODO: enforce only update text posts + room = data["room"] post = data["post"] post = post.strip().replace("
", "
") post = tools.handle_img(post) @@ -178,30 +194,25 @@ def update_post(data): @socketio.on("dice_post") +@qm_only() def dice_post(data): """ Called when the QM posts a new dice call. """ room = data["room"] - res = db.get_quest_meta(quest_id=room) - if not res: - return - if session.get("user_id") != res[3]: - return - data = {k: v for k, v in data.items() if v} try: diceNum = int(data.get("diceNum", 0)) diceSides = int(data.get("diceSides", 0)) diceMod = int(data.get("diceMod", 0)) diceChal = int(data.get("diceChal", 0)) - diceRolls = int(data.get("diceRolls", 0)) + diceRollsTaken = int(data.get("diceRollsTaken", 0)) assert 0 < diceNum < 100 assert 0 < diceSides < 100 assert -1000 < diceMod < 1000 assert 0 <= diceChal < 100 - assert 0 <= diceRolls < 100 + assert 0 <= diceRollsTaken < 100 except (ValueError, AssertionError): return diceStrict = bool(data.get("diceStrict")) @@ -215,56 +226,83 @@ def dice_post(data): post = "Roll " + dice_roll if diceChal: post += " vs DC" + str(diceChal) + date = int(time.time()) + + post_id = db.insert_quest_post(room, "dice", post, date) + new_call = (dice_roll, diceStrict, diceChal, diceRollsTaken) + db.insert_dice_call(post_id, room, new_call) + db.set_post_open(post_id, room) data = {} data["post"] = post data["post_type"] = "dice" - data["date"] = int(time.time()) - post_id = db.insert_quest_post(room, "dice", post, data["date"]) + data["date"] = date data["post_id"] = post_id - - new_call = (dice_roll, diceStrict, diceChal, diceRolls) - db.set_dice_call_open(post_id, room, new_call) emit("new_post", data, room=room) -@socketio.on('close_dice_call') +@socketio.on('close_post') +@qm_only() def close_dice_call(data): """ - Closes an active dice call. + Closes an active post. """ room = data["room"] - res = db.get_quest_meta(quest_id=room) - if not res: - return - if session.get("user_id") != res[3]: - return - post_id = data.get("post_id") - db.set_dice_call_closed(room) + db.set_post_closed(room) data = {} data["post_id"] = post_id - emit("close_dice_call", data, room=room) + emit("close_post", data, room=room) -@socketio.on("open_dice_call") +@socketio.on("open_post") +@qm_only() def open_dice_call(data): """ - Opens a closed dice call. This is only permitted if the dice call is + Opens an active post. This is only permitted if the active post is the last post in the quest. """ - room = data["room"] - res = db.get_quest_meta(quest_id=room) - if not res: - return - if session.get("user_id") != res[3]: - return - # TODO: enforce only open if last post + room = data["room"] post_id = data.get("post_id") - db.set_dice_call_open(post_id, room) + db.set_post_open(post_id, room) data = {} data["post_id"] = post_id - emit("open_dice_call", data, room=room) + emit("open_post", data, room=room) + + +@socketio.on("poll_post") +@qm_only() +def poll_post(data): + """ + Called when the QM posts a new dice call. + """ + room = data.pop("room", None) + multi_choice = bool(data.pop("pollAllowMultipleChoices", None)) + allow_writein = bool(data.pop("pollAllowUserOptions", None)) + options = [] + for key, value in data.items(): + if not value or not key.startswith("pollOption-"): + continue + if len(value) >= 200: + return + options.append(value) + + post = "Poll" + date = int(time.time()) + + post_id = db.insert_quest_post(room, "poll", post, date) + db.insert_poll(post_id, multi_choice, allow_writein) + for option in options: + db.insert_poll_option(post_id, room, option) + db.set_post_open(post_id, room) + + data = {} + data["post"] = post + data["post_id"] = post_id + data["post_type"] = "poll" + data["date"] = int(time.time()) + data["options"] = options + emit("new_post", data, room=room) diff --git a/templates/quest.html b/templates/quest.html index fefb014..d2209ad 100644 --- a/templates/quest.html +++ b/templates/quest.html @@ -31,15 +31,17 @@ post_str += 'textPost">'; } else if (data.post_type == 'dice') { post_str += 'dicePost active_post">'; + } else if (data.post_type == 'poll') { + post_str += 'pollPost active_post">'; } post_str += '
' + strftime(data.date); {% if session.get("user_id") == owner_id %} if (data.post_type == 'text') { post_str += '
Edit'; post_str += ''; - } else if (data.post_type == 'dice') { - post_str += '
Close'; - post_str += '' + } else if (data.post_type == 'dice' || data.post_type == 'poll') { + post_str += '
Close'; + post_str += '' } {% endif %} post_str += '
'; @@ -47,6 +49,13 @@ post_str += data.post; } else if (data.post_type == 'dice') { post_str += '

' + data.post + ' - Open

'; + } else if (data.post_type == 'poll') { + post_str += '

' + data.post + ' - Open

'; + post_str += ''; + for (i = 0; i < data.options.length; i++) { + post_str += ''; + } + post_str += '
' + data.options[i] + '
'; } post_str += '

'; qposts.innerHTML = qposts.innerHTML + post_str; @@ -61,17 +70,17 @@ post.innerHTML += '' + data.post + '
'; } }); - socket.on('close_dice_call', function(data) { + socket.on('close_post', function(data) { let post = document.getElementById('questPostData-' + data.post_id); post.children[0].textContent = post.children[0].textContent.replace('Open', 'Closed'); - document.getElementById('close_dc-' + data.post_id).style.display = 'none'; - document.getElementById('open_dc-' + data.post_id).style.display = 'initial'; + document.getElementById('close_post_id-' + data.post_id).style.display = 'none'; + document.getElementById('open_post_id-' + data.post_id).style.display = 'initial'; }); - socket.on('open_dice_call', function(data) { + socket.on('open_post', function(data) { let post = document.getElementById('questPostData-' + data.post_id); post.firstElementChild.textContent = post.firstElementChild.textContent.replace('Closed', 'Open'); - document.getElementById('close_dc-' + data.post_id).style.display = 'initial'; - document.getElementById('open_dc-' + data.post_id).style.display = 'none'; + document.getElementById('close_post_id-' + data.post_id).style.display = 'initial'; + document.getElementById('open_post_id-' + data.post_id).style.display = 'none'; }); let mtarea = document.getElementById('messageTextArea'); mtarea.addEventListener('keypress', function(event) { @@ -120,32 +129,30 @@ document.getElementById('savePost-' + post_id).style.display = 'none'; document.getElementById('editPost-' + post_id).style.display = 'initial'; } - function dice_post() { - let formData = new FormData(document.getElementById('QMDicePostForm')); + function form_post(form_id, emit_msg) { + let formData = new FormData(document.getElementById(form_id)); let obj = {}; formData.forEach(function(value, key) { obj[key] = value; }); obj.room = {{ room_id }}; - socket.emit('dice_post', obj); - document.getElementById('QMDicePostForm').reset(); + socket.emit(emit_msg, obj); + document.getElementById(form_id).reset(); } - function close_dice_call(post_id) { - socket.emit('close_dice_call', {post_id: post_id, room: {{ room_id }}}); + function close_post(post_id) { + socket.emit('close_post', {post_id: post_id, room: {{ room_id }}}); } - function open_dice_call(post_id) { - socket.emit('open_dice_call', {post_id: post_id, room: {{ room_id }}}); + function open_post(post_id) { + socket.emit('open_post', {post_id: post_id, room: {{ room_id }}}); } function deactivate_post() { let post = document.getElementsByClassName('active_post'); if (post.length == 0) { return; } post = post[0]; - if (post.classList.contains('dicePost')) { - post.children[1].children[0].textContent = post.children[1].children[0].textContent.replace('Open', 'Closed'); - post.classList.remove("active_post"); - post.children[0].children[2].outerHTML = ""; - post.children[0].children[1].outerHTML = ""; - } + post.children[1].children[0].textContent = post.children[1].children[0].textContent.replace('Open', 'Closed'); + post.classList.remove("active_post"); + post.children[0].children[2].outerHTML = ""; + post.children[0].children[1].outerHTML = ""; } function insertPollOption() { let opts = document.getElementById('pollOptions'); @@ -195,6 +202,8 @@
{% elif quest_post[2] == "dice" %}
+ {% elif quest_post[2] == "poll" %} +
{% endif %}
{{ quest_post[4] | strftime }} @@ -202,13 +211,13 @@ {% if quest_post[2] == "text" %}
Edit - {% elif quest_post[2] == "dice" and quest_post == quest_posts|last %} - {% if quest_post[0] == dice_call %} -
Close - + {% elif quest_post[2] == "dice" or quest_post[2] == "poll" and quest_post == quest_posts|last %} + {% if quest_post[0] == open_post_id %} +
Close + {% else %} -
- Open +
+ Open {% endif %} {% endif %} {% endif %} @@ -219,14 +228,25 @@ {{ quest_post[3] }} {% endautoescape %} {% elif quest_post[2] == "dice" %} -

{{ quest_post[3] }} - {% if quest_post[0] == dice_call_id %}Open{% else %}Closed{% endif %}

- {% for quest_roll in quest_rolls %} - {% if quest_roll[2] == quest_post[0] %} -
- Rolled {{ quest_roll[4] }} = {{ quest_roll[5] }} ({{ quest_roll[3] }}){% if quest_post[0]|dice_chal != 0 %} - {% if quest_roll[5] >= quest_post[0]|dice_chal %}Pass{% else %} Fail{% endif %}{% endif %} +

{{ quest_post[3] }} - {% if quest_post[0] == open_post_id %}Open{% else %}Closed{% endif %}

+ {% for dice_roll in dice_rolls %} + {% if dice_roll[2] == quest_post[0] %} +
+ Rolled {{ dice_roll[4] }} = {{ dice_roll[5] }} ({{ dice_roll[3] }}){% if quest_post[0]|dice_chal != 0 %} - {% if dice_roll[5] >= quest_post[0]|dice_chal %}Pass{% else %} Fail{% endif %}{% endif %}
{% endif %} {% endfor %} + {% elif quest_post[2] == "poll" %} +

{{ quest_post[3] }} - {% if quest_post[0] == open_post_id %}Open{% else %}Closed{% endif %}

+ + {% for option in poll_options %} + {% if option[0] == quest_post[0] %} + + + + {% endif %} + {% endfor %} +
{{ option[2] }}
{% endif %}

@@ -247,7 +267,7 @@