diff --git a/backend/social/chat/consumers.py b/backend/social/chat/consumers.py index 3fa4c01..1b61ebb 100644 --- a/backend/social/chat/consumers.py +++ b/backend/social/chat/consumers.py @@ -9,19 +9,34 @@ from channels.generic.websocket import AsyncWebsocketConsumer class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): + user = self.scope["user"] + + if not user.is_authenticated: + await self.close(code=4401) # unauthorized + return + await self.accept() async def disconnect(self, close_code): + self.disconnect() pass - async def receive(self, text_data): - text_data_json = json.loads(text_data) - message = text_data_json["message"] + async def receive(self, data): + if data["type"] == "chat_message": + pass + else: + self.close(reason="Unsupported message type") + + # -- CUSTOM METHODS -- + + async def send_message_to_chat_group(self, event): + message = event["message"] + await create_new_message() await self.send(text_data=json.dumps({"message": message})) @database_sync_to_async -def get_user_profile(user_id): - return UserProfile.objects.get(pk=user_id) \ No newline at end of file +def create_new_message(user_id): + return None \ No newline at end of file diff --git a/backend/social/chat/models.py b/backend/social/chat/models.py index 71a8362..f8e3b79 100644 --- a/backend/social/chat/models.py +++ b/backend/social/chat/models.py @@ -1,3 +1,178 @@ from django.db import models +from django.conf import settings +from django.core.exceptions import ValidationError +from django.utils import timezone -# Create your models here. +class Chat(models.Model): + owner = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + related_name='owned_chats' + ) + + members = models.ManyToManyField( + settings.AUTH_USER_MODEL, + related_name='chats', + blank=True + ) + + moderators = models.ManyToManyField( + settings.AUTH_USER_MODEL, + related_name='moderated_chats', + blank=True + ) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def save(self, *args, **kwargs): + is_new = self._state.adding + super().save(*args, **kwargs) + + # LOGIC: Ensure owner is always a member and moderator + if is_new and self.owner: + self.members.add(self.owner) + self.moderators.add(self.owner) + + def __str__(self): + return f"Chat {self.id}" + + +class Message(models.Model): + chat = models.ForeignKey(Chat, related_name='messages', on_delete=models.CASCADE) + + sender = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='sent_messages' + ) + + content = models.TextField(blank=True) + + # --- TRACKING EDIT STATUS --- + # We add these so the frontend doesn't need to check MessageHistory table + is_edited = models.BooleanField(default=False) + edited_at = models.DateTimeField(null=True, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def clean(self): + # VALIDATION: Ensure sender is actually in the chat + # Note: We check self.id to avoid running this on creation if logic depends on M2M + # But generally, a sender must be a member. + if self.chat and self.sender: + if not self.chat.members.filter(id=self.sender.id).exists(): + raise ValidationError("Sender is not a member of this chat.") + + def save(self, *args, **kwargs): + # Optional: Run validation before saving + # self.full_clean() + super().save(*args, **kwargs) + + # --- HELPER METHODS FOR WEBSOCKETS / VIEWS --- + + def edit_content(self, new_text): + """ + Handles the complex logic of editing: + 1. Checks if text actually changed. + 2. Saves old text to History. + 3. Updates current text and timestamps. + """ + if self.content == new_text: + return False # No change happened + + # 1. Save History + MessageHistory.objects.create( + message=self, + old_content=self.content + ) + + # 2. Update Self + self.content = new_text + self.is_edited = True + self.edited_at = timezone.now() + self.save() + return True + + def toggle_reaction(self, user, emoji): + """ + Handles Add/Remove/Switch logic. + Returns a tuple: (action, reaction_object) + action can be: 'added', 'removed', 'switched' + """ + try: + reaction = MessageReaction.objects.get(message=self, user=user) + + if reaction.emoji == emoji: + # Same emoji -> Remove it (Toggle) + reaction.delete() + return 'removed', None + else: + # Different emoji -> Switch it + reaction.emoji = emoji + reaction.save() + return 'switched', reaction + + except MessageReaction.DoesNotExist: + # New reaction -> Create it + reaction = MessageReaction.objects.create( + message=self, + user=user, + emoji=emoji + ) + return 'added', reaction + + def __str__(self): + return f"Message {self.id} from {self.sender}" + + +class MessageHistory(models.Model): + message = models.ForeignKey( + Message, + on_delete=models.CASCADE, + related_name='edit_history' + ) + old_content = models.TextField() + archived_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-archived_at'] + + +class MessageReaction(models.Model): + message = models.ForeignKey( + Message, + on_delete=models.CASCADE, + related_name='reactions' + ) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + emoji = models.CharField(max_length=10) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('message', 'user') + + def __str__(self): + return f"{self.user} reacted {self.emoji}" + + +class MessageFile(models.Model): + message = models.ForeignKey( + Message, + on_delete=models.CASCADE, + related_name='media_files' + ) + file = models.FileField(upload_to='chat_uploads/%Y/%m/%d/') + + media_type = models.CharField(max_length=20, choices=[ + ('IMAGE', 'Image'), + ('VIDEO', 'Video'), + ('FILE', 'File') + ], default='FILE') + + uploaded_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Media {self.id} for Message {self.message.id}" \ No newline at end of file diff --git a/backend/social/pages/__init__.py b/backend/social/pages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/social/pages/admin.py b/backend/social/pages/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/social/pages/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/social/pages/apps.py b/backend/social/pages/apps.py new file mode 100644 index 0000000..cdd024b --- /dev/null +++ b/backend/social/pages/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PagesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'pages' diff --git a/backend/social/pages/migrations/__init__.py b/backend/social/pages/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/social/pages/models.py b/backend/social/pages/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/social/pages/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/social/pages/tests.py b/backend/social/pages/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/social/pages/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/social/pages/views.py b/backend/social/pages/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/social/pages/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/social/posts/__init__.py b/backend/social/posts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/social/posts/admin.py b/backend/social/posts/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/social/posts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/social/posts/apps.py b/backend/social/posts/apps.py new file mode 100644 index 0000000..b18ed0d --- /dev/null +++ b/backend/social/posts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PostsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'posts' diff --git a/backend/social/posts/migrations/__init__.py b/backend/social/posts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/social/posts/models.py b/backend/social/posts/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/social/posts/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/social/posts/tests.py b/backend/social/posts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/social/posts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/social/posts/views.py b/backend/social/posts/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/social/posts/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.