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.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.exceptions import PermissionDenied
from django.shortcuts import render
from django.forms.models import model_to_dict
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 .models import Message, Profile
import json
def format_messages(messages):
msg_list = [{
'time': msg.time,
'author': msg.author,
'body': msg.body,
'image': gravatar(msg.author, 32),
'id': msg.id,
'css_class': msg.author.css_class,
} for msg in messages]
return json.dumps(msg_list, default=str)
class ChatView(ListView):
context_object_name = 'message'
template_name = 'chat/chat.html'
title = _('Chat Box')
paginate_by = 50
messages = Message.objects.filter(hidden=False)
paginator = Paginator(messages, paginate_by)
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():
all_admin = Profile.objects.filter(display_rank='admin')
@ -35,63 +128,10 @@ def get_admin_online_status():
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):
return Message.objects.filter(hidden=False)
def get(self, request, *args, **kwargs):
page = request.GET.get('page')
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)
@login_required
def online_status_ajax(request):
return render(request, 'chat/online_status.html', {
'online_users': get_user_online_status(),
'admin_status': get_admin_online_status(),
})

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',
'django_jinja',
'chat_box',
'channels',
'newsletter',
)
@ -513,17 +512,6 @@ FILE_UPLOAD_PERMISSIONS = 0o644
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
# 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.urls import include, url
from django.contrib import admin
@ -372,8 +372,10 @@ urlpatterns = [
url(r'^$',
login_required(ChatView.as_view()),
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/',

View file

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

View file

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

View file

@ -4,90 +4,30 @@
{% block title %} {{_('Chat Box')}} {% endblock %}
{% 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="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS_HTML"></script>
<script type="text/javascript" src="{{ static('event.js') }}"></script>
<script type="text/javascript">
$(function() {
let currentPage = 1;
window.currentPage = 1;
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) {
let data = JSON.parse(e.data);
data = data['message'];
loadMessage(data['body'],
data['author'],
data['time'],
data['id'],
data['image'],
data['css_class'],
true);
// console.log(data);
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
};
function encodeHTML(content) {
return content.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
return '&#'+i.charCodeAt(0)+';';
});
container.scrollTop(scrollTopOfBottom(container) - lastMsgPos);
}, 500);
})
}
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) {
// if (isNew) content = encodeHTML(content)
time = new Date(time);
if (datesAreOnSameDay(time, new Date())) {
time = moment(time).format("HH:mm");
}
else {
time = moment(time).format("HH:mm DD-MM-YYYY");
}
content = encodeHTML(content);
li = `<li class="message">
<img src="${image}" class="profile-pic">
<div class="body-message">
<div class="user-time">
<span class="${css_class}">
<a href="{{ url('user_page') }}/${user}">
${user}
</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()
@ -99,40 +39,71 @@
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)
}
})
load_page(currentPage);
}
}
})}
function remove_day_if_today() {
$('.message_date').each(function() {
sent_date = $(this).html()
console.log(sent_date);
if (sent_date === "{{today}}") {
$(this).hide();
}
})
}
window.load_dynamic_update = function (last_msg) {
return new EventReceiver(
"{{ EVENT_DAEMON_LOCATION }}", "{{ EVENT_DAEMON_POLL_LOCATION }}",
['chat'], last_msg, function (message) {
switch (message.type) {
case 'new_message':
add_new_message(message.message);
break;
}
}
);
}
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'))
{% if request.user.is_staff %}
$(document).on("click", ".chatbtn_remove_mess", function() {
var elt = $(this);
$.ajax({
url: 'delete/',
url: "{{ url('delete_chat_message') }}",
type: 'post',
data: elt.data(),
data: {
message: elt.attr('value'),
},
dataType: 'json',
success: function(data){
elt.closest('li').hide();
@ -147,24 +118,22 @@
$("#chat-submit").click(function() {
if ($("#chat-input").val().trim()) {
let body = $('#chat-input').val().trim();
let img = '{{ gravatar(request.user, 32) }}'
let img = '{{ gravatar(request.user, 32) }}';
message = {
'body': body,
}
chatSocket.send(JSON.stringify({
'message': message
}));
body: body,
};
$.post("{{ url('post_chat_message') }}", message)
.fail(function(res) {
console.log('Fail to send message');
})
.done(function(res, status) {
$('#chat-input').val('').focus();
})
}
});
chatSocket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
};
$("#chat-log").change(function() {
$('#chat-log').scrollTop($('#chat-log')[0].scrollHeight);
});
@ -196,9 +165,6 @@
return true
});
});
$(document).ready(function () {
$('.chat-right-panel').hide();
$('#chat-tab').find('a').click(function (e) {
e.preventDefault();
@ -214,6 +180,25 @@
$('.chat-left-panel').hide();
$('.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>
@ -224,6 +209,16 @@
#content {
margin-top: -0.5em;
}
#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 %}
@ -244,6 +239,7 @@
<div id="chat-box">
<img src="http://opengraphicdesign.com/wp-content/uploads/2009/01/loader64.gif" id="loader">
<ul id="chat-log">
{% include 'chat/message_list.html' %}
</ul>
</div>
<textarea id="chat-input" placeholder="{{_('Enter your message')}}"></textarea>
@ -252,33 +248,10 @@
<div id="chat-online" class="chat-right-panel sidebox">
<h3>
{{_('Online Users')}}
<i class="fa fa-wifi"></i>
<button id="refresh-button" class="fa fa-rotate-right" title="{{_('Refresh')}}"></button>
</h3>
<ul id="chat-online-content">
<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 %}
{% include "chat/online_status.html" %}
</ul>
</div>
</div>

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 %}