Change chat channel to node websocket

This commit is contained in:
cuom1999 2021-06-18 22:26:43 -05:00
parent aeda77b924
commit 231687e081
12 changed files with 308 additions and 280 deletions

View file

@ -1,12 +0,0 @@
"""
ASGI entrypoint. Configures Django and then runs the application
defined in the ASGI_APPLICATION setting.
"""
import os
import django
from channels.routing import get_default_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
django.setup()
application = get_default_application()

View file

@ -1,8 +0,0 @@
from django.urls import re_path
from . import consumers
ASGI_APPLICATION = "chat_box.routing.application"
websocket_urlpatterns = [
re_path(r'ws/chat/', consumers.ChatConsumer),
]

View file

@ -1,26 +1,119 @@
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import ListView from django.views.generic import ListView
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.core.exceptions import PermissionDenied
from django.shortcuts import render from django.shortcuts import render
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
from django.utils import timezone from django.utils import timezone
from django.contrib.auth.decorators import login_required
import datetime
from judge import event_poster as event
from judge.jinja2.gravatar import gravatar from judge.jinja2.gravatar import gravatar
from .models import Message, Profile from .models import Message, Profile
import json import json
def format_messages(messages):
msg_list = [{ class ChatView(ListView):
'time': msg.time, context_object_name = 'message'
'author': msg.author, template_name = 'chat/chat.html'
'body': msg.body, title = _('Chat Box')
'image': gravatar(msg.author, 32), paginate_by = 50
'id': msg.id, messages = Message.objects.filter(hidden=False)
'css_class': msg.author.css_class, paginator = Paginator(messages, paginate_by)
} for msg in messages]
return json.dumps(msg_list, default=str) def get_queryset(self):
return self.messages
def get(self, request, *args, **kwargs):
page = request.GET.get('page')
if page == None:
return super().get(request, *args, **kwargs)
cur_page = self.paginator.get_page(page)
return render(request, 'chat/message_list.html', {
'object_list': cur_page.object_list,
})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = self.title
context['last_msg'] = event.last()
context['online_users'] = get_user_online_status()
context['admin_status'] = get_admin_online_status()
context['today'] = timezone.now().strftime("%d-%m-%Y")
return context
def delete_message(request):
ret = {'delete': 'done'}
if request.method == 'GET':
return JsonResponse(ret)
if request.user.is_staff:
try:
messid = int(request.POST.get('message'))
mess = Message.objects.get(id=messid)
except:
return HttpResponseBadRequest()
mess.hidden = True
mess.save()
return JsonResponse(ret)
return JsonResponse(ret)
@login_required
def post_message(request):
ret = {'msg': 'posted'}
if request.method == 'GET':
return JsonResponse(ret)
new_message = Message(author=request.profile,
body=request.POST['body'])
new_message.save()
event.post('chat', {
'type': 'new_message',
'message': new_message.id,
})
return JsonResponse(ret)
@login_required
def chat_message_ajax(request):
if request.method != 'GET':
return HttpResponseBadRequest()
try:
message_id = request.GET['message']
except KeyError:
return HttpResponseBadRequest()
try:
message = Message.objects.filter(hidden=False).get(id=message_id)
except Message.DoesNotExist:
return HttpResponseBadRequest()
return render(request, 'chat/message.html', {
'message': message,
})
def get_user_online_status():
last_five_minutes = timezone.now()-timezone.timedelta(minutes=5)
return Profile.objects \
.filter(display_rank='user',
last_access__gte = last_five_minutes)\
.order_by('-rating')
def get_admin_online_status(): def get_admin_online_status():
all_admin = Profile.objects.filter(display_rank='admin') all_admin = Profile.objects.filter(display_rank='admin')
@ -35,63 +128,10 @@ def get_admin_online_status():
return ret return ret
class ChatView(ListView):
model = Message
context_object_name = 'message'
template_name = 'chat/chat.html'
title = _('Chat Box')
paginate_by = 50
paginator = Paginator(Message.objects.filter(hidden=False), paginate_by)
def get_queryset(self): @login_required
return Message.objects.filter(hidden=False) def online_status_ajax(request):
return render(request, 'chat/online_status.html', {
def get(self, request, *args, **kwargs): 'online_users': get_user_online_status(),
page = request.GET.get('page') 'admin_status': get_admin_online_status(),
if (page == None): })
# return render(request, 'chat/chat.html', {'message': format_messages(Message.objects.all())})
return super().get(request, *args, **kwargs)
cur_page = self.paginator.get_page(page)
return HttpResponse(format_messages(cur_page.object_list))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# hard code, should be fixed later
address = f'{self.request.get_host()}/ws/chat/'
if self.request.is_secure():
context['ws_address'] = f'wss://{address}'
else:
context['ws_address'] = f'ws://{address}'
context['title'] = self.title
last_five_minutes = timezone.now()-timezone.timedelta(minutes=5)
context['online_users'] = Profile.objects \
.filter(display_rank='user',
last_access__gte = last_five_minutes)\
.order_by('-rating')
context['admin_status'] = get_admin_online_status()
return context
def delete_message(request):
ret = {'delete': 'done'}
if request.method == 'GET':
return JsonResponse(ret)
if request.user.is_staff:
messid = int(request.POST.get('messid'))
all_mess = Message.objects.all()
for mess in all_mess:
if mess.id == messid:
mess.hidden = True
mess.save()
new_elt = {'time': mess.time, 'content': mess.body}
ret = new_elt
break
return JsonResponse(ret)
return JsonResponse(ret)

