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 .events import events
from .models import Quest
class QuestConsumer(WebsocketConsumer):
"""
@ -25,6 +26,11 @@ class QuestConsumer(WebsocketConsumer):
self.quest_id = self.scope['url_route']['kwargs']['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)(
self.group_name,
self.channel_name

View File

@ -3,8 +3,6 @@
Individual functions for handling WebSocket events. Gets called by the
QuestConsumer object in consumers.py.
"""
# TODO: quest owner only events
# TODO: try/except on database calls
import re
import time
import types
@ -25,12 +23,12 @@ def message(socket, data):
"""
Gets called when the server receives a 'message' event.
"""
# TODO: validation
message = data.get('message')
# cleaning
message = message[:512]
message = message.strip()
message = re.sub(r'\n\n+', '\n\n', message)
if not message:
return
tags = ["b", "code", "i", "s"]
@ -63,6 +61,7 @@ def message(socket, data):
if not reg:
return
try:
# TODO: form validation?
groups = [0 if d is None else int(d) for d in reg.groups()]
dice_num, dice_sides, dice_mod = groups
assert 1 <= dice_num <= 256
@ -149,20 +148,28 @@ def text_post(socket, data):
"""
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')
page_num = data.get('page_num')
try:
page = Page.objects.get(quest=quest, page_num=page_num)
except Page.DoesNotExist:
return
# cleaning
post_text = bleach.clean(post_text.strip())
post_text = post_text.replace("\n", "<br>")
# handle image
quest = Quest.objects.get(id=socket.quest_id)
p = Post(
quest=quest,
page=Page.objects.get(quest=quest, page_num=page_num),
page=page,
post_type='text',
post_text=post_text)
p.save()
@ -194,18 +201,31 @@ def dice_post(socket, data):
"""
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')
try:
page = Page.objects.get(quest=quest, page_num=page_num)
except Page.DoesNotExist:
return
form = DiceCallForm(data)
if not form.is_valid():
return # error message?
form = form.cleaned_data
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:
post.dicecall.open = False
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['diceSides'])
@ -218,10 +238,9 @@ def dice_post(socket, data):
if form['diceChal']:
post_text += " vs DC" + str(form['diceChal'])
quest = Quest.objects.get(id=socket.quest_id)
p = Post(
quest=quest,
page=Page.objects.get(quest=quest, page_num=page_num),
page=page,
post_type='dice',
post_text=post_text
)
@ -263,16 +282,25 @@ def poll_post(socket, data):
"""
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')
form = PollForm(data)
if not form.is_valid():
return # error message?
form = form.cleaned_data
quest=Quest.objects.get(id=socket.quest_id)
p = Post(
quest=quest,
page=Page.objects.get(quest=quest, page_num=page_num),
page=page,
post_type='poll',
post_text="Poll"
)
@ -321,16 +349,25 @@ def edit_post(socket, data):
"""
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_text = data.get('post_text')
try:
p = Post.objects.get(id=post_id)
except Post.DoesNotExist:
return
# cleaning
post_text = bleach.clean(post_text.strip())
post_text = post_text.replace("\n", "<br>")
# handle image
p = Post.objects.get(id=post_id)
p.post_text = post_text
p.save()
@ -345,8 +382,17 @@ def close_post(socket, data):
"""
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')
p = Post.objects.get(id=post_id)
try:
p = Post.objects.get(id=post_id)
except Post.DoesNotExist:
return
if data.get('post_type') == 'dice':
p.dicecall.open = False
p.dicecall.save()
@ -363,12 +409,23 @@ def open_post(socket, data):
"""
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')
p = Post.objects.get(id=post_id)
try:
p = Post.objects.get(id=post_id)
except Post.DoesNotExist:
return
if data.get('post_type') == 'dice':
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:
post.dicecall.open = False
post.dicecall.save()
@ -385,89 +442,18 @@ def open_post(socket, 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):
"""
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')
appendix = bool(data.get('appendix'))
quest = Quest.objects.get(id=socket.quest_id)
if appendix:
page = Page.objects.filter(
quest=quest,
@ -517,6 +503,96 @@ def new_page(socket, 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 = {}
for obj in dir():
if type(locals()[obj]) == types.FunctionType:

View File

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

View File

@ -147,6 +147,7 @@ socket.events['vote'] = function(data) {
arr = Array.prototype.slice.call(table.rows);
arr.sort(sort_by_votes);
let new_tbody = document.createElement('tbody');
for (let i = 0; i < arr.length; 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>';
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 */
function vote(post_id, option_id) {

View File

@ -4,11 +4,14 @@ Some miscellaneous tools and helper functions. Primarily for quests.
"""
import os
import re
import json
import hashlib
import magic
import requests
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/"
ALLOWED_MIMES = [
@ -69,3 +72,19 @@ def handle_img(text, limit=5):
text = text.replace("[img]" + ext_url + "[/img]", img_tag, 1)
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 .forms import EditQuestForm, QuestForm, PostForm
from user.models import User
from .tools import send_to_websocket
def index(request):
"""The quest page index."""
@ -106,6 +107,13 @@ def edit_quest(request, quest_id, page_num='0'):
else:
quest.live_time = None
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)
else:
messages.error(request, "Error")

1
todo
View File

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