Notifications Endpoint

This commit is contained in:
KoalaSat
2024-06-07 15:31:22 +02:00
parent 997e9aeacc
commit 3d1b673df8
8 changed files with 207 additions and 63 deletions

View File

@ -6,7 +6,7 @@ from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from api.models import Robot from api.models import Robot
from api.notifications import Telegram from api.notifications import Notifications
from api.utils import get_session from api.utils import get_session
@ -17,7 +17,7 @@ class Command(BaseCommand):
bot_token = config("TELEGRAM_TOKEN") bot_token = config("TELEGRAM_TOKEN")
updates_url = f"https://api.telegram.org/bot{bot_token}/getUpdates" updates_url = f"https://api.telegram.org/bot{bot_token}/getUpdates"
session = get_session() session = get_session()
telegram = Telegram() notifications = Notifications()
def handle(self, *args, **options): def handle(self, *args, **options):
offset = 0 offset = 0
@ -49,7 +49,7 @@ class Command(BaseCommand):
continue continue
parts = message.split(" ") parts = message.split(" ")
if len(parts) < 2: if len(parts) < 2:
self.telegram.send_message( self.notifications.send_telegram_message(
chat_id=result["message"]["from"]["id"], chat_id=result["message"]["from"]["id"],
text='You must enable the notifications bot using the RoboSats client. Click on your "Robot robot" -> "Enable Telegram" and follow the link or scan the QR code.', text='You must enable the notifications bot using the RoboSats client. Click on your "Robot robot" -> "Enable Telegram" and follow the link or scan the QR code.',
) )
@ -57,7 +57,7 @@ class Command(BaseCommand):
token = parts[-1] token = parts[-1]
robot = Robot.objects.filter(telegram_token=token).first() robot = Robot.objects.filter(telegram_token=token).first()
if not robot: if not robot:
self.telegram.send_message( self.notifications.send_telegram_message(
chat_id=result["message"]["from"]["id"], chat_id=result["message"]["from"]["id"],
text=f'Wops, invalid token! There is no Robot with telegram chat token "{token}"', text=f'Wops, invalid token! There is no Robot with telegram chat token "{token}"',
) )
@ -71,7 +71,7 @@ class Command(BaseCommand):
robot.telegram_lang_code = result["message"]["from"][ robot.telegram_lang_code = result["message"]["from"][
"language_code" "language_code"
] ]
self.telegram.welcome(robot.user) self.notifications.welcome(robot.user)
robot.telegram_enabled = True robot.telegram_enabled = True
robot.save( robot.save(
update_fields=[ update_fields=[

View File

@ -0,0 +1,35 @@
# We use custom seeded UUID generation during testing
import uuid
from decouple import config
from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone
if config("TESTING", cast=bool, default=False):
import random
import string
random.seed(1)
chars = string.ascii_lowercase + string.digits
def custom_uuid():
return uuid.uuid5(uuid.NAMESPACE_DNS, "".join(random.choices(chars, k=20)))
else:
custom_uuid = uuid.uuid4
class Notification(models.Model):
# notification info
reference = models.UUIDField(default=custom_uuid, editable=False)
created_at = models.DateTimeField(default=timezone.now)
user = models.ForeignKey(User, on_delete=models.SET_NULL, default=None)
# notification details
title = models.CharField(max_length=120, null=False, default=None)
description = models.CharField(max_length=120, default=None, blank=True)
def __str__(self):
return f"{self.title} {self.description}"

View File

@ -3,10 +3,11 @@ from secrets import token_urlsafe
from decouple import config from decouple import config
from api.models import Order from api.models import Order
from api.models import Notification
from api.utils import get_session from api.utils import get_session
class Telegram: class Notifications:
"""Simple telegram messages using TG's API""" """Simple telegram messages using TG's API"""
session = get_session() session = get_session()
@ -29,13 +30,25 @@ class Telegram:
return context return context
def send_message(self, chat_id, text): def send_message(self, robot, title, description):
"""Save a message for a user and sends it to Telegram"""
self.save_message(robot, title, description)
self.send_telegram_message(robot.telegram_chat_id, title, description)
def save_message(self, robot, title, description):
"""Save a message for a user"""
notification = Notification()
notification.title = title
notification.description = description
notification.user = robot
notification.save()
def send_telegram_message(self, chat_id, title, description):
"""sends a message to a user with telegram notifications enabled""" """sends a message to a user with telegram notifications enabled"""
bot_token = config("TELEGRAM_TOKEN") bot_token = config("TELEGRAM_TOKEN")
text = f"{title} {description}"
message_url = f"https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}&text={text}" message_url = f"https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}&text={text}"
# if it fails, it should keep trying # if it fails, it should keep trying
while True: while True:
try: try:
@ -49,10 +62,10 @@ class Telegram:
lang = user.robot.telegram_lang_code lang = user.robot.telegram_lang_code
if lang == "es": if lang == "es":
text = f"🔔 Hola {user.username}, te enviaré notificaciones sobre tus órdenes en RoboSats." title = f"🔔 Hola {user.username}, te enviaré notificaciones sobre tus órdenes en RoboSats."
else: else:
text = f"🔔 Hey {user.username}, I will send you notifications about your RoboSats orders." title = f"🔔 Hey {user.username}, I will send you notifications about your RoboSats orders."
self.send_message(user.robot.telegram_chat_id, text) self.send_message(user.robot.telegram_chat_id, title)
user.robot.telegram_welcomed = True user.robot.telegram_welcomed = True
user.robot.save(update_fields=["telegram_welcomed"]) user.robot.save(update_fields=["telegram_welcomed"])
return return
@ -61,18 +74,22 @@ class Telegram:
if order.maker.robot.telegram_enabled: if order.maker.robot.telegram_enabled:
lang = order.maker.robot.telegram_lang_code lang = order.maker.robot.telegram_lang_code
if lang == "es": if lang == "es":
text = f"✅ Hey {order.maker.username} ¡Tu orden con ID {order.id} ha sido tomada por {order.taker.username}!🥳 Visita http://{self.site}/order/{order.id} para continuar." title = f"✅ Hey {order.maker.username} ¡Tu orden con ID {order.id} ha sido tomada por {order.taker.username}!🥳"
description = (
f"Visita http://{self.site}/order/{order.id} para continuar."
)
else: else:
text = f"✅ Hey {order.maker.username}, your order was taken by {order.taker.username}!🥳 Visit http://{self.site}/order/{order.id} to proceed with the trade." title = f"✅ Hey {order.maker.username}, your order was taken by {order.taker.username}!🥳"
self.send_message(order.maker.robot.telegram_chat_id, text) description = f"Visit http://{self.site}/order/{order.id} to proceed with the trade."
self.send_message(order.maker.robot.telegram_chat_id, title, description)
if order.taker.robot.telegram_enabled: if order.taker.robot.telegram_enabled:
lang = order.taker.robot.telegram_lang_code lang = order.taker.robot.telegram_lang_code
if lang == "es": if lang == "es":
text = f"✅ Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}." title = f"✅ Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}."
else: else:
text = f"✅ Hey {order.taker.username}, you just took the order with ID {order.id}." title = f"✅ Hey {order.taker.username}, you just took the order with ID {order.id}."
self.send_message(order.taker.robot.telegram_chat_id, text) self.send_message(order.taker.robot.telegram_chat_id, title)
return return
@ -81,20 +98,26 @@ class Telegram:
if user.robot.telegram_enabled: if user.robot.telegram_enabled:
lang = user.robot.telegram_lang_code lang = user.robot.telegram_lang_code
if lang == "es": if lang == "es":
text = f"✅ Hey {user.username}, el depósito de garantía y el recibo del comprador han sido recibidos. Es hora de enviar el dinero fiat. Visita http://{self.site}/order/{order.id} para hablar con tu contraparte." title = f"✅ Hey {user.username}, el depósito de garantía y el recibo del comprador han sido recibidos. Es hora de enviar el dinero fiat."
description = f"Visita http://{self.site}/order/{order.id} para hablar con tu contraparte."
else: else:
text = f"✅ Hey {user.username}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat. Visit http://{self.site}/order/{order.id} to talk with your counterpart." title = f"✅ Hey {user.username}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat."
self.send_message(user.robot.telegram_chat_id, text) description = f"Visit http://{self.site}/order/{order.id} to talk with your counterpart."
self.send_message(user.robot.telegram_chat_id, title, description)
return return
def order_expired_untaken(self, order): def order_expired_untaken(self, order):
if order.maker.robot.telegram_enabled: if order.maker.robot.telegram_enabled:
lang = order.maker.robot.telegram_lang_code lang = order.maker.robot.telegram_lang_code
if lang == "es": if lang == "es":
text = f"😪 Hey {order.maker.username}, tu orden con ID {order.id} ha expirado sin ser tomada por ningún robot. Visita http://{self.site}/order/{order.id} para renovarla." title = f"😪 Hey {order.maker.username}, tu orden con ID {order.id} ha expirado sin ser tomada por ningún robot."
description = (
f"Visita http://{self.site}/order/{order.id} para renovarla."
)
else: else:
text = f"😪 Hey {order.maker.username}, your order with ID {order.id} has expired without a taker. Visit http://{self.site}/order/{order.id} to renew it." title = f"😪 Hey {order.maker.username}, your order with ID {order.id} has expired without a taker."
self.send_message(order.maker.robot.telegram_chat_id, text) description = f"Visit http://{self.site}/order/{order.id} to renew it."
self.send_message(order.maker.robot.telegram_chat_id, title, description)
return return
def trade_successful(self, order): def trade_successful(self, order):
@ -102,20 +125,28 @@ class Telegram:
if user.robot.telegram_enabled: if user.robot.telegram_enabled:
lang = user.robot.telegram_lang_code lang = user.robot.telegram_lang_code
if lang == "es": if lang == "es":
text = f"🥳 ¡Tu orden con ID {order.id} ha finalizado exitosamente!⚡ Únete a nosotros en @robosats_es y ayúdanos a mejorar." title = (
f"🥳 ¡Tu orden con ID {order.id} ha finalizado exitosamente!"
)
description = (
"⚡ Únete a nosotros en @robosats_es y ayúdanos a mejorar."
)
else: else:
text = f"🥳 Your order with ID {order.id} has finished successfully!⚡ Join us @robosats and help us improve." title = (
self.send_message(user.robot.telegram_chat_id, text) f"🥳 Your order with ID {order.id} has finished successfully!"
)
description = "⚡ Join us @robosats and help us improve."
self.send_message(user.robot.telegram_chat_id, title, description)
return return
def public_order_cancelled(self, order): def public_order_cancelled(self, order):
if order.maker.robot.telegram_enabled: if order.maker.robot.telegram_enabled:
lang = order.maker.robot.telegram_lang_code lang = order.maker.robot.telegram_lang_code
if lang == "es": if lang == "es":
text = f"❌ Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}." title = f"❌ Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}."
else: else:
text = f"❌ Hey {order.maker.username}, you have cancelled your public order with ID {order.id}." title = f"❌ Hey {order.maker.username}, you have cancelled your public order with ID {order.id}."
self.send_message(order.maker.robot.telegram_chat_id, text) self.send_message(order.maker.robot.telegram_chat_id, title)
return return
def collaborative_cancelled(self, order): def collaborative_cancelled(self, order):
@ -123,10 +154,10 @@ class Telegram:
if user.robot.telegram_enabled: if user.robot.telegram_enabled:
lang = user.robot.telegram_lang_code lang = user.robot.telegram_lang_code
if lang == "es": if lang == "es":
text = f"❌ Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente." title = f"❌ Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente."
else: else:
text = f"❌ Hey {user.username}, your order with ID {str(order.id)} has been collaboratively cancelled." title = f"❌ Hey {user.username}, your order with ID {str(order.id)} has been collaboratively cancelled."
self.send_message(user.robot.telegram_chat_id, text) self.send_message(user.robot.telegram_chat_id, title)
return return
def dispute_opened(self, order): def dispute_opened(self, order):
@ -134,18 +165,23 @@ class Telegram:
if user.robot.telegram_enabled: if user.robot.telegram_enabled:
lang = user.robot.telegram_lang_code lang = user.robot.telegram_lang_code
if lang == "es": if lang == "es":
text = f"⚖️ Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa." title = f"⚖️ Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa."
else: else:
text = f"⚖️ Hey {user.username}, a dispute has been opened on your order with ID {str(order.id)}." title = f"⚖️ Hey {user.username}, a dispute has been opened on your order with ID {str(order.id)}."
self.send_message(user.robot.telegram_chat_id, text) self.send_message(user.robot.telegram_chat_id, title)
admin_chat_id = config("TELEGRAM_COORDINATOR_CHAT_ID") admin_chat_id = config("TELEGRAM_COORDINATOR_CHAT_ID")
if len(admin_chat_id) == 0: if len(admin_chat_id) == 0:
return return
coordinator_text = f"There is a new dispute opened for the order with ID {str(order.id)}. Visit http://{self.site}/coordinator/api/order/{str(order.id)}/change to proceed." coordinator_text = (
self.send_message(admin_chat_id, coordinator_text) f"There is a new dispute opened for the order with ID {str(order.id)}."
)
coordinator_description = f"Visit http://{self.site}/coordinator/api/order/{str(order.id)}/change to proceed."
self.send_telegram_message(
admin_chat_id, coordinator_text, coordinator_description
)
return return
@ -158,10 +194,10 @@ class Telegram:
return return
order = queryset.last() order = queryset.last()
if lang == "es": if lang == "es":
text = f"✅ Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes." title = f"✅ Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes."
else: else:
text = f"✅ Hey {order.maker.username}, your order with ID {str(order.id)} is public in the order book." title = f"✅ Hey {order.maker.username}, your order with ID {str(order.id)} is public in the order book."
self.send_message(order.maker.robot.telegram_chat_id, text) self.send_message(order.maker.robot.telegram_chat_id, title)
return return
def new_chat_message(self, order, chat_message): def new_chat_message(self, order, chat_message):
@ -190,13 +226,13 @@ class Telegram:
user = chat_message.receiver user = chat_message.receiver
if user.robot.telegram_enabled: if user.robot.telegram_enabled:
text = f"💬 Hey {user.username}, a new chat message in-app was sent to you by {chat_message.sender.username} for order ID {str(order.id)}. {notification_reason}" title = f"💬 Hey {user.username}, a new chat message in-app was sent to you by {chat_message.sender.username} for order ID {str(order.id)}. {notification_reason}"
self.send_message(user.robot.telegram_chat_id, text) self.send_message(user.robot.telegram_chat_id, title)
return return
def coordinator_cancelled(self, order): def coordinator_cancelled(self, order):
if order.maker.robot.telegram_enabled: if order.maker.robot.telegram_enabled:
text = f"🛠️ Your order with ID {order.id} has been cancelled by the coordinator {config('COORDINATOR_ALIAS', cast=str, default='NoAlias')} for the upcoming maintenance stop." title = f"🛠️ Your order with ID {order.id} has been cancelled by the coordinator {config('COORDINATOR_ALIAS', cast=str, default='NoAlias')} for the upcoming maintenance stop."
self.send_message(order.maker.robot.telegram_chat_id, text) self.send_message(order.maker.robot.telegram_chat_id, title)
return return

View File

@ -378,6 +378,21 @@ class BookViewSchema:
} }
class NotificationSchema:
get = {
"summary": "Get robot notifications",
"description": "Get a list of notifications sent to the robot.",
"parameters": [
OpenApiParameter(
name="created_at",
location=OpenApiParameter.QUERY,
description=("Shows notifications created AFTER this date."),
type=str,
),
],
}
class RobotViewSchema: class RobotViewSchema:
get = { get = {
"summary": "Get robot info", "summary": "Get robot info",

View File

@ -2,7 +2,7 @@ from decouple import config
from decimal import Decimal from decimal import Decimal
from rest_framework import serializers from rest_framework import serializers
from .models import MarketTick, Order from .models import MarketTick, Order, Notification
RETRY_TIME = int(config("RETRY_TIME")) RETRY_TIME = int(config("RETRY_TIME"))
@ -490,6 +490,21 @@ class OrderDetailSerializer(serializers.ModelSerializer):
) )
class NotificationSerializer(serializers.ModelSerializer):
title = serializers.CharField(required=True)
description = serializers.CharField(required=False)
class Meta:
model = Notification
fields = ("title", "description")
class ListNotificationSerializer(serializers.ModelSerializer):
class Meta:
model = Notification
fields = ("title", "description")
class OrderPublicSerializer(serializers.ModelSerializer): class OrderPublicSerializer(serializers.ModelSerializer):
maker_nick = serializers.CharField(required=False) maker_nick = serializers.CharField(required=False)
maker_hash_id = serializers.CharField(required=False) maker_hash_id = serializers.CharField(required=False)

View File

@ -267,44 +267,44 @@ def send_notification(order_id=None, chat_message_id=None, message=None):
if not (order.maker.robot.telegram_enabled or taker_enabled): if not (order.maker.robot.telegram_enabled or taker_enabled):
return return
from api.notifications import Telegram from api.notifications import Notifications
telegram = Telegram() notifications = Notifications()
if message == "welcome": if message == "welcome":
telegram.welcome(order) notifications.welcome(order)
elif message == "order_expired_untaken": elif message == "order_expired_untaken":
telegram.order_expired_untaken(order) notifications.order_expired_untaken(order)
elif message == "trade_successful": elif message == "trade_successful":
telegram.trade_successful(order) notifications.trade_successful(order)
elif message == "public_order_cancelled": elif message == "public_order_cancelled":
telegram.public_order_cancelled(order) notifications.public_order_cancelled(order)
elif message == "taker_expired_b4bond": elif message == "taker_expired_b4bond":
telegram.taker_expired_b4bond(order) notifications.taker_expired_b4bond(order)
elif message == "order_published": elif message == "order_published":
telegram.order_published(order) notifications.order_published(order)
elif message == "order_taken_confirmed": elif message == "order_taken_confirmed":
telegram.order_taken_confirmed(order) notifications.order_taken_confirmed(order)
elif message == "fiat_exchange_starts": elif message == "fiat_exchange_starts":
telegram.fiat_exchange_starts(order) notifications.fiat_exchange_starts(order)
elif message == "dispute_opened": elif message == "dispute_opened":
telegram.dispute_opened(order) notifications.dispute_opened(order)
elif message == "collaborative_cancelled": elif message == "collaborative_cancelled":
telegram.collaborative_cancelled(order) notifications.collaborative_cancelled(order)
elif message == "new_chat_message": elif message == "new_chat_message":
telegram.new_chat_message(order, chat_message) notifications.new_chat_message(order, chat_message)
elif message == "coordinator_cancelled": elif message == "coordinator_cancelled":
telegram.coordinator_cancelled(order) notifications.coordinator_cancelled(order)
return return

View File

@ -15,6 +15,7 @@ from .views import (
RobotView, RobotView,
StealthView, StealthView,
TickView, TickView,
NotificationsView,
) )
urlpatterns = [ urlpatterns = [
@ -36,4 +37,5 @@ urlpatterns = [
path("ticks/", TickView.as_view(), name="ticks"), path("ticks/", TickView.as_view(), name="ticks"),
path("stealth/", StealthView.as_view(), name="stealth"), path("stealth/", StealthView.as_view(), name="stealth"),
path("chat/", ChatView.as_view({"get": "get", "post": "post"}), name="chat"), path("chat/", ChatView.as_view({"get": "get", "post": "post"}), name="chat"),
path("notifications/", NotificationsView.as_view(), name="notifications"),
] ]

View File

@ -5,6 +5,8 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q, Sum from django.db.models import Q, Sum
from django.utils import timezone from django.utils import timezone
from django.utils.dateparse import parse_datetime
from django.http import HttpResponseBadRequest
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework import status, viewsets from rest_framework import status, viewsets
from rest_framework.authentication import TokenAuthentication from rest_framework.authentication import TokenAuthentication
@ -14,8 +16,15 @@ from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from api.logics import Logics from api.logics import Logics
from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order from api.models import (
from api.notifications import Telegram Currency,
LNPayment,
MarketTick,
OnchainPayment,
Order,
Notification,
)
from api.notifications import Notifications
from api.oas_schemas import ( from api.oas_schemas import (
BookViewSchema, BookViewSchema,
HistoricalViewSchema, HistoricalViewSchema,
@ -28,6 +37,7 @@ from api.oas_schemas import (
RobotViewSchema, RobotViewSchema,
StealthViewSchema, StealthViewSchema,
TickViewSchema, TickViewSchema,
NotificationSchema,
) )
from api.serializers import ( from api.serializers import (
ClaimRewardSerializer, ClaimRewardSerializer,
@ -39,6 +49,8 @@ from api.serializers import (
StealthSerializer, StealthSerializer,
TickSerializer, TickSerializer,
UpdateOrderSerializer, UpdateOrderSerializer,
NotificationSerializer,
ListNotificationSerializer,
) )
from api.utils import ( from api.utils import (
compute_avg_premium, compute_avg_premium,
@ -659,7 +671,7 @@ class RobotView(APIView):
context["last_login"] = user.last_login context["last_login"] = user.last_login
# Adds/generate telegram token and whether it is enabled # Adds/generate telegram token and whether it is enabled
context = {**context, **Telegram.get_context(user)} context = {**context, **Notifications.get_context(user)}
# return active order or last made order if any # return active order or last made order if any
has_no_active_order, _, order = Logics.validate_already_maker_or_taker( has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
@ -730,6 +742,35 @@ class BookView(ListAPIView):
return Response(book_data, status=status.HTTP_200_OK) return Response(book_data, status=status.HTTP_200_OK)
class NotificationsView(ListAPIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
serializer_class = NotificationSerializer
@extend_schema(**NotificationSchema.get)
def get(self, request, format=None):
user = request.user
queryset = Notification.objects.filter(user=user)
created_at = request.GET.get("created_at")
if created_at:
created_at = parse_datetime(created_at)
if not created_at:
return HttpResponseBadRequest("Invalid date format")
queryset = Order.objects.filter(created_at__gte=created_at)
notification_data = []
for notification in queryset:
data = ListNotificationSerializer(notification).data
data["title"] = str(notification.title)
data["description"] = str(notification.description)
notification_data.append(data)
return Response(notification_data, status=status.HTTP_200_OK)
class InfoView(viewsets.ViewSet): class InfoView(viewsets.ViewSet):
serializer_class = InfoSerializer serializer_class = InfoSerializer