View file

@ -1,12 +0,0 @@
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat_box.routing
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter(
chat_box.routing.websocket_urlpatterns
)
),
})

View file

@ -243,7 +243,6 @@ INSTALLED_APPS += (
'impersonate', 'impersonate',
'django_jinja', 'django_jinja',
'chat_box', 'chat_box',
'channels',
'newsletter', 'newsletter',
) )
@ -513,17 +512,6 @@ FILE_UPLOAD_PERMISSIONS = 0o644
MESSAGES_TO_LOAD = 15 MESSAGES_TO_LOAD = 15
ASGI_APPLICATION = 'dmoj.routing.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('0.0.0.0', 6379)],
},
},
}
NEWSLETTER_CONFIRM_EMAIL = False NEWSLETTER_CONFIRM_EMAIL = False
# Amount of seconds to wait between each email. Here 100ms is used. # Amount of seconds to wait between each email. Here 100ms is used.

View file

@ -1,4 +1,4 @@
from chat_box.views import ChatView, delete_message from chat_box.views import ChatView, delete_message, post_message, chat_message_ajax, online_status_ajax
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
@ -372,8 +372,10 @@ urlpatterns = [
url(r'^$', url(r'^$',
login_required(ChatView.as_view()), login_required(ChatView.as_view()),
name='chat'), name='chat'),
url(r'^delete/$', delete_message, name='delete_message') url(r'^delete/$', delete_message, name='delete_chat_message'),
url(r'^post/$', post_message, name='post_chat_message'),
url(r'^ajax$', chat_message_ajax, name='chat_message_ajax'),
url(r'^online_status/ajax$', online_status_ajax, name='online_status_ajax')
])), ])),
url(r'^notifications/', url(r'^notifications/',

View file

@ -30,9 +30,6 @@ packaging
celery celery
-e git://github.com/DMOJ/ansi2html.git#egg=ansi2html -e git://github.com/DMOJ/ansi2html.git#egg=ansi2html
sqlparse sqlparse
channels==2.4.0
channels-redis==2.4.2
docker
django-newsletter django-newsletter
netaddr netaddr
redis redis

View file

@ -51,11 +51,14 @@
.clear { .clear {
clear: both; clear: both;
} }
.content-message { .content-message {
word-wrap: break-word; word-wrap: break-word;
white-space: pre-line;
} }
.content-message p {
margin: 0;
}
#chat-area { #chat-area {
height: 85vh; height: 85vh;

View file

@ -4,135 +4,106 @@
{% block title %} {{_('Chat Box')}} {% endblock %} {% block title %} {{_('Chat Box')}} {% endblock %}
{% block js_media %} {% block js_media %}
<script type="text/javascript">
// change ws to wss if using HTTPS
var chatSocket = new WebSocket( "{{ws_address}}" );
</script>
<script type="text/javascript" src="{{ static('mathjax_config.js') }}"></script> <script type="text/javascript" src="{{ static('mathjax_config.js') }}"></script>
<script type="text/javascript" <script type="text/javascript" src="{{ static('event.js') }}"></script>
src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS_HTML"></script>
<script type="text/javascript"> <script type="text/javascript">
$(function() { window.currentPage = 1;
let currentPage = 1;
$('#loader').hide(); function load_page(page) {
$.get('?page=' + page)
.fail(function() {
console.log("Fail to load page " + page);
})
.done(function(data) {
setTimeout(function() {
let container = $('#chat-box');
let lastMsgPos = scrollTopOfBottom(container)
$('#loader').hide();
$('#chat-log').prepend(data);
remove_day_if_today();
chatSocket.onmessage = function(e) { container.scrollTop(scrollTopOfBottom(container) - lastMsgPos);
let data = JSON.parse(e.data); }, 500);
data = data['message']; })
loadMessage(data['body'], }
data['author'],
data['time'], function scrollTopOfBottom(container) {
data['id'], return container[0].scrollHeight - container.innerHeight()
data['image'], }
data['css_class'],
true); function scrollContainer(container, loader) {
// console.log(data); container.scroll(function() {
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight); if (container.scrollTop() == 0) {
}; if (currentPage < {{paginator.num_pages}}) {
currentPage++;
function encodeHTML(content) { loader.show();
return content.replace(/[\u00A0-\u9999<>\&]/gim, function(i) { load_page(currentPage);
return '&#'+i.charCodeAt(0)+';'; }
});
} }
const datesAreOnSameDay = (first, second) => })}
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate();
function loadMessage(content, user, time, messid, image, css_class, isNew) { function remove_day_if_today() {
// if (isNew) content = encodeHTML(content) $('.message_date').each(function() {
time = new Date(time); sent_date = $(this).html()
if (datesAreOnSameDay(time, new Date())) { console.log(sent_date);
time = moment(time).format("HH:mm"); if (sent_date === "{{today}}") {
$(this).hide();
} }
else { })
time = moment(time).format("HH:mm DD-MM-YYYY"); }
}
content = encodeHTML(content); window.load_dynamic_update = function (last_msg) {
li = `<li class="message"> return new EventReceiver(
<img src="${image}" class="profile-pic"> "{{ EVENT_DAEMON_LOCATION }}", "{{ EVENT_DAEMON_POLL_LOCATION }}",
<div class="body-message"> ['chat'], last_msg, function (message) {
<div class="user-time"> switch (message.type) {
<span class="${css_class}"> case 'new_message':
<a href="{{ url('user_page') }}/${user}"> add_new_message(message.message);
${user} break;
</a>
</span>
<span class="time">${time}</span>
{% if request.user.is_staff %}
<a class="chatbtn_remove_mess" style="color:red; cursor: pointer;" data-messtime="${time}" data-author="${user}" data-messid="${messid}">Delete</a>
{% endif %}
</div>
<span class="content-message">${content} </span>
</div>
<div class="clear"></div>
</li>`
ul = $('#chat-log')
if (isNew) {
ul.append(li)
}
else {
ul.prepend(li)
}
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
}
(function init_chatlog() {
ul = $('#chat-log')
{% autoescape on %}
{% for msg in message %}
loadMessage("{{msg.body|safe|escapejs}}", `{{msg.author}}`, `{{msg.time}}`, `{{msg.id}}`, `{{gravatar(msg.author, 32)}}`,`{{msg.author.css_class}}`);
{% endfor %}
{% endautoescape %}
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
})()
function scrollTopOfBottom(container) {
return container[0].scrollHeight - container.innerHeight()
}
function scrollContainer(container, loader) {
container.scroll(function() {
if (container.scrollTop() == 0) {
if (currentPage < {{paginator.num_pages}}) {
currentPage++;
loader.show();
$.ajax({
url: `{{request.path}}?page=${currentPage}`,
success: function(data) {
let lastMsg = $('.message:first')
let lastMsgPos = scrollTopOfBottom(container)
data = JSON.parse(data)
setTimeout( () => {
for (msg of data) {
loadMessage(msg.body, msg.author, msg.time, msg.id, msg.image, msg.css_class)
}
loader.hide()
// scroll to last msg
container.scrollTop(
scrollTopOfBottom(container) - lastMsgPos
)
}, 500)
}
})
} }
} }
})} );
}
function add_new_message(message) {
// console.log(message);
$.get({
url: "{{ url('chat_message_ajax') }}",
data: {
message: message,
},
success: function (data) {
$('#chat-log').append($(data));
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight)
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
},
error: function (data) {
if (data.status === 403)
console.log('No right to see: ' + message);
else {
console.log('Could not load chat message:');
console.log(data.responseText);
}
}
});
}
$(function() {
$('#loader').hide();
scrollContainer($('#chat-box'), $('#loader')) scrollContainer($('#chat-box'), $('#loader'))
{% if request.user.is_staff %} {% if request.user.is_staff %}
$(document).on("click", ".chatbtn_remove_mess", function() { $(document).on("click", ".chatbtn_remove_mess", function() {
var elt = $(this); var elt = $(this);
$.ajax({ $.ajax({
url: 'delete/', url: "{{ url('delete_chat_message') }}",
type: 'post', type: 'post',
data: elt.data(), data: {
message: elt.attr('value'),
},
dataType: 'json', dataType: 'json',
success: function(data){ success: function(data){
elt.closest('li').hide(); elt.closest('li').hide();
@ -147,24 +118,22 @@
$("#chat-submit").click(function() { $("#chat-submit").click(function() {
if ($("#chat-input").val().trim()) { if ($("#chat-input").val().trim()) {
let body = $('#chat-input').val().trim(); let body = $('#chat-input').val().trim();
let img = '{{ gravatar(request.user, 32) }}' let img = '{{ gravatar(request.user, 32) }}';
message = { message = {
'body': body, body: body,
} };
chatSocket.send(JSON.stringify({ $.post("{{ url('post_chat_message') }}", message)
'message': message .fail(function(res) {
})); console.log('Fail to send message');
})
$('#chat-input').val('').focus(); .done(function(res, status) {
$('#chat-input').val('').focus();
})
} }
}); });
chatSocket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
};
$("#chat-log").change(function() { $("#chat-log").change(function() {
$('#chat-log').scrollTop($('#chat-log')[0].scrollHeight); $('#chat-log').scrollTop($('#chat-log')[0].scrollHeight);
}); });
@ -196,9 +165,6 @@
return true return true
}); });
});
$(document).ready(function () {
$('.chat-right-panel').hide(); $('.chat-right-panel').hide();
$('#chat-tab').find('a').click(function (e) { $('#chat-tab').find('a').click(function (e) {
e.preventDefault(); e.preventDefault();
@ -214,17 +180,46 @@
$('.chat-left-panel').hide(); $('.chat-left-panel').hide();
$('.chat-right-panel').show(); $('.chat-right-panel').show();
}); });
$('#refresh-button').on('click', function() {
$.get("{{url('online_status_ajax')}}")
.fail(function() {
console.log("Fail to get online status");
})
.done(function(data) {
if (data.status == 403) {
console.log("Fail to retrieve data");
}
else {
$('#chat-online-content').html(data);
}
})
})
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
remove_day_if_today();
load_dynamic_update({{last_msg}});
}); });
</script> </script>
{% endblock js_media %} {% endblock js_media %}
{% block media %} {% block media %}
<style> <style>
#content { #content {
margin-top: -0.5em; margin-top: -0.5em;
} }
</style> #refresh-button {
padding: 0 0.1em 0 0.1em;
margin: 0.1em;
border-radius: 0.1em;
background: goldenrod;
}
#refresh-button:hover {
background: lightgreen;
color: grey !important;
}
</style>
{% endblock media %} {% endblock media %}
{% block body %} {% block body %}
@ -244,6 +239,7 @@
<div id="chat-box"> <div id="chat-box">
<img src="http://opengraphicdesign.com/wp-content/uploads/2009/01/loader64.gif" id="loader"> <img src="http://opengraphicdesign.com/wp-content/uploads/2009/01/loader64.gif" id="loader">
<ul id="chat-log"> <ul id="chat-log">
{% include 'chat/message_list.html' %}
</ul> </ul>
</div> </div>
<textarea id="chat-input" placeholder="{{_('Enter your message')}}"></textarea> <textarea id="chat-input" placeholder="{{_('Enter your message')}}"></textarea>
@ -251,36 +247,13 @@
<button id="chat-submit" style="display:none;"> Send </button> <button id="chat-submit" style="display:none;"> Send </button>
<div id="chat-online" class="chat-right-panel sidebox"> <div id="chat-online" class="chat-right-panel sidebox">
<h3> <h3>
{{_('Online Users')}} {{_('Online Users')}}
<i class="fa fa-wifi"></i> <button id="refresh-button" class="fa fa-rotate-right" title="{{_('Refresh')}}"></button>
</h3> </h3>
<ul id="chat-online-content"> <ul id="chat-online-content">
<h4>{{_('Admins')}}: </h4> {% include "chat/online_status.html" %}
<hr/>
{% for user in admin_status %}
<li style="padding-left: 1em">
{% if user.is_online %}
<span class="green-dot"></span>
{% else %}
<span class="red-dot"></span>
{% endif %}
<span style="padding-left:0.25em">
{{ link_user(user.user) }}
</span>
</li>
{% endfor %}
<h4 style="margin-top:1em;">{{_('Users')}}: </h4>
<hr/>
{% for user in online_users %}
<li style="padding-left: 1em">
<span class="green-dot"></span>
<span style="padding-left:0.25em">
{{ link_user(user.user) }}
</span>
</li>
{% endfor %}
</ul> </ul>
</div> </div>
</div> </div>
{% endblock body %} {% endblock body %}

