From 79dad7afe21d65c2c2b4491eb0c03d7ccdc3204f Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi <90936742+Reckless-Satoshi@users.noreply.github.com> Date: Sun, 16 Oct 2022 21:11:48 +0000 Subject: [PATCH] Add Chat endpoint to API v0 (#288) * Add /api/chat route and GET method * Add message POST method * Wrap /api/chat GET in /api/order GET * Add send channel message on POST request * Fix OAS schema bug --- api/serializers.py | 2 + api/urls.py | 2 + api/views.py | 9 +- chat/consumers.py | 1 + chat/serializers.py | 26 ++++++ chat/views.py | 206 ++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 237 insertions(+), 9 deletions(-) create mode 100644 chat/serializers.py diff --git a/api/serializers.py b/api/serializers.py index 478c0fad..b4b3aff5 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -448,6 +448,8 @@ class OrderPublicSerializer(serializers.ModelSerializer): "maker_status", "price", "escrow_duration", + "satoshis_now", + "bond_size" ) diff --git a/api/urls.py b/api/urls.py index ba35eaed..2bfb32b9 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,6 +1,7 @@ from django.urls import path from .views import MakerView, OrderView, UserView, BookView, InfoView, RewardView, PriceView, LimitView, HistoricalView, TickView, StealthView from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView +from chat.views import ChatView urlpatterns = [ path('schema/', SpectacularAPIView.as_view(), name='schema'), @@ -20,4 +21,5 @@ urlpatterns = [ path("historical/", HistoricalView.as_view()), path("ticks/", TickView.as_view()), path("stealth/", StealthView.as_view()), + path("chat/", ChatView.as_view({"get": "get","post":"post"})), ] diff --git a/api/views.py b/api/views.py index 7f1b4c83..6fe4f3d1 100644 --- a/api/views.py +++ b/api/views.py @@ -8,14 +8,12 @@ from rest_framework.exceptions import bad_request from rest_framework.generics import CreateAPIView, ListAPIView, UpdateAPIView from rest_framework.views import APIView from rest_framework.response import Response -import textwrap from django.contrib.auth import authenticate, login, logout -from django.utils.decorators import method_decorator -from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.models import User from api.oas_schemas import BookViewSchema, HistoricalViewSchema, InfoViewSchema, LimitViewSchema, MakerViewSchema, OrderViewSchema, PriceViewSchema, RewardViewSchema, StealthViewSchema, TickViewSchema, UserViewSchema +from chat.views import ChatView from api.serializers import InfoSerializer, ListOrderSerializer, MakeOrderSerializer, OrderPublicSerializer, UpdateOrderSerializer, ClaimRewardSerializer, PriceSerializer, UserGenSerializer, TickSerializer, StealthSerializer from api.models import LNPayment, MarketTick, OnchainPayment, Order, Currency, Profile from control.models import AccountingDay, BalanceLog @@ -28,7 +26,6 @@ from .nick_generator.nick_generator import NickGenerator from robohash import Robohash from scipy.stats import entropy from math import log2 -import numpy as np import hashlib from pathlib import Path from datetime import timedelta, datetime @@ -370,6 +367,10 @@ class OrderView(viewsets.ViewSet): data["asked_for_cancel"] = True else: data["asked_for_cancel"] = False + + offset = request.GET.get('offset', None) + if offset: + data["chat"] = ChatView.get(None, request).data # 9) If status is 'DIS' and all HTLCS are in LOCKED elif order.status == Order.Status.DIS: diff --git a/chat/consumers.py b/chat/consumers.py index 83570c28..66c238a7 100644 --- a/chat/consumers.py +++ b/chat/consumers.py @@ -2,6 +2,7 @@ from channels.generic.websocket import AsyncWebsocketConsumer from channels.db import database_sync_to_async from api.models import Order from chat.models import ChatRoom, Message +from asgiref.sync import async_to_sync import json diff --git a/chat/serializers.py b/chat/serializers.py new file mode 100644 index 00000000..22faac71 --- /dev/null +++ b/chat/serializers.py @@ -0,0 +1,26 @@ +from rest_framework import serializers +from chat.models import Message + +class ChatSerializer(serializers.ModelSerializer): + + class Meta: + model = Message + fields = ( + "index", + "sender", + "PGP_message", + "created_at", + ) + depth = 0 + +class PostMessageSerializer(serializers.ModelSerializer): + class Meta: + model = Message + fields = ("PGP_message","order","offset") + depth = 0 + + offset = serializers.IntegerField(allow_null=True, + default=None, + required=False, + min_value=0, + help_text="Offset for message index to get as response") \ No newline at end of file diff --git a/chat/views.py b/chat/views.py index 920bd54f..af7ef7ea 100644 --- a/chat/views.py +++ b/chat/views.py @@ -1,6 +1,202 @@ -from django.shortcuts import render +from operator import index +from rest_framework import status, viewsets +from chat.serializers import ChatSerializer, PostMessageSerializer +from chat.models import Message, ChatRoom +from api.models import Order, User +from rest_framework.response import Response +from datetime import timedelta +from django.utils import timezone -# def room(request, order_id): -# return render(request, 'chatroom.html', { -# 'order_id': order_id -# }) +from asgiref.sync import async_to_sync +from channels.layers import get_channel_layer + +class ChatView(viewsets.ViewSet): + serializer_class = PostMessageSerializer + lookup_url_kwarg = ["order_id","offset"] + + queryset = Message.objects.filter(order__status__in=[Order.Status.CHA, Order.Status.FSE]) + + def get(self, request, format=None): + """ + Returns chat messages for an order with an index higher than `offset`. + """ + + order_id = request.GET.get("order_id", None) + offset = request.GET.get("offset", 0) + + if order_id is None: + return Response( + { + "bad_request": + "Order ID does not exist" + }, + status.HTTP_400_BAD_REQUEST, + ) + + order = Order.objects.get(id=order_id) + + if not (request.user == order.maker or request.user == order.taker): + return Response( + { + "bad_request": + "You are not participant in this order" + }, + status.HTTP_400_BAD_REQUEST, + ) + + if not order.status in [Order.Status.CHA, Order.Status.FSE]: + return Response( + { + "bad_request": + "Order is not in chat status" + }, + status.HTTP_400_BAD_REQUEST, + ) + + queryset = Message.objects.filter(order=order, index__gt=offset) + chatroom = ChatRoom.objects.get(order=order) + + # Poor idea: is_peer_connected() mockup. Update connection status based on last time a GET request was sent + if chatroom.maker == request.user: + chatroom.taker_connected = order.taker_last_seen > (timezone.now() - timedelta(minutes=1)) + chatroom.maker_connected = True + chatroom.save() + peer_connected = chatroom.taker_connected + elif chatroom.taker == request.user: + chatroom.maker_connected = order.maker_last_seen > (timezone.now() - timedelta(minutes=1)) + chatroom.taker_connected = True + chatroom.save() + peer_connected = chatroom.maker_connected + + + messages = [] + for message in queryset: + d = ChatSerializer(message).data + print(d) + # Re-serialize so the response is identical to the consumer message + data = { + 'index':d['index'], + 'time':d['created_at'], + 'message':d['PGP_message'], + 'nick': User.objects.get(id=d['sender']).username + } + messages.append(data) + + response = {'peer_connected': peer_connected, 'messages':messages} + + return Response(response, status.HTTP_200_OK) + + def post(self, request, format=None): + """ + Adds one new message to the chatroom. + """ + + serializer = self.serializer_class(data=request.data) + # Return bad request if serializer is not valid + if not serializer.is_valid(): + context = {"bad_request": "Invalid serializer"} + return Response(context, status=status.HTTP_400_BAD_REQUEST) + + print(request) + order_id = serializer.data.get("order", None) + + if order_id is None: + return Response( + { + "bad_request": + "Order ID does not exist" + }, + status.HTTP_400_BAD_REQUEST, + ) + + order = Order.objects.get(id=order_id) + + if not (request.user == order.maker or request.user == order.taker): + return Response( + { + "bad_request": + "You are not participant in this order" + }, + status.HTTP_400_BAD_REQUEST, + ) + + if not order.status in [Order.Status.CHA, Order.Status.FSE]: + return Response( + { + "bad_request": + "Order is not in chat status" + }, + status.HTTP_400_BAD_REQUEST, + ) + + if order.maker == request.user: + sender = order.maker + receiver = order.taker + elif order.taker == request.user: + sender = order.taker + receiver = order.maker + + chatroom, _ = ChatRoom.objects.get_or_create( + id=order_id, + order=order, + room_group_name=f"chat_order_{order_id}", + defaults={ + "maker": order.maker, + "maker_connected": order.maker == request.user, + "taker": order.taker, + "taker_connected": order.taker == request.user, + } + ) + + last_index = Message.objects.filter(order=order, chatroom=chatroom).count() + new_message = Message.objects.create( + index=last_index+1, + PGP_message=serializer.data.get("PGP_message"), + order=order, + chatroom=chatroom, + sender=sender, + receiver=receiver, + ) + + # Send websocket message + if chatroom.maker == request.user: + peer_connected = chatroom.taker_connected + elif chatroom.taker == request.user: + peer_connected = chatroom.maker_connected + + channel_layer = get_channel_layer() + async_to_sync(channel_layer.group_send)( + f"chat_order_{order_id}", + { + "type": "PGP_message", + "index": new_message.index, + "message": new_message.PGP_message, + "time": str(new_message.created_at), + "nick": new_message.sender.username, + "peer_connected": peer_connected, + } + ) + + # if offset is given, reply with messages + offset = serializer.data.get("offset", None) + if offset: + queryset = Message.objects.filter(order=order, index__gt=offset) + messages = [] + for message in queryset: + d = ChatSerializer(message).data + print(d) + # Re-serialize so the response is identical to the consumer message + data = { + 'index':d['index'], + 'time':d['created_at'], + 'message':d['PGP_message'], + 'nick': User.objects.get(id=d['sender']).username + } + messages.append(data) + + response = {'peer_connected': peer_connected, 'messages':messages} + else: + response = {} + + return Response(response, status.HTTP_200_OK) + \ No newline at end of file