diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | chat/ai.py | 18 | ||||
| -rw-r--r-- | chat/chat_cache.py | 43 | ||||
| -rw-r--r-- | chat/consumers.py | 63 | ||||
| -rw-r--r-- | requirements.txt | 4 | ||||
| -rw-r--r-- | static/js/chat.js | 11 | ||||
| -rw-r--r-- | thatcomputerscientist/settings.py | 24 | ||||
| -rw-r--r-- | users/forms.py | 24 |
8 files changed, 155 insertions, 34 deletions
@@ -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 |