View file

@ -0,0 +1,25 @@
<li class="message" id="message-{{ message.id }}">
<img src="{{ gravatar(message.author, 32) }}" class="profile-pic">
<div class="body-message">
<div class="user-time">
<span class="username {{ message.author.css_class }}">
<a href="{{ url('user_page', message.author.user.username) }}">
{{message.author}}
</a>
</span>
<span class="time">
{{ message.time|date('TIME_FORMAT') }}
<span class="message_date">{{ message.time|date('d-m-Y') }}</span>
</span>
{% if request.user.is_staff %}
<a class="chatbtn_remove_mess" value="{{message.id}}" style="color:red; cursor: pointer;">
Delete
</a>
{% endif %}
</div>
<span class="content-message">
{{message.body | markdown('comment', MATH_ENGINE)|reference|str|safe }}
</span>
</div>
<div class="clear"></div>
</li>

View file

@ -0,0 +1,8 @@
{% for message in object_list | reverse%}
{% include "chat/message.html" %}
{% endfor %}
{% if REQUIRE_JAX %}
{% include "mathjax-load.html" %}
{% endif %}
{% include "comments/math.html" %}

View file

@ -0,0 +1,24 @@
<h4>{{_('Admins')}}: </h4>
<hr/>
{% for user in admin_status %}
<li style="padding-left: 1em">
{% if user.is_online %}
<span class="green-dot"></span>
{% else %}
<span class="red-dot"></span>
{% endif %}
<span style="padding-left:0.25em">
{{ link_user(user.user) }}
</span>
</li>
{% endfor %}
<h4 style="margin-top:1em;">{{_('Users')}}: </h4>
<hr/>
{% for user in online_users %}
<li style="padding-left: 1em">
<span class="green-dot"></span>
<span style="padding-left:0.25em">
{{ link_user(user.user) }}
</span>
</li>
{% endfor %}