aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--chat/ai.py18
-rw-r--r--chat/chat_cache.py43
-rw-r--r--chat/consumers.py63
-rw-r--r--requirements.txt4
-rw-r--r--static/js/chat.js11
-rw-r--r--thatcomputerscientist/settings.py24
-rw-r--r--users/forms.py24
8 files changed, 155 insertions, 34 deletions
diff --git a/.gitignore b/.gitignore
index f027c710..59100861 100644
--- a/.gitignore
+++ b/.gitignore
@@ -138,3 +138,5 @@ staticfiles/
siteshot.png
image_downloader.ipynb
+
+dump.rdb
diff --git a/chat/ai.py b/chat/ai.py
new file mode 100644
index 00000000..494f6357
--- /dev/null
+++ b/chat/ai.py
@@ -0,0 +1,18 @@
+
+import redis
+from .chat_cache import get_user_messages, save_user_messages
+
+r = redis.Redis(host='localhost', port=6379, db=0)
+subsequent_message = "I told you, no one's around. Stop talking to yourself, you weirdo!"
+
+def invokeMFSkippy(message, identifier):
+ save_user_messages(user_identifier=identifier, message={'content': message, 'role': 'user'})
+ user_messages = get_user_messages(user_identifier=identifier)
+ if len(user_messages) == 1:
+ return "Skippy here. No one's around, you lonely rat!"
+ elif len(user_messages) == 2:
+ return "I told you, no one's around. Stop talking to yourself, you weirdo!"
+ elif len(user_messages) == 3:
+ return "I'm not going to respond to you anymore. Not only are you a weirdo, you're also relentlesslly annoying."
+ else:
+ return None \ No newline at end of file
diff --git a/chat/chat_cache.py b/chat/chat_cache.py
new file mode 100644
index 00000000..08cbf178
--- /dev/null
+++ b/chat/chat_cache.py
@@ -0,0 +1,43 @@
+import redis
+import json
+
+r = redis.Redis(host='localhost', port=6379, db=0)
+
+def handle_connect():
+ # increase number of connected users
+ r.set('n_connected_lc_users', max(1, int(r.get('n_connected_lc_users')) + 1))
+ print('There are now {} connected users.'.format(r.get('n_connected_lc_users')))
+
+def handle_disconnect():
+ # decrease number of connected users
+ r.set('n_connected_lc_users', max(0, int(r.get('n_connected_lc_users')) - 1))
+ print('There are now {} connected users.'.format(r.get('n_connected_lc_users')))
+
+def handle_alone_user():
+ if int(r.get('n_connected_lc_users')) == 1:
+ return True
+ else:
+ return False
+
+def save_user_messages(user_identifier, message):
+ # get user_messages from redis
+ user_messages = r.get(user_identifier)
+ if user_messages:
+ user_messages = json.loads(user_messages)
+ else:
+ user_messages = []
+ # append new message
+ user_messages.append(message)
+ # save user_messages to redis
+ r.set(user_identifier, json.dumps(user_messages))
+
+def get_user_messages(user_identifier):
+ # get user_messages from redis
+ user_messages = r.get(user_identifier)
+ if user_messages:
+ return json.loads(user_messages)
+ else:
+ return []
+
+def discard_user_messages(user_identifier):
+ r.delete(user_identifier) \ No newline at end of file
diff --git a/chat/consumers.py b/chat/consumers.py
index be1cf265..5b0e7ddf 100644
--- a/chat/consumers.py
+++ b/chat/consumers.py
@@ -1,40 +1,45 @@
import json
-from channels.generic.websocket import WebsocketConsumer
-from asgiref.sync import async_to_sync
+from channels.generic.websocket import AsyncWebsocketConsumer
+from .chat_cache import handle_connect, handle_disconnect, handle_alone_user, discard_user_messages
+from .ai import invokeMFSkippy
-class ChatConsumer(WebsocketConsumer):
- def connect(self):
+class ChatConsumer(AsyncWebsocketConsumer):
+ async def connect(self):
self.room_group_name = 'chat'
- async_to_sync(self.channel_layer.group_add)(
- self.room_group_name,
- self.channel_name
- )
- self.accept()
+ await self.channel_layer.group_add(self.room_group_name, self.channel_name)
+ await self.accept()
+ handle_connect()
+
+ async def disconnect(self, close_code):
+ # Leave room group
+ await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
+ handle_disconnect()
+ discard_user_messages(user_identifier=self.channel_name)
- def receive(self, text_data):
+ async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
username = text_data_json['username']
- async_to_sync(self.channel_layer.group_send)(
- self.room_group_name,
- {
- 'type': 'chat',
- 'message': message,
- 'username': username
- }
+
+ # Send message to room group
+ await self.channel_layer.group_send(
+ self.room_group_name, {"type": "chat", "message": message, "username": username}
)
+ is_alone_user = handle_alone_user()
+ if is_alone_user:
+ bot_response = invokeMFSkippy(message=message, identifier=self.channel_name)
+ if bot_response:
+ await self.channel_layer.group_send(
+ self.room_group_name, {"type": "chat", "message": bot_response, "username": "Skippy"}
+ )
+ else:
+ discard_user_messages(user_identifier=self.channel_name)
- def chat(self, event):
- message = event['message']
- username = event['username']
- self.send(text_data=json.dumps({
- 'message': message,
- 'username': username
- }))
+ # Receive message from room group
+ async def chat(self, event):
+ message = event["message"]
+ username = event["username"]
- def disconnect(self, close_code):
- async_to_sync(self.channel_layer.group_discard)(
- self.room_group_name,
- self.channel_name
- )
+ # Send message to WebSocket
+ await self.send(text_data=json.dumps({"message": message, "username": username}))
diff --git a/requirements.txt b/requirements.txt
index 4145d5ef..92ee0500 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
-Django
+django
+django-redis
python-dotenv
gunicorn
whitenoise
@@ -17,4 +18,5 @@ python-Levenshtein
selenium
django-haystack
channels
+channels_redis
daphne
diff --git a/static/js/chat.js b/static/js/chat.js
index d5e8640c..411e89a2 100644
--- a/static/js/chat.js
+++ b/static/js/chat.js
@@ -10,6 +10,9 @@ $(document).ready(function(){
case 'disconnect':
messageElement.innerHTML = '<span style="color: #ff9494;">' + "<b>" + username + "</b>: " + message + '</span>';
break;
+ case 'bot':
+ messageElement.innerHTML = '<span style="color: #fbbf49;">' + "<b>" + username + "</b>: " + message + '</span>';
+ break;
case 'default':
messageElement.innerHTML ="<b>" + username + "</b>: " + message;
break;
@@ -33,7 +36,13 @@ $(document).ready(function(){
}
ws.onmessage = function(e) {
var data = JSON.parse(e.data);
- createMessageElement(data['message'].trim(), data['username'], 'default');
+ if(data['username'] == 'Skippy') { // Bot
+ createMessageElement(data['message'].trim(), data['username'], 'bot');
+ } else if (data['username'] == 'System') { // System
+ createMessageElement(data['message'].trim(), data['username'], 'connect');
+ } else { // User
+ createMessageElement(data['message'].trim(), data['username'], 'default');
+ }
}
$('#chatbox-input').on('keyup', function(e) {
diff --git a/thatcomputerscientist/settings.py b/thatcomputerscientist/settings.py
index dbc4f102..aaf8bcbb 100644
--- a/thatcomputerscientist/settings.py
+++ b/thatcomputerscientist/settings.py
@@ -19,6 +19,11 @@ load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
+# set n_connected_lc_users to 0 on startup
+
+import redis
+r = redis.Redis(host='localhost', port=6379, db=0)
+r.set('n_connected_lc_users', 0)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
@@ -112,9 +117,15 @@ TEMPLATES = [
ASGI_APPLICATION = 'thatcomputerscientist.asgi.application'
CHANNEL_LAYERS = {
+ # "default": {
+ # "BACKEND": "channels.layers.InMemoryChannelLayer"
+ # }
"default": {
- "BACKEND": "channels.layers.InMemoryChannelLayer"
- }
+ "BACKEND": "channels_redis.core.RedisChannelLayer",
+ "CONFIG": {
+ "hosts": [("127.0.0.1", 6379)],
+ },
+ },
}
# Database
@@ -146,6 +157,15 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
+CACHES = {
+ "default": {
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://127.0.0.1:6379/1",
+ "OPTIONS": {
+ "CLIENT_CLASS": "django_redis.client.DefaultClient",
+ }
+ }
+}
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
diff --git a/users/forms.py b/users/forms.py
index 50179abd..8ce104b1 100644
--- a/users/forms.py
+++ b/users/forms.py
@@ -9,7 +9,7 @@ from .accountFunctions import store_token
from .mail_send import send_email
from random import choice
from blog.context_processors import avatar_list
-
+import string
class RegisterForm(forms.Form):
username = forms.CharField(label='Username', max_length=30, min_length=4)
email = forms.EmailField(label='Email')
@@ -17,6 +17,23 @@ class RegisterForm(forms.Form):
password2 = forms.CharField(label='Password (again)', widget=forms.PasswordInput, min_length=8)
captcha = forms.CharField(label='Captcha', max_length=6)
expected_captcha = None
+ protected_usernames = [
+ 'admin',
+ 'administrator',
+ 'root',
+ 'thatcomputerscientist',
+ 'skippy',
+ 'system',
+ 'test',
+ 'user',
+ 'webmaster',
+ 'www',
+ 'postmaster',
+ 'hostmaster',
+ 'info',
+ 'support',
+ ]
+ allowed_chars = string.ascii_letters + string.digits
def __init__(self, *args, **kwargs):
if 'expected_captcha' in kwargs:
@@ -37,6 +54,11 @@ class RegisterForm(forms.Form):
raise forms.ValidationError('Captcha does not match.')
if User.objects.filter(username=cleaned_data.get('username')).exists():
raise forms.ValidationError('Username already exists.')
+ if cleaned_data.get('username').lower() in self.protected_usernames:
+ raise forms.ValidationError('Username not allowed. Please choose another.')
+ for char in cleaned_data.get('username'):
+ if char not in self.allowed_chars:
+ raise forms.ValidationError('Username contains invalid characters. Only A-Z, a-z, and 0-9 are allowed.')
if User.objects.filter(email=cleaned_data.get('email')).exists():
raise forms.ValidationError('Email already exists.')
return cleaned_data