From 7716e32904691e7b3e5a97f5bbbbd2b103de06cc Mon Sep 17 00:00:00 2001 From: sahayana Date: Fri, 3 Nov 2023 21:19:07 +0400 Subject: [PATCH] =?UTF-8?q?=EC=B6=94=EA=B0=80:=20chat=20=EC=95=B1=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- alaltalk/settings/base.py | 6 +- apps/chat/__init__.py | 0 apps/chat/admin.py | 3 + apps/chat/apis/__init__.py | 0 apps/chat/apis/v1/__init__.py | 0 apps/chat/apis/v1/chat_message_router.py | 0 apps/chat/apis/v1/chat_room_router.py | 67 +++++ apps/chat/apis/v1/schemas/__init__.py | 0 .../v1/schemas/chat_message_create_request.py | 7 + .../apis/v1/schemas/chat_message_response.py | 8 + .../v1/schemas/chat_room_create_request.py | 6 + .../apis/v1/schemas/chat_room_response.py | 7 + apps/chat/apps.py | 6 + apps/chat/consumers.py | 92 ++++++ apps/chat/models.py | 32 +++ apps/chat/routing.py | 8 + apps/chat/services/__init__.py | 0 apps/chat/services/chat_service.py | 13 + apps/chat/urls.py | 24 ++ apps/chat/views.py | 272 ++++++++++++++++++ 20 files changed, 549 insertions(+), 2 deletions(-) create mode 100644 apps/chat/__init__.py create mode 100644 apps/chat/admin.py create mode 100644 apps/chat/apis/__init__.py create mode 100644 apps/chat/apis/v1/__init__.py create mode 100644 apps/chat/apis/v1/chat_message_router.py create mode 100644 apps/chat/apis/v1/chat_room_router.py create mode 100644 apps/chat/apis/v1/schemas/__init__.py create mode 100644 apps/chat/apis/v1/schemas/chat_message_create_request.py create mode 100644 apps/chat/apis/v1/schemas/chat_message_response.py create mode 100644 apps/chat/apis/v1/schemas/chat_room_create_request.py create mode 100644 apps/chat/apis/v1/schemas/chat_room_response.py create mode 100644 apps/chat/apps.py create mode 100644 apps/chat/consumers.py create mode 100644 apps/chat/models.py create mode 100644 apps/chat/routing.py create mode 100644 apps/chat/services/__init__.py create mode 100644 apps/chat/services/chat_service.py create mode 100644 apps/chat/urls.py create mode 100644 apps/chat/views.py diff --git a/alaltalk/settings/base.py b/alaltalk/settings/base.py index ba59d11..b69c7ea 100644 --- a/alaltalk/settings/base.py +++ b/alaltalk/settings/base.py @@ -30,8 +30,10 @@ # Application definition INSTALLED_APPS = [ - # "apps.chat", - # "channels", + # chat + "apps.chat", + "channels", + # django "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", diff --git a/apps/chat/__init__.py b/apps/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/admin.py b/apps/chat/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/chat/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/chat/apis/__init__.py b/apps/chat/apis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/apis/v1/__init__.py b/apps/chat/apis/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/apis/v1/chat_message_router.py b/apps/chat/apis/v1/chat_message_router.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/apis/v1/chat_room_router.py b/apps/chat/apis/v1/chat_room_router.py new file mode 100644 index 0000000..06428eb --- /dev/null +++ b/apps/chat/apis/v1/chat_room_router.py @@ -0,0 +1,67 @@ +from django.http import JsonResponse +from ninja import Router +from search.models import Book, News, Shopping, Youtube + +router = Router(tags=["chat_room"]) + + +@router.post("/youtube") +def create_chat_room(request) -> JsonResponse: + youtube_id = request.POST["id"] + friend_youtube = Youtube.objects.get(pk=youtube_id) + friend_youtube.pk = None + friend_youtube.user_id = request.user.id + if Youtube.objects.filter(user=request.user.id, url=friend_youtube.url).exists(): + result = "AlreadyExist" + else: + result = "success" + friend_youtube.save() + data = {"result": result} + return JsonResponse(data) + + +@router.post("/news") +def create_chat_room(request) -> JsonResponse: + news_id = request.POST["id"] + friend_news = News.objects.get(pk=news_id) + friend_news.pk = None + friend_news.user_id = request.user.id + if News.objects.filter(user=request.user.id, link=friend_news.link).exists(): + result = "AlreadyExist" + else: + result = "success" + friend_news.save() + data = {"result": result} + return JsonResponse(data) + + +@router.post("/book") +def create_chat_room(request) -> JsonResponse: + book_id = request.POST["id"] + friend_book = Book.objects.get(pk=book_id) + friend_book.pk = None + friend_book.user_id = request.user.id + if Book.objects.filter(user=request.user.id, link=friend_book.link).exists(): + result = "AlreadyExist" + else: + result = "success" + friend_book.save() + data = {"result": result} + return JsonResponse(data) + + +@router.post("/shopping") +def create_chat_room(request) -> JsonResponse: + shopping_id = request.POST["id"] + friend_shopping = Shopping.objects.get(pk=shopping_id) + friend_shopping.pk = None + friend_shopping.user_id = request.user.id + if Shopping.objects.filter( + user=request.user.id, link=friend_shopping.link + ).exists(): + result = "AlreadyExist" + else: + result = "success" + friend_shopping.save() + data = {"result": result} + return JsonResponse(data) diff --git a/apps/chat/apis/v1/schemas/__init__.py b/apps/chat/apis/v1/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/apis/v1/schemas/chat_message_create_request.py b/apps/chat/apis/v1/schemas/chat_message_create_request.py new file mode 100644 index 0000000..4d73550 --- /dev/null +++ b/apps/chat/apis/v1/schemas/chat_message_create_request.py @@ -0,0 +1,7 @@ +from ninja import Schema + + +class ChatMessageCreateRequest(Schema): + room_id: int + user_id: int + message: str diff --git a/apps/chat/apis/v1/schemas/chat_message_response.py b/apps/chat/apis/v1/schemas/chat_message_response.py new file mode 100644 index 0000000..d02c409 --- /dev/null +++ b/apps/chat/apis/v1/schemas/chat_message_response.py @@ -0,0 +1,8 @@ +from ninja import Schema + + +class ChatMessageCreateRequest(Schema): + id: int + room_id: int + user_id: int + message: str diff --git a/apps/chat/apis/v1/schemas/chat_room_create_request.py b/apps/chat/apis/v1/schemas/chat_room_create_request.py new file mode 100644 index 0000000..4f59c42 --- /dev/null +++ b/apps/chat/apis/v1/schemas/chat_room_create_request.py @@ -0,0 +1,6 @@ +from ninja import Schema + + +class ChatRoomCreateRequest(Schema): + user1_id: int + user2_id: int diff --git a/apps/chat/apis/v1/schemas/chat_room_response.py b/apps/chat/apis/v1/schemas/chat_room_response.py new file mode 100644 index 0000000..7deb9e8 --- /dev/null +++ b/apps/chat/apis/v1/schemas/chat_room_response.py @@ -0,0 +1,7 @@ +from ninja import Schema + + +class ChatRoomResponse(Schema): + room_id: int + user1_id: int + user2_id: int diff --git a/apps/chat/apps.py b/apps/chat/apps.py new file mode 100644 index 0000000..8bb7c3d --- /dev/null +++ b/apps/chat/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ChatConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.chat" diff --git a/apps/chat/consumers.py b/apps/chat/consumers.py new file mode 100644 index 0000000..4033433 --- /dev/null +++ b/apps/chat/consumers.py @@ -0,0 +1,92 @@ +import json + +from asgiref.sync import async_to_sync +from channels.generic.websocket import WebsocketConsumer + +from apps.account.models import CustomUser + +from .models import ChatMessage + + +class ChatConsumer(WebsocketConsumer): + # 이전 메세지 불러오기(소켓시 진입자 구별 불가로 미사용) + def fetch_messages(self, data): + messages = ChatMessage.last_10_messages() + content = {"command": "messages", "messages": self.messages_to_json(messages)} + self.send_chat_message(content) + + # 새로운 메세지 DB에 저장하기 + def new_message(self, data): + user_id = data["from"] + chatroom_id = data["room_id"] + chat_count = ChatMessage.objects.filter(chatroom_id=chatroom_id) + author = CustomUser.objects.filter(id=user_id)[0] + author_id = author.id + message = ChatMessage.objects.create( + author_id=author_id, message=data["message"], chatroom_id=chatroom_id + ) + content = { + "command": "new_message", + "message": self.message_to_json(message), + "chat_count": len(chat_count), + } + return self.send_chat_message(content) + + # DB에서 불러온 이전 메세지를 리스트 형태로 변환 + def messages_to_json(self, messages): + result = [] + for message in messages: + result.append(self.message_to_json(message)) + return result + + # 리스트 형태로 변환된 메세지를 key:value 형태로 반환 + def message_to_json(self, message): + return { + "author": message.author.id, + "message": message.message, + "created_at": str(message.created_at), + "chatroom_id": message.chatroom.id, + } + + # commands에 따라 실행되는 함수를 제어 + commands = {"fetch_messages": fetch_messages, "new_messages": new_message} + + # 웹소켓에 연결 되었을 때의 동작 + def connect(self): + self.room_id = self.scope["url_route"]["kwargs"]["room_id"] + self.room_group_id = "chat_%s" % self.room_id + + # room_id를 통해 그룹에 들어가기 + async_to_sync(self.channel_layer.group_add)( + self.room_group_id, self.channel_name + ) + + self.accept() + + # 웹소켓 연결이 끊어 졌을 때의 동작 + def disconnect(self, close_code): + # 그룹에서 나오기 + async_to_sync(self.channel_layer.group_discard)( + self.room_group_id, self.channel_name + ) + + # commands 를 통해 데이터 받아오기 + def receive(self, text_data): + data = json.loads(text_data) + self.commands[data["command"]](self, data) + + # room_id로 묶인 그룹에 메세지 보내주기 + def send_chat_message(self, message): + async_to_sync(self.channel_layer.group_send)( + self.room_group_id, {"type": "chat_message", "message": message} + ) + + # 메세지는 json이나 바이너리 형태로 전송 + def send_message(self, message): + self.send(text_data=json.dumps(message)) + + # room_id로 묶인 그룹에서 메세지 받기 + # 웹소켓으로 메세지 전달 + def chat_message(self, event): + message = event["message"] + self.send(text_data=json.dumps(message)) diff --git a/apps/chat/models.py b/apps/chat/models.py new file mode 100644 index 0000000..f6838ca --- /dev/null +++ b/apps/chat/models.py @@ -0,0 +1,32 @@ +from django.db import models + +from apps.account.models import CustomUser + + +class ChatRoom(models.Model): + class Meta: + db_table = "chat" + + created_at = models.DateTimeField(auto_now_add=True) + participant1 = models.ForeignKey( + CustomUser, related_name="participant1_chatroom", on_delete=models.CASCADE + ) + participant2 = models.ForeignKey( + CustomUser, related_name="participant2_chatroom", on_delete=models.CASCADE + ) + + +class ChatMessage(models.Model): + class Meta: + db_table = "message" + + chatroom = models.ForeignKey(ChatRoom, on_delete=models.CASCADE) + author = models.ForeignKey(CustomUser, on_delete=models.CASCADE) + message = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.author + + def last_10_messages(): + return ChatMessage.objects.order_by("-created_at").all()[:10] diff --git a/apps/chat/routing.py b/apps/chat/routing.py new file mode 100644 index 0000000..11eb348 --- /dev/null +++ b/apps/chat/routing.py @@ -0,0 +1,8 @@ +# 유저를 대신하여 consumers와 연결해주는 router 생성 +from django.urls import re_path + +from apps.chat import consumers + +websocket_urlpatterns = [ + re_path(r"ws/chat/room/(?P\w+)/$", consumers.ChatConsumer.as_asgi()), +] diff --git a/apps/chat/services/__init__.py b/apps/chat/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/services/chat_service.py b/apps/chat/services/chat_service.py new file mode 100644 index 0000000..515fa6f --- /dev/null +++ b/apps/chat/services/chat_service.py @@ -0,0 +1,13 @@ +from chat.models import ChatRoom + + +def create_an_chat_room(user1_id: int, user2_id: int) -> ChatRoom: + return ChatRoom.objects.create(user1_id=user1_id, user2_id=user2_id) + + +def get_an_chat_room(room_id: int, user_id: int) -> ChatRoom: + return ChatRoom.objects.filter(user_id=user_id).get(id=room_id) + + +def delete_an_chat_room(room_id: int) -> None: + ChatRoom.objects.filter(id=room_id).delete() diff --git a/apps/chat/urls.py b/apps/chat/urls.py new file mode 100644 index 0000000..35eea3e --- /dev/null +++ b/apps/chat/urls.py @@ -0,0 +1,24 @@ +from chat import views +from django.urls import path + +app_name = "chat" + +urlpatterns = [ + path("", views.show_chat_list, name="show_chat_list"), + path("/", views.create_chat_room, name="create_chat_room"), + path( + "room//", + views.post_data_to_chat_room, + name="post_data_to_chat_room", + ), + path("chatlog/", views.chat_log_send, name="chat_log_send"), + path("morelist/", views.more_list, name="more_list"), + path("messageloader/", views.message_loader, name="massage_loader"), + path( + "latestmessagenotconnected/", + views.latest_message_not_connected, + name="latest_message_not_connected", + ), + path("getroomid/", views.get_room_id, name="get_room_id"), + path("delete//", views.delete_chat_room, name="delete_chat_room"), +] diff --git a/apps/chat/views.py b/apps/chat/views.py new file mode 100644 index 0000000..874fbd6 --- /dev/null +++ b/apps/chat/views.py @@ -0,0 +1,272 @@ +import json + +from accounts.models import CustomUser +from accounts.utils import LoginConfirm +from chat.models import ChatMessage, ChatRoom +from django.contrib.auth.decorators import login_required +from django.db.models import Q +from django.http import HttpResponse +from django.shortcuts import redirect, render +from django.utils.safestring import mark_safe +from django.views.decorators.csrf import csrf_exempt +from search.models import Book, News, Shopping, Youtube + + +# ChatRoom 모델에서 유저가 속해있는 채팅방 리스트 불러오기 +@LoginConfirm +def show_chat_list(request): + user = request.user + chatroom_list = ( + ChatRoom.objects.filter(Q(participant1=user) | Q(participant2=user)) + .all() + .order_by("-created_at") + ) + all_message = ChatMessage.objects.all() + return render( + request, + "chat/chat_list.html", + { + "user_id": user.id, + "chatroom_list": chatroom_list, + "all_message": all_message, + }, + ) + + +# 채팅하기 버튼 클릭 시 채팅방 생성 +# 채팅방에 참여하는 유저가 동일할 경우 새로 생성하지 않고 기존의 채팅을 불러옴 +@LoginConfirm +def create_chat_room(request, id): + user = request.user + partner = CustomUser.objects.get(id=id) + + if user == request.user: + exist_room1 = ChatRoom.objects.filter(participant1=user, participant2=partner) + exist_room2 = ChatRoom.objects.filter(participant1=partner, participant2=user) + if exist_room1: + room = ChatRoom.objects.get(participant1=user, participant2=partner) + room_id = room.id + return redirect("/chat/room/" + str(room_id) + "/") + elif exist_room2: + room = ChatRoom.objects.get(participant1=partner, participant2=user) + room_id = room.id + return redirect("/chat/room/" + str(room_id) + "/") + else: + try: + chat_room = ChatRoom.objects.create( + participant1=user, participant2=partner + ) + chat_room.save() + + # ChatRoom에 있는 room id 로 redirect + room = ChatRoom.objects.get(participant1=user, participant2=partner) + room_id = room.id + return redirect("/chat/room/" + str(room_id) + "/") + except exist_room1.DoesNotExist or exist_room1.DoesNotExist: + return render( + request, "chat/chat_room.html", {"error": "존재하지 않거나 사라진 채팅방입니다."} + ) + + else: + return render(request, "chat/chat_room.html", {"error": "접근 불가한 채팅방 입니다."}) + + +# room_id로 채팅방 삭제 +@csrf_exempt +@LoginConfirm +def delete_chat_room(request, room_id): + target_room = ChatRoom.objects.get(id=room_id) + target_room.delete() + return redirect("/chat") + + +# 웹소켓이 실행되면서 열린 html로 데이터 전달 +@csrf_exempt +@LoginConfirm +def post_data_to_chat_room(request, room_id): + if request.method == "GET": + user = request.user + chatroom = ChatRoom.objects.get(id=room_id) + chatroom_list = ( + ChatRoom.objects.filter(Q(participant1=user) | Q(participant2=user)) + .all() + .order_by("-created_at") + ) + all_message = ChatMessage.objects.all() + + if chatroom.participant1.id == user.id: + participant = chatroom.participant2 + participant_like_youtube = Youtube.objects.filter( + user=chatroom.participant2 + ) + participant_like_news = News.objects.filter(user=chatroom.participant2) + participant_like_book = Book.objects.filter(user=chatroom.participant2) + participant_like_shopping = Shopping.objects.filter( + user=chatroom.participant2 + ) + else: + participant = chatroom.participant1 + participant_like_youtube = Youtube.objects.filter( + user=chatroom.participant1 + ) + participant_like_news = News.objects.filter(user=chatroom.participant1) + participant_like_book = Book.objects.filter(user=chatroom.participant1) + participant_like_shopping = Shopping.objects.filter( + user=chatroom.participant1 + ) + + return render( + request, + "chat/chat_room.html", + { + "room_id": mark_safe(json.dumps(room_id)), + "chatroom_list": chatroom_list, + "user_id": mark_safe(json.dumps(request.user.id)), + "participant1": chatroom.participant1, + "participant2": chatroom.participant2, + "participant": participant, + "all_message": all_message, + "participant_like_youtube": participant_like_youtube, + "participant_like_news": participant_like_news, + "participant_like_book": participant_like_book, + "participant_like_shopping": participant_like_shopping, + }, + ) + + +# AI API로 전달할 채팅로그 +@csrf_exempt +@LoginConfirm +def chat_log_send(request): + room_id = json.loads(request.body.decode("utf-8"))["room_id"] + chat_log = [] + sentence = "" + chatroom = ChatRoom.objects.get(id=room_id) + all_chat = ChatMessage.objects.filter(chatroom=chatroom) + + if len(all_chat) > 40: + all_chat = all_chat[len(all_chat) - 40 :] + + for chat in all_chat: + sentence = sentence + chat.message + " " + + chat_log.append(sentence) + + context = {"chat_log": chat_log} + return HttpResponse(json.dumps(context), content_type="application/json") + + +# 채팅방의 이전메세지 더보기(버튼 클릭시 10개씩 로드) +@csrf_exempt +@LoginConfirm +def more_list(request): + room_id = json.loads(request.body.decode("utf-8"))["room_id"] + num = json.loads(request.body.decode("utf-8"))["startNum"] + # user_id = json.loads(request.body.decode("utf-8"))["user_id"] + + all_chat = ChatMessage.objects.filter(chatroom_id=room_id).order_by("created_at") + if all_chat is not None: + all_chat = all_chat[num : num + 10] + if all_chat is None: + all_chat = all_chat[num:] + + chat_list = [] + for chat in all_chat: + chat_list.append( + { + "message": chat.message, + "author_id": chat.author_id, + "created_at": str(chat.created_at), + } + ) + + context = {"chat_list": chat_list} + + return HttpResponse(json.dumps(context), content_type="application/json") + + +# 채팅방 이전메세지 로더 +@csrf_exempt +@LoginConfirm +def message_loader(request): + room_id = json.loads(request.body.decode("utf-8"))["room_id"] + last_messages = ChatMessage.objects.filter(chatroom_id=room_id).order_by( + "created_at" + ) + limit = len(last_messages) - 40 + if len(last_messages) > 40: + last_messages = last_messages[limit:] + + last_messages_list = [] + for chat in last_messages: + last_messages_list.append( + { + "message": chat.message, + "author_id": chat.author_id, + "created_at": str(chat.created_at), + } + ) + + context = {"last_messages_list": last_messages_list} + + return HttpResponse(json.dumps(context), content_type="application/json") + + +# 채팅방 별 최신 메세지 1개 뽑기 +@csrf_exempt +@LoginConfirm +def latest_message_not_connected(request): + partner_list = json.loads(request.body.decode("utf-8"))["partner_list"] + + latest_chat_list = [] + for partner in partner_list: + + chatter = CustomUser.objects.get(id=partner) + chatroom = ChatRoom.objects.get( + Q(participant1=request.user, participant2_id=chatter) + | Q(participant2=request.user, participant1=chatter) + ) + + latest_message_each_chatroom = ChatMessage.objects.filter( + chatroom=chatroom + ).order_by("-created_at") + for message in latest_message_each_chatroom: + if message is latest_message_each_chatroom[0]: + + latest_chat_list.append( + { + "partner": chatter.id, + "author_message": message.author_id, + "latest_message_each_chatroom": message.message, + } + ) + + context = { + "latest_chat_list": latest_chat_list, + } + + return HttpResponse(json.dumps(context), content_type="application/json") + + +# partner_id로 chatroom을 찾아 list 형태로 전달 +@csrf_exempt +@LoginConfirm +def get_room_id(request): + partner_list = json.loads(request.body.decode("utf-8"))["partner_list"] + + room_list = [] + for partner in partner_list: + chatter = CustomUser.objects.get(id=partner) + chatroom = ChatRoom.objects.filter( + Q(participant1=request.user, participant2_id=chatter) + | Q(participant2=request.user, participant1=chatter) + ) + + for room in chatroom: + room_list.append(room.id) + + context = { + "room_list": room_list, + } + + return HttpResponse(json.dumps(context), content_type="application/json")