Compare commits

..

3 Commits

7 changed files with 221 additions and 103 deletions

View File

@ -8,6 +8,7 @@ from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer from channels.generic.websocket import WebsocketConsumer
from .events import events from .events import events
from .models import Quest
class QuestConsumer(WebsocketConsumer): class QuestConsumer(WebsocketConsumer):
""" """
@ -25,6 +26,11 @@ class QuestConsumer(WebsocketConsumer):
self.quest_id = self.scope['url_route']['kwargs']['quest_id'] self.quest_id = self.scope['url_route']['kwargs']['quest_id']
self.group_name = 'quest_' + str(self.quest_id) self.group_name = 'quest_' + str(self.quest_id)
try:
Quest.objects.get(id=self.quest_id)
except Quest.DoesNotExist:
return
async_to_sync(self.channel_layer.group_add)( async_to_sync(self.channel_layer.group_add)(
self.group_name, self.group_name,
self.channel_name self.channel_name

View File

@ -3,8 +3,6 @@
Individual functions for handling WebSocket events. Gets called by the Individual functions for handling WebSocket events. Gets called by the
QuestConsumer object in consumers.py. QuestConsumer object in consumers.py.
""" """
# TODO: quest owner only events
# TODO: try/except on database calls
import re import re
import time import time
import types import types
@ -25,12 +23,12 @@ def message(socket, data):
""" """
Gets called when the server receives a 'message' event. Gets called when the server receives a 'message' event.
""" """
# TODO: validation
message = data.get('message') message = data.get('message')
# cleaning # cleaning
message = message[:512] message = message[:512]
message = message.strip() message = message.strip()
message = re.sub(r'\n\n+', '\n\n', message)
if not message: if not message:
return return
tags = ["b", "code", "i", "s"] tags = ["b", "code", "i", "s"]
@ -63,6 +61,7 @@ def message(socket, data):
if not reg: if not reg:
return return
try: try:
# TODO: form validation?
groups = [0 if d is None else int(d) for d in reg.groups()] groups = [0 if d is None else int(d) for d in reg.groups()]
dice_num, dice_sides, dice_mod = groups dice_num, dice_sides, dice_mod = groups
assert 1 <= dice_num <= 256 assert 1 <= dice_num <= 256
@ -149,20 +148,28 @@ def text_post(socket, data):
""" """
Called when the QM creates a new text post. Called when the QM creates a new text post.
""" """
# TODO: security quest = Quest.objects.get(id=socket.quest_id)
user = socket.scope['user']
if quest.owner != user:
return # error message?
post_text = data.get('text') post_text = data.get('text')
page_num = data.get('page_num') page_num = data.get('page_num')
try:
page = Page.objects.get(quest=quest, page_num=page_num)
except Page.DoesNotExist:
return
# cleaning # cleaning
post_text = bleach.clean(post_text.strip()) post_text = bleach.clean(post_text.strip())
post_text = post_text.replace("\n", "<br>") post_text = post_text.replace("\n", "<br>")
# handle image # handle image
quest = Quest.objects.get(id=socket.quest_id)
p = Post( p = Post(
quest=quest, quest=quest,
page=Page.objects.get(quest=quest, page_num=page_num), page=page,
post_type='text', post_type='text',
post_text=post_text) post_text=post_text)
p.save() p.save()
@ -194,18 +201,31 @@ def dice_post(socket, data):
""" """
Called when the QM makes a new dice post. Called when the QM makes a new dice post.
""" """
quest = Quest.objects.get(id=socket.quest_id)
user = socket.scope['user']
if quest.owner != user:
return # error message?
page_num = data.get('page_num') page_num = data.get('page_num')
try:
page = Page.objects.get(quest=quest, page_num=page_num)
except Page.DoesNotExist:
return
form = DiceCallForm(data) form = DiceCallForm(data)
if not form.is_valid(): if not form.is_valid():
return # error message? return # error message?
form = form.cleaned_data form = form.cleaned_data
posts = Post.objects.filter( posts = Post.objects.filter(
quest__id=socket.quest_id, post_type='dice', dicecall__open=True) quest=quest,
post_type='dice',
dicecall__open=True
)
for post in posts: for post in posts:
post.dicecall.open = False post.dicecall.open = False
post.dicecall.save() post.dicecall.save()
socket.send('close_all_posts') socket.send('close_all_posts', {'post_type': 'dice'})
dice_roll = str(form['diceNum']) + "d" dice_roll = str(form['diceNum']) + "d"
dice_roll += str(form['diceSides']) dice_roll += str(form['diceSides'])
@ -218,10 +238,9 @@ def dice_post(socket, data):
if form['diceChal']: if form['diceChal']:
post_text += " vs DC" + str(form['diceChal']) post_text += " vs DC" + str(form['diceChal'])
quest = Quest.objects.get(id=socket.quest_id)
p = Post( p = Post(
quest=quest, quest=quest,
page=Page.objects.get(quest=quest, page_num=page_num), page=page,
post_type='dice', post_type='dice',
post_text=post_text post_text=post_text
) )
@ -263,16 +282,25 @@ def poll_post(socket, data):
""" """
Called when the QM makes a new dice post. Called when the QM makes a new dice post.
""" """
quest = Quest.objects.get(id=socket.quest_id)
user = socket.scope['user']
if quest.owner != user:
return # error message?
try:
page = Page.objects.get(quest=quest, page_num=page_num)
except Page.DoesNotExist:
return
page_num = data.get('page_num') page_num = data.get('page_num')
form = PollForm(data) form = PollForm(data)
if not form.is_valid(): if not form.is_valid():
return # error message? return # error message?
form = form.cleaned_data form = form.cleaned_data
quest=Quest.objects.get(id=socket.quest_id)
p = Post( p = Post(
quest=quest, quest=quest,
page=Page.objects.get(quest=quest, page_num=page_num), page=page,
post_type='poll', post_type='poll',
post_text="Poll" post_text="Poll"
) )
@ -321,16 +349,25 @@ def edit_post(socket, data):
""" """
Called when the QM saves an edited post. Called when the QM saves an edited post.
""" """
quest = Quest.objects.get(id=socket.quest_id)
user = socket.scope['user']
if quest.owner != user:
return # error message?
post_id = data.get('post_id') post_id = data.get('post_id')
post_text = data.get('post_text') post_text = data.get('post_text')
try:
p = Post.objects.get(id=post_id)
except Post.DoesNotExist:
return
# cleaning # cleaning
post_text = bleach.clean(post_text.strip()) post_text = bleach.clean(post_text.strip())
post_text = post_text.replace("\n", "<br>") post_text = post_text.replace("\n", "<br>")
# handle image # handle image
p = Post.objects.get(id=post_id)
p.post_text = post_text p.post_text = post_text
p.save() p.save()
@ -345,8 +382,17 @@ def close_post(socket, data):
""" """
Called when the QM closes an open post. Called when the QM closes an open post.
""" """
quest = Quest.objects.get(id=socket.quest_id)
user = socket.scope['user']
if quest.owner != user:
return # error message?
post_id = data.get('post_id') post_id = data.get('post_id')
try:
p = Post.objects.get(id=post_id) p = Post.objects.get(id=post_id)
except Post.DoesNotExist:
return
if data.get('post_type') == 'dice': if data.get('post_type') == 'dice':
p.dicecall.open = False p.dicecall.open = False
p.dicecall.save() p.dicecall.save()
@ -363,12 +409,23 @@ def open_post(socket, data):
""" """
Called when the QM opens a closed post. Called when the QM opens a closed post.
""" """
# TODO: only posts on last page can be opened quest = Quest.objects.get(id=socket.quest_id)
user = socket.scope['user']
if quest.owner != user:
return # error message?
post_id = data.get('post_id') post_id = data.get('post_id')
try:
p = Post.objects.get(id=post_id) p = Post.objects.get(id=post_id)
except Post.DoesNotExist:
return
if data.get('post_type') == 'dice': if data.get('post_type') == 'dice':
posts = Post.objects.filter( posts = Post.objects.filter(
quest__id=socket.quest_id, post_type='dice', dicecall__open=True) quest=quest,
post_type='dice',
dicecall__open=True
)
for post in posts: for post in posts:
post.dicecall.open = False post.dicecall.open = False
post.dicecall.save() post.dicecall.save()
@ -385,89 +442,18 @@ def open_post(socket, data):
socket.send('open_post', data) socket.send('open_post', data)
def vote(socket, data):
"""
Called when a player votes in a poll.
"""
post_id = data.get('post_id')
option_id = data.get('option_id')
polarity = data.get('polarity')
ip_address = socket.scope['client'][0]
user = socket.scope['user']
if polarity == False:
v = PollVote.objects.get(ip_address=ip_address, option__id=option_id)
v.delete()
else:
p = Poll.objects.get(post_id=post_id)
if p.multi_choice == False:
votes = PollVote.objects.filter(
ip_address=ip_address,
option__poll=p
)
for vote in votes:
vote.delete()
data = {}
data['option_id'] = vote.option.id
data['polarity'] = False
socket.send('vote', data)
socket.self_send('set_option_box', data)
v = PollVote(
option=PollOption.objects.get(id=option_id),
ip_address=ip_address
)
if user.username:
v.user = user
try:
v.save()
except IntegrityError: # shouldn't we check this first?
return
data = {}
data['option_id'] = option_id
data['polarity'] = polarity
socket.send('vote', data)
def write_in(socket, data):
"""
Called when a player creates a new write-in.
"""
post_id = data.get('post_id')
option_text = data.get('option_text', '')
user = socket.scope['user']
option_text = option_text.strip()
if not option_text:
return
option_text = "Write in: " + bleach.clean(option_text)
if len(option_text) > 200:
# error message?
return
p = Poll.objects.get(post_id=post_id)
o = PollOption(
poll=p,
text=option_text
)
o.save()
data = {}
data['post_id'] = post_id
data['post_type'] = 'poll'
data['option_id'] = o.id
data['option_text'] = option_text
socket.send('update_post', data)
def new_page(socket, data): def new_page(socket, data):
""" """
Called when the QM creates a new page. Called when the QM creates a new page.
""" """
quest = Quest.objects.get(id=socket.quest_id)
user = socket.scope['user']
if quest.owner != user:
return # error message?
title = data.get('page_title') title = data.get('page_title')
appendix = bool(data.get('appendix')) appendix = bool(data.get('appendix'))
quest = Quest.objects.get(id=socket.quest_id)
if appendix: if appendix:
page = Page.objects.filter( page = Page.objects.filter(
quest=quest, quest=quest,
@ -517,6 +503,96 @@ def new_page(socket, data):
socket.send('message', data) socket.send('message', data)
def vote(socket, data):
"""
Called when a player votes in a poll.
"""
post_id = data.get('post_id')
option_id = data.get('option_id')
polarity = data.get('polarity')
ip_address = socket.scope['client'][0]
user = socket.scope['user']
if polarity == False:
try:
v = PollVote.objects.get(
ip_address=ip_address,
option__id=option_id
)
except PollVote.DoesNotExist:
return
v.delete()
else:
try:
p = Poll.objects.get(post_id=post_id)
option = PollOption.objects.get(id=option_id)
except (Poll.DoesNotExist, PollOption.DoesNotExist):
return
pvs = PollVote.objects.filter(option=option, ip_address=ip_address)
if pvs.count() != 0:
return
if p.multi_choice == False:
votes = PollVote.objects.filter(
ip_address=ip_address,
option__poll=p
)
for vote in votes:
vote.delete()
data = {}
data['option_id'] = vote.option.id
data['polarity'] = False
socket.send('vote', data)
socket.self_send('set_option_box', data)
pv = PollVote(
option=option,
ip_address=ip_address
)
if user.username:
pv.user = user
pv.save()
data = {}
data['option_id'] = option_id
data['polarity'] = polarity
socket.send('vote', data)
def write_in(socket, data):
"""
Called when a player creates a new write-in.
"""
post_id = data.get('post_id')
option_text = data.get('option_text', '')
user = socket.scope['user']
try:
p = Poll.objects.get(post_id=post_id)
except Poll.DoesNotExist:
return
option_text = option_text.strip()
if not option_text:
return
option_text = "Write in: " + bleach.clean(option_text)
if len(option_text) > 200:
# error message?
return
o = PollOption(
poll=p,
text=option_text
)
o.save()
data = {}
data['post_id'] = post_id
data['post_type'] = 'poll'
data['option_id'] = o.id
data['option_text'] = option_text
socket.send('update_post', data)
events = {} events = {}
for obj in dir(): for obj in dir():
if type(locals()[obj]) == types.FunctionType: if type(locals()[obj]) == types.FunctionType:

View File

@ -21,7 +21,7 @@
{% if request.user == quest.owner %} {% if request.user == quest.owner %}
<span><a href="{{ url('quest:edit_quest', args=[quest_id, page_num]) }}">Edit Quest</a></span> <span><a href="{{ url('quest:edit_quest', args=[quest_id, page_num]) }}">Edit Quest</a></span>
{% endif %} {% endif %}
<span> <span id="pageSelection">
<select onChange="window.location.href=this.value"> <select onChange="window.location.href=this.value">
<optgroup label="Pages"> <optgroup label="Pages">
{% for page in pages %} {% for page in pages %}
@ -37,17 +37,12 @@
{% endif %} {% endif %}
</select> </select>
</span> </span>
{% if quest.live %} <span id="live" style="display:{% if quest.live %}initial{% else %}none{% endif %};">
<span id="live">
LIVE LIVE
</span> </span>
{% else %} <span id="liveIn" style="display:{% if quest.live_time and not quest.live %}initial{% else %}none{% endif %};">
{% if quest.live_time %} Live in: <span id="liveCountdown"></span> (<span id="liveTime">{% if quest.live_time %}{{ localtime(quest.live_time).strftime('%Y-%m-%d %H:%M') }}{% endif %}</span>)
<span id="liveIn">
Live in: <span id="liveCountdown"></span> (<span id="liveTime">{{ localtime(quest.live_time).strftime('%Y-%m-%d %H:%M') }}</span>)
</span> </span>
{% endif %}
{% endif %}
<span id="toggleChat"><a onclick="toggle_chat()" href="javascript:void(0);">{% if request.session.get("hide_chat") == True %}←{% else %}→{% endif %}</a></span> <span id="toggleChat"><a onclick="toggle_chat()" href="javascript:void(0);">{% if request.session.get("hide_chat") == True %}←{% else %}→{% endif %}</a></span>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -147,6 +147,7 @@ socket.events['vote'] = function(data) {
arr = Array.prototype.slice.call(table.rows); arr = Array.prototype.slice.call(table.rows);
arr.sort(sort_by_votes); arr.sort(sort_by_votes);
let new_tbody = document.createElement('tbody'); let new_tbody = document.createElement('tbody');
for (let i = 0; i < arr.length; i++) { for (let i = 0; i < arr.length; i++) {
new_tbody.appendChild(arr[i]); new_tbody.appendChild(arr[i]);
} }
@ -160,6 +161,20 @@ socket.events['new_page'] = function(data) {
let html_str = '<div id="nextPageContainer"><input type="button" id="nextPage" value="Next Page: ' + data.title + '" onclick="window.location.href=\'' + SCRIPT_NAME + data.url + '\'"></div>'; let html_str = '<div id="nextPageContainer"><input type="button" id="nextPage" value="Next Page: ' + data.title + '" onclick="window.location.href=\'' + SCRIPT_NAME + data.url + '\'"></div>';
document.getElementById('questPane').innerHTML = document.getElementById('questPane').innerHTML + html_str; document.getElementById('questPane').innerHTML = document.getElementById('questPane').innerHTML + html_str;
} }
socket.events['live'] = function(data) {
if (data.live) {
document.getElementById('live').style.display = 'initial';
document.getElementById('liveIn').style.display = 'none';
} else if (data.live_time) {
document.getElementById('live').style.display = 'none';
document.getElementById('liveIn').style.display = 'initial';
document.getElementById('liveTime').innerHTML = data.live_time;
live_countdown();
} else {
document.getElementById('live').style.display = 'none';
document.getElementById('liveIn').style.display = 'none';
}
}
/* Websocket send */ /* Websocket send */
function vote(post_id, option_id) { function vote(post_id, option_id) {

View File

@ -4,11 +4,14 @@ Some miscellaneous tools and helper functions. Primarily for quests.
""" """
import os import os
import re import re
import json
import hashlib import hashlib
import magic import magic
import requests import requests
from django.conf import settings from django.conf import settings
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
IMG_DIR = "/usr/local/www/html/img/" IMG_DIR = "/usr/local/www/html/img/"
ALLOWED_MIMES = [ ALLOWED_MIMES = [
@ -69,3 +72,19 @@ def handle_img(text, limit=5):
text = text.replace("[img]" + ext_url + "[/img]", img_tag, 1) text = text.replace("[img]" + ext_url + "[/img]", img_tag, 1)
return text return text
def send_to_websocket(event, quest_id, data={}):
"""
Acts like QuestConsumer.send() but callable from views.
"""
channel_layer = get_channel_layer()
group_name = f'quest_{quest_id}'
data = json.dumps({'event': event, 'data': data})
async_to_sync(channel_layer.group_send)(
group_name,
{
'type': 'dispatch_send',
'message': data
}
)

View File

@ -14,6 +14,7 @@ from django.conf import settings
from .models import Quest, DiceRoll, PollOption, PollVote, Page, Post from .models import Quest, DiceRoll, PollOption, PollVote, Page, Post
from .forms import EditQuestForm, QuestForm, PostForm from .forms import EditQuestForm, QuestForm, PostForm
from user.models import User from user.models import User
from .tools import send_to_websocket
def index(request): def index(request):
"""The quest page index.""" """The quest page index."""
@ -106,6 +107,13 @@ def edit_quest(request, quest_id, page_num='0'):
else: else:
quest.live_time = None quest.live_time = None
quest.save() quest.save()
data = {
'live': quest.live,
'live_time': quest.live_time,
}
if data['live_time']:
data['live_time'] =data['live_time'].strftime('%Y-%m-%d %H:%M')
send_to_websocket('live', quest_id, data)
return redirect('quest:quest',quest_id=quest.id, page_num=page_num) return redirect('quest:quest',quest_id=quest.id, page_num=page_num)
else: else:
messages.error(request, "Error") messages.error(request, "Error")

1
todo
View File

@ -10,7 +10,6 @@ Quote backlinks
Improvements: Improvements:
More options for text posts (lists and so on) More options for text posts (lists and so on)
More rigorous input checking in events.py
Poll vote highlights entire option Poll vote highlights entire option
Total voters per poll Total voters per poll
Chat archives Chat archives