diff --git a/addon/admin.py b/addon/admin.py new file mode 100644 index 0000000..7583423 --- /dev/null +++ b/addon/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from addon.models import Post + + +@admin.register(Post) +class PostAdmin(admin.ModelAdmin): + pass diff --git a/addon/models.py b/addon/models.py index 3090b87..9bea5f0 100644 --- a/addon/models.py +++ b/addon/models.py @@ -3,3 +3,6 @@ class Post(models.Model): token = models.CharField(max_length=50) + + def __str__(self): + return self.token diff --git a/boilerplate/urls.py b/boilerplate/urls.py index 6169449..cb4be5c 100644 --- a/boilerplate/urls.py +++ b/boilerplate/urls.py @@ -18,6 +18,7 @@ from django.urls import include, path urlpatterns = [ + path("admin/", admin.site.urls), path("addon/", include("addon.urls")), path("chat/", include("chat.urls")), path("oauth/", include("oauth.urls")), diff --git a/chat/admin.py b/chat/admin.py new file mode 100644 index 0000000..1888efd --- /dev/null +++ b/chat/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from chat.models import Chat + + +@admin.register(Chat) +class ChatAdmin(admin.ModelAdmin): + pass diff --git a/chat/handler.py b/chat/handler.py index d915804..e37f5c2 100644 --- a/chat/handler.py +++ b/chat/handler.py @@ -1,10 +1,85 @@ import abc +from datetime import datetime +from enum import Enum +from typing import Optional from pydantic import BaseModel from chat.models import Chat +class EventType(str, Enum): + UNKNOWN = "UNKNOWN" + NEW_MESSAGE = "NEW_MESSAGE" + NEW_CHATBOT_MESSAGE = "NEW_CHATBOT_MESSAGE" + + +class MessageSenderSide(str, Enum): + SIDE_UNKNOWN = "SIDE_UNKNOWN" + BUYER = "BUYER" + SELLER = "SELLER" + + +class MessageSenderType(str, Enum): + TYPE_UNKNOWN = "TYPE_UNKNOWN" + HUMAN = "HUMAN" + BOT = "BOT" + + +class MessageSender(BaseModel): + side: MessageSenderSide = MessageSenderSide.SIDE_UNKNOWN + type: MessageSenderType = MessageSenderType.TYPE_UNKNOWN + + +class ConversationType(str, Enum): + UNKNOWN = "UNKNOWN" + POST = "POST" + BOT = "BOT" + + +class Conversation(BaseModel): + id: str + type: ConversationType + post_token: Optional[str] = None + + +class MessageType(str, Enum): + UNKNOWN = "UNKNOWN" + TEXT = "TEXT" + + +class Message(BaseModel): + id: str + conversation: Conversation + sender: MessageSender + type: MessageType + sent_at: datetime + text: Optional[str] = None + + +class Event(BaseModel): + type: EventType + new_message: Optional[Message] = None + new_chatbot_message: Optional[Message] = None + metadata: Optional[dict] = None + + +class EventHandler(BaseModel): + def handle_event(self, event: Event): + if event.type == EventType.NEW_MESSAGE: + self.handle_new_message(event) + elif event.type == EventType.NEW_CHATBOT_MESSAGE: + self.handle_new_chatbot_message(event) + else: + raise ValueError(f"Unknown event type: {event.type}") + + def handle_new_message(self, event: Event): + raise NotImplemented + + def handle_new_chatbot_message(self, event: Event): + raise NotImplemented + + class StartChatSessionUser(BaseModel): id: str diff --git a/chat/migrations/0002_chatbot_chatmessage.py b/chat/migrations/0002_chatbot_chatmessage.py new file mode 100644 index 0000000..884aaa7 --- /dev/null +++ b/chat/migrations/0002_chatbot_chatmessage.py @@ -0,0 +1,38 @@ +# Generated by Django 5.0.6 on 2025-03-09 07:44 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='ChatBot', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('user_id', models.CharField(max_length=255, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='ChatMessage', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('sender_side', models.CharField(max_length=255)), + ('sender_type', models.CharField(max_length=255)), + ('type', models.CharField(max_length=255)), + ('message', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('chat', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='chat.chat')), + ('chat_bot', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bot_messages', to='chat.chatbot')), + ], + ), + ] diff --git a/chat/models.py b/chat/models.py index 25fe19e..b6ab9b2 100644 --- a/chat/models.py +++ b/chat/models.py @@ -15,3 +15,24 @@ class Chat(models.Model): class Meta: unique_together = ("post", "user_id", "peer_id") + + +class ChatBot(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + user_id = models.CharField(max_length=255, unique=True, blank=False, null=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + +class ChatMessage(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + chat_bot = models.ForeignKey(ChatBot, on_delete=models.CASCADE, related_name="bot_messages", null=True) + chat = models.ForeignKey(Chat, on_delete=models.CASCADE, related_name="messages", null=True) + + sender_side = models.CharField(max_length=255) + sender_type = models.CharField(max_length=255) + type = models.CharField(max_length=255) + message = models.TextField() + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) diff --git a/chat/static/sample.pdf b/chat/static/sample.pdf new file mode 100644 index 0000000..c01805e Binary files /dev/null and b/chat/static/sample.pdf differ diff --git a/chat/views.py b/chat/views.py index 51b4969..8f76b6b 100644 --- a/chat/views.py +++ b/chat/views.py @@ -14,7 +14,7 @@ from addon.models import Post from boilerplate import settings from boilerplate.clients import get_divar_kenar_client -from chat.handler import ChatNotificationHandler, Notification, StartChatSessionRequest +from chat.handler import * from chat.models import Chat from oauth.models import OAuth from oauth.schemas import OAuthSession, OAuthSessionType @@ -135,43 +135,13 @@ def chat_app(request): @api_view(["POST"]) def receive_notify(request): - signed_authorization = request.headers.get("Authorization") - if not signed_authorization: - return JsonResponse( - { - "status": "403", - } - ) - - try: - chat_id = signer.unsign(signed_authorization) - except signing.BadSignature: - return JsonResponse( - { - "status": "403", - } - ) - - try: - chat = Chat.objects.get(id=chat_id) - except Chat.DoesNotExist: - return JsonResponse( - { - "status": "404", - } - ) + auth_header = request.headers.get("Authorization") - try: - notification = Notification(**json.loads(request.body)) - handler = ChatNotificationHandler(chat=chat) - handler.handle(notification) - except Exception as e: - logger.error(e) - return JsonResponse( - { - "status": "500", - } - ) + if not auth_header or auth_header != settings.DIVAR_IDENTIFICATION_KEY: + return HttpResponseForbidden("Unauthorized access") + event = Event(**json.loads(request.body)) + handler = EventHandler() + handler.handle_event(event) return JsonResponse( { diff --git a/oauth/admin.py b/oauth/admin.py new file mode 100644 index 0000000..fef0ccb --- /dev/null +++ b/oauth/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from oauth.models import OAuth + + +@admin.register(OAuth) +class OAuthAdmin(admin.ModelAdmin): + pass diff --git a/oauth/models.py b/oauth/models.py index 49b145f..164165d 100644 --- a/oauth/models.py +++ b/oauth/models.py @@ -20,3 +20,6 @@ class OAuth(models.Model): def is_expired(self): return now() >= self.expires_in + + def __str__(self): + return self.session_id diff --git a/oauth/views.py b/oauth/views.py index f64181d..a3f8858 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -9,7 +9,7 @@ from django.shortcuts import redirect from django.urls import reverse from django.utils import timezone -from kenar.chatmessage import SetNotifyChatPostConversationsRequest +from kenar import RegisterEventSubscriptionRequest from rest_framework.decorators import api_view from addon.models import Post @@ -71,18 +71,12 @@ def oauth_callback(request): oauth.save() logger.error(access_token_response.access_token) - logger.error(SetNotifyChatPostConversationsRequest( - post_token=chat.post.token, - endpoint=settings.APP_BASE_URL + reverse("receive_notify"), - identification_key=signer.sign(str(chat.id)), - ),) - kenar_client.chat.set_notify_chat_post_conversations( + kenar_client.events.register_event_subscription( access_token=access_token_response.access_token, - data=SetNotifyChatPostConversationsRequest( - post_token=chat.post.token, - endpoint=settings.APP_BASE_URL + reverse("receive_notify"), - identification_key=signer.sign(str(chat.id)), + data=RegisterEventSubscriptionRequest( + event_type=RegisterEventSubscriptionRequest.EventType.NEW_MESSAGE_ON_POST, + event_resource_id=chat.post.token, ), ) base_url = reverse("chat_app") diff --git a/requirements.txt b/requirements.txt index 6a8601d..4ebde18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Django==5.0.10 djangorestframework==3.15.2 psycopg2-binary==2.9.9 -kenar==0.5.* pydantic==2.7.4 httpx==0.27.0 +kenar