mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 02:46:28 +00:00
Feat: add notifications api endpoint (#1347)
* Notifications Endpoint * Fix tests * CR * Check tests * Fix Tests * Fix Chat * Remove unused notifications * Fix chat * Fix chat * Fix chat
This commit is contained in:
parent
82b5604ecb
commit
1757a9781a
@ -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=[
|
||||||
|
26
api/migrations/0047_notification.py
Normal file
26
api/migrations/0047_notification.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-06-14 18:31
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api', '0046_alter_currency_currency'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Notification',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('title', models.CharField(default=None, max_length=240)),
|
||||||
|
('description', models.CharField(blank=True, default=None, max_length=240)),
|
||||||
|
('order', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='api.order')),
|
||||||
|
('robot', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='api.robot')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -4,5 +4,14 @@ from .market_tick import MarketTick
|
|||||||
from .onchain_payment import OnchainPayment
|
from .onchain_payment import OnchainPayment
|
||||||
from .order import Order
|
from .order import Order
|
||||||
from .robot import Robot
|
from .robot import Robot
|
||||||
|
from .notification import Notification
|
||||||
|
|
||||||
__all__ = ["Currency", "LNPayment", "MarketTick", "OnchainPayment", "Order", "Robot"]
|
__all__ = [
|
||||||
|
"Currency",
|
||||||
|
"LNPayment",
|
||||||
|
"MarketTick",
|
||||||
|
"OnchainPayment",
|
||||||
|
"Order",
|
||||||
|
"Robot",
|
||||||
|
"Notification",
|
||||||
|
]
|
||||||
|
35
api/models/notification.py
Normal file
35
api/models/notification.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# We use custom seeded UUID generation during testing
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from decouple import config
|
||||||
|
from api.models import Order, Robot
|
||||||
|
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
|
||||||
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
robot = models.ForeignKey(Robot, on_delete=models.CASCADE, default=None)
|
||||||
|
order = models.ForeignKey(Order, on_delete=models.CASCADE, default=None)
|
||||||
|
|
||||||
|
# notification details
|
||||||
|
title = models.CharField(max_length=240, null=False, default=None)
|
||||||
|
description = models.CharField(max_length=240, default=None, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.title} {self.description}"
|
@ -1,12 +1,14 @@
|
|||||||
from secrets import token_urlsafe
|
from secrets import token_urlsafe
|
||||||
|
|
||||||
from decouple import config
|
from decouple import config
|
||||||
|
from api.models import (
|
||||||
from api.models import Order
|
Order,
|
||||||
|
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 +31,24 @@ class Telegram:
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def send_message(self, chat_id, text):
|
def send_message(self, order, robot, title, description=""):
|
||||||
|
"""Save a message for a user and sends it to Telegram"""
|
||||||
|
self.save_message(order, robot, title, description)
|
||||||
|
if robot.telegram_enabled:
|
||||||
|
self.send_telegram_message(robot.telegram_chat_id, title, description)
|
||||||
|
|
||||||
|
def save_message(self, order, robot, title, description):
|
||||||
|
"""Save a message for a user"""
|
||||||
|
Notification.objects.create(
|
||||||
|
title=title, description=description, robot=robot, order=order
|
||||||
|
)
|
||||||
|
|
||||||
|
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,108 +62,116 @@ 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_telegram_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
|
||||||
|
|
||||||
def order_taken_confirmed(self, order):
|
def order_taken_confirmed(self, order):
|
||||||
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, order.maker.robot, title, description)
|
||||||
|
|
||||||
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, order.taker.robot, title)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def fiat_exchange_starts(self, order):
|
def fiat_exchange_starts(self, order):
|
||||||
for user in [order.maker, order.taker]:
|
for user in [order.maker, order.taker]:
|
||||||
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(order, user.robot, title, description)
|
||||||
return
|
return
|
||||||
|
|
||||||
def order_expired_untaken(self, order):
|
def order_expired_untaken(self, order):
|
||||||
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, order.maker.robot, title, description)
|
||||||
return
|
return
|
||||||
|
|
||||||
def trade_successful(self, order):
|
def trade_successful(self, order):
|
||||||
for user in [order.maker, order.taker]:
|
for user in [order.maker, order.taker]:
|
||||||
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 = f"🥳 Your order with ID {order.id} has finished successfully!"
|
||||||
self.send_message(user.robot.telegram_chat_id, text)
|
description = "⚡ Join us @robosats and help us improve."
|
||||||
|
self.send_message(order, user.robot, title, description)
|
||||||
return
|
return
|
||||||
|
|
||||||
def public_order_cancelled(self, order):
|
def public_order_cancelled(self, order):
|
||||||
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, order.maker.robot, title)
|
||||||
return
|
return
|
||||||
|
|
||||||
def collaborative_cancelled(self, order):
|
def collaborative_cancelled(self, order):
|
||||||
for user in [order.maker, order.taker]:
|
for user in [order.maker, order.taker]:
|
||||||
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(order, user.robot, title)
|
||||||
return
|
return
|
||||||
|
|
||||||
def dispute_opened(self, order):
|
def dispute_opened(self, order):
|
||||||
for user in [order.maker, order.taker]:
|
for user in [order.maker, order.taker]:
|
||||||
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(order, user.robot, 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
|
||||||
|
|
||||||
def order_published(self, order):
|
def order_published(self, order):
|
||||||
if order.maker.robot.telegram_enabled:
|
|
||||||
lang = order.maker.robot.telegram_lang_code
|
lang = order.maker.robot.telegram_lang_code
|
||||||
# In weird cases the order cannot be found (e.g. it is cancelled)
|
# In weird cases the order cannot be found (e.g. it is cancelled)
|
||||||
queryset = Order.objects.filter(maker=order.maker)
|
queryset = Order.objects.filter(maker=order.maker)
|
||||||
@ -158,10 +179,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, order.maker.robot, title)
|
||||||
return
|
return
|
||||||
|
|
||||||
def new_chat_message(self, order, chat_message):
|
def new_chat_message(self, order, chat_message):
|
||||||
@ -189,14 +210,12 @@ class Telegram:
|
|||||||
notification_reason = f"(You receive this notification because this was the first in-chat message. You will only be notified again if there is a gap bigger than {TIMEGAP} minutes between messages)"
|
notification_reason = f"(You receive this notification because this was the first in-chat message. You will only be notified again if there is a gap bigger than {TIMEGAP} minutes between messages)"
|
||||||
|
|
||||||
user = chat_message.receiver
|
user = chat_message.receiver
|
||||||
if user.robot.telegram_enabled:
|
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)}."
|
||||||
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}"
|
self.send_message(order, user.robot, title, notification_reason)
|
||||||
self.send_message(user.robot.telegram_chat_id, text)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def coordinator_cancelled(self, order):
|
def coordinator_cancelled(self, order):
|
||||||
if order.maker.robot.telegram_enabled:
|
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."
|
||||||
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."
|
self.send_message(order, order.maker.robot, title)
|
||||||
self.send_message(order.maker.robot.telegram_chat_id, text)
|
|
||||||
return
|
return
|
||||||
|
@ -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",
|
||||||
|
@ -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,12 @@ class OrderDetailSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ListNotificationSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Notification
|
||||||
|
fields = ("title", "description", "order_id")
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
32
api/tasks.py
32
api/tasks.py
@ -263,48 +263,44 @@ def send_notification(order_id=None, chat_message_id=None, message=None):
|
|||||||
chat_message = Message.objects.get(id=chat_message_id)
|
chat_message = Message.objects.get(id=chat_message_id)
|
||||||
order = chat_message.order
|
order = chat_message.order
|
||||||
|
|
||||||
taker_enabled = False if order.taker is None else order.taker.robot.telegram_enabled
|
from api.notifications import Notifications
|
||||||
if not (order.maker.robot.telegram_enabled or taker_enabled):
|
|
||||||
return
|
|
||||||
|
|
||||||
from api.notifications import Telegram
|
notifications = Notifications()
|
||||||
|
|
||||||
telegram = Telegram()
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
46
api/views.py
46
api/views.py
@ -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,7 @@ from api.serializers import (
|
|||||||
StealthSerializer,
|
StealthSerializer,
|
||||||
TickSerializer,
|
TickSerializer,
|
||||||
UpdateOrderSerializer,
|
UpdateOrderSerializer,
|
||||||
|
ListNotificationSerializer,
|
||||||
)
|
)
|
||||||
from api.utils import (
|
from api.utils import (
|
||||||
compute_avg_premium,
|
compute_avg_premium,
|
||||||
@ -659,7 +670,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 +741,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 = ListNotificationSerializer
|
||||||
|
|
||||||
|
@extend_schema(**NotificationSchema.get)
|
||||||
|
def get(self, request, format=None):
|
||||||
|
robot = request.user.robot
|
||||||
|
queryset = Notification.objects.filter(robot=robot).order_by("-created_at")
|
||||||
|
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 = queryset.filter(created_at__gte=created_at)
|
||||||
|
|
||||||
|
notification_data = []
|
||||||
|
for notification in queryset:
|
||||||
|
data = self.serializer_class(notification).data
|
||||||
|
data["title"] = str(notification.title)
|
||||||
|
data["description"] = str(notification.description)
|
||||||
|
data["order_id"] = notification.order.id
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@ -284,6 +284,30 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
description: Reason for the failure
|
description: Reason for the failure
|
||||||
description: ''
|
description: ''
|
||||||
|
/api/notifications/:
|
||||||
|
get:
|
||||||
|
operationId: notifications_list
|
||||||
|
description: Get a list of notifications sent to the robot.
|
||||||
|
summary: Get robot notifications
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: created_at
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Shows notifications created AFTER this date.
|
||||||
|
tags:
|
||||||
|
- notifications
|
||||||
|
security:
|
||||||
|
- tokenAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ListNotification'
|
||||||
|
description: ''
|
||||||
/api/order/:
|
/api/order/:
|
||||||
get:
|
get:
|
||||||
operationId: order_retrieve
|
operationId: order_retrieve
|
||||||
@ -1070,6 +1094,20 @@ components:
|
|||||||
- swap_enabled
|
- swap_enabled
|
||||||
- taker_fee
|
- taker_fee
|
||||||
- version
|
- version
|
||||||
|
ListNotification:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
maxLength: 240
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
maxLength: 240
|
||||||
|
order_id:
|
||||||
|
type: integer
|
||||||
|
readOnly: true
|
||||||
|
required:
|
||||||
|
- order_id
|
||||||
ListOrder:
|
ListOrder:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -239,6 +239,16 @@ class TradeTest(BaseAPITestCase):
|
|||||||
self.assertIsNone(data["taker"], "New order's taker is not null")
|
self.assertIsNone(data["taker"], "New order's taker is not null")
|
||||||
self.assert_order_logs(data["id"])
|
self.assert_order_logs(data["id"])
|
||||||
|
|
||||||
|
maker_headers = trade.get_robot_auth(trade.maker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **maker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(
|
||||||
|
len(notifications_data),
|
||||||
|
0,
|
||||||
|
"User has no notification",
|
||||||
|
)
|
||||||
|
|
||||||
def test_make_order_on_blocked_country(self):
|
def test_make_order_on_blocked_country(self):
|
||||||
"""
|
"""
|
||||||
Test the creation of an F2F order on a geoblocked location
|
Test the creation of an F2F order on a geoblocked location
|
||||||
@ -342,6 +352,16 @@ class TradeTest(BaseAPITestCase):
|
|||||||
self.assertIsInstance(public_data["price_now"], float)
|
self.assertIsInstance(public_data["price_now"], float)
|
||||||
self.assertIsInstance(data["satoshis_now"], int)
|
self.assertIsInstance(data["satoshis_now"], int)
|
||||||
|
|
||||||
|
maker_headers = trade.get_robot_auth(trade.maker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **maker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"✅ Hey {data['maker_nick']}, your order with ID {trade.order_id} is public in the order book.",
|
||||||
|
)
|
||||||
|
|
||||||
# Cancel order to avoid leaving pending HTLCs after a successful test
|
# Cancel order to avoid leaving pending HTLCs after a successful test
|
||||||
trade.cancel_order()
|
trade.cancel_order()
|
||||||
|
|
||||||
@ -506,6 +526,25 @@ class TradeTest(BaseAPITestCase):
|
|||||||
self.assertEqual(data["status_message"], Order.Status(Order.Status.CHA).label)
|
self.assertEqual(data["status_message"], Order.Status(Order.Status.CHA).label)
|
||||||
self.assertFalse(data["is_fiat_sent"])
|
self.assertFalse(data["is_fiat_sent"])
|
||||||
|
|
||||||
|
maker_headers = trade.get_robot_auth(trade.maker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **maker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"✅ Hey {data['maker_nick']}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat.",
|
||||||
|
)
|
||||||
|
taker_headers = trade.get_robot_auth(trade.taker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **taker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"✅ Hey {data['taker_nick']}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat.",
|
||||||
|
)
|
||||||
|
|
||||||
# Cancel order to avoid leaving pending HTLCs after a successful test
|
# Cancel order to avoid leaving pending HTLCs after a successful test
|
||||||
trade.cancel_order(trade.maker_index)
|
trade.cancel_order(trade.maker_index)
|
||||||
trade.cancel_order(trade.taker_index)
|
trade.cancel_order(trade.taker_index)
|
||||||
@ -532,6 +571,27 @@ class TradeTest(BaseAPITestCase):
|
|||||||
self.assertEqual(data["status_message"], Order.Status(Order.Status.CHA).label)
|
self.assertEqual(data["status_message"], Order.Status(Order.Status.CHA).label)
|
||||||
self.assertFalse(data["is_fiat_sent"])
|
self.assertFalse(data["is_fiat_sent"])
|
||||||
|
|
||||||
|
maker_headers = trade.get_robot_auth(trade.maker_index)
|
||||||
|
maker_nick = read_file(f"tests/robots/{trade.maker_index}/nickname")
|
||||||
|
response = self.client.get(reverse("notifications"), **maker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"✅ Hey {maker_nick}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat.",
|
||||||
|
)
|
||||||
|
taker_headers = trade.get_robot_auth(trade.taker_index)
|
||||||
|
taker_nick = read_file(f"tests/robots/{trade.taker_index}/nickname")
|
||||||
|
response = self.client.get(reverse("notifications"), **taker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"✅ Hey {taker_nick}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat.",
|
||||||
|
)
|
||||||
|
|
||||||
# Cancel order to avoid leaving pending HTLCs after a successful test
|
# Cancel order to avoid leaving pending HTLCs after a successful test
|
||||||
trade.cancel_order(trade.maker_index)
|
trade.cancel_order(trade.maker_index)
|
||||||
trade.cancel_order(trade.taker_index)
|
trade.cancel_order(trade.taker_index)
|
||||||
@ -595,6 +655,25 @@ class TradeTest(BaseAPITestCase):
|
|||||||
|
|
||||||
self.assert_order_logs(data["id"])
|
self.assert_order_logs(data["id"])
|
||||||
|
|
||||||
|
maker_headers = trade.get_robot_auth(trade.maker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **maker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"🥳 Your order with ID {str(trade.order_id)} has finished successfully!",
|
||||||
|
)
|
||||||
|
taker_headers = trade.get_robot_auth(trade.taker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **taker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"🥳 Your order with ID {str(trade.order_id)} has finished successfully!",
|
||||||
|
)
|
||||||
|
|
||||||
def test_successful_LN(self):
|
def test_successful_LN(self):
|
||||||
"""
|
"""
|
||||||
Tests a trade from order creation until Sats sent to buyer
|
Tests a trade from order creation until Sats sent to buyer
|
||||||
@ -670,6 +749,17 @@ class TradeTest(BaseAPITestCase):
|
|||||||
data["bad_request"], "This order has been cancelled by the maker"
|
data["bad_request"], "This order has been cancelled by the maker"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
maker_headers = trade.get_robot_auth(trade.maker_index)
|
||||||
|
maker_nick = read_file(f"tests/robots/{trade.maker_index}/nickname")
|
||||||
|
response = self.client.get(reverse("notifications"), **maker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"❌ Hey {maker_nick}, you have cancelled your public order with ID {trade.order_id}.",
|
||||||
|
)
|
||||||
|
|
||||||
def test_collaborative_cancel_order_in_chat(self):
|
def test_collaborative_cancel_order_in_chat(self):
|
||||||
"""
|
"""
|
||||||
Tests the collaborative cancellation of an order in the chat state
|
Tests the collaborative cancellation of an order in the chat state
|
||||||
@ -702,6 +792,27 @@ class TradeTest(BaseAPITestCase):
|
|||||||
"This order has been cancelled collaborativelly",
|
"This order has been cancelled collaborativelly",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
maker_headers = trade.get_robot_auth(trade.maker_index)
|
||||||
|
maker_nick = read_file(f"tests/robots/{trade.maker_index}/nickname")
|
||||||
|
response = self.client.get(reverse("notifications"), **maker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"❌ Hey {maker_nick}, your order with ID {trade.order_id} has been collaboratively cancelled.",
|
||||||
|
)
|
||||||
|
taker_headers = trade.get_robot_auth(trade.taker_index)
|
||||||
|
taker_nick = read_file(f"tests/robots/{trade.taker_index}/nickname")
|
||||||
|
response = self.client.get(reverse("notifications"), **taker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"❌ Hey {taker_nick}, your order with ID {trade.order_id} has been collaboratively cancelled.",
|
||||||
|
)
|
||||||
|
|
||||||
def test_created_order_expires(self):
|
def test_created_order_expires(self):
|
||||||
"""
|
"""
|
||||||
Tests the expiration of a public order
|
Tests the expiration of a public order
|
||||||
@ -740,11 +851,7 @@ class TradeTest(BaseAPITestCase):
|
|||||||
"""
|
"""
|
||||||
trade = Trade(self.client)
|
trade = Trade(self.client)
|
||||||
trade.publish_order()
|
trade.publish_order()
|
||||||
|
trade.expire_order()
|
||||||
# Change order expiry to now
|
|
||||||
order = Order.objects.get(id=trade.response.json()["id"])
|
|
||||||
order.expires_at = datetime.now()
|
|
||||||
order.save()
|
|
||||||
|
|
||||||
# Make orders expire
|
# Make orders expire
|
||||||
trade.clean_orders()
|
trade.clean_orders()
|
||||||
@ -767,6 +874,16 @@ class TradeTest(BaseAPITestCase):
|
|||||||
|
|
||||||
self.assert_order_logs(data["id"])
|
self.assert_order_logs(data["id"])
|
||||||
|
|
||||||
|
maker_headers = trade.get_robot_auth(trade.maker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **maker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"😪 Hey {data['maker_nick']}, your order with ID {str(trade.order_id)} has expired without a taker.",
|
||||||
|
)
|
||||||
|
|
||||||
def test_taken_order_expires(self):
|
def test_taken_order_expires(self):
|
||||||
"""
|
"""
|
||||||
Tests the expiration of a public order
|
Tests the expiration of a public order
|
||||||
@ -777,9 +894,7 @@ class TradeTest(BaseAPITestCase):
|
|||||||
trade.lock_taker_bond()
|
trade.lock_taker_bond()
|
||||||
|
|
||||||
# Change order expiry to now
|
# Change order expiry to now
|
||||||
order = Order.objects.get(id=trade.response.json()["id"])
|
trade.expire_order()
|
||||||
order.expires_at = datetime.now()
|
|
||||||
order.save()
|
|
||||||
|
|
||||||
# Make orders expire
|
# Make orders expire
|
||||||
trade.clean_orders()
|
trade.clean_orders()
|
||||||
@ -876,19 +991,41 @@ class TradeTest(BaseAPITestCase):
|
|||||||
self.assertTrue(response.json()["peer_connected"])
|
self.assertTrue(response.json()["peer_connected"])
|
||||||
|
|
||||||
# Post new message as maker
|
# Post new message as maker
|
||||||
body = {"PGP_message": message, "order_id": trade.order_id, "offset": 0}
|
trade.send_chat_message(message, trade.maker_index)
|
||||||
response = self.client.post(path, data=body, **maker_headers)
|
self.assertResponse(trade.response)
|
||||||
|
self.assertEqual(trade.response.status_code, 200)
|
||||||
|
self.assertEqual(trade.response.json()["messages"][0]["message"], message)
|
||||||
|
self.assertTrue(trade.response.json()["peer_connected"])
|
||||||
|
|
||||||
|
taker_headers = trade.get_robot_auth(trade.taker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **taker_headers)
|
||||||
self.assertResponse(response)
|
self.assertResponse(response)
|
||||||
self.assertEqual(response.status_code, 200)
|
notifications_data = list(response.json())
|
||||||
self.assertEqual(response.json()["messages"][0]["message"], message)
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
self.assertTrue(response.json()["peer_connected"])
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"💬 Hey {taker_nick}, a new chat message in-app was sent to you by {maker_nick} for order ID {trade.order_id}.",
|
||||||
|
)
|
||||||
|
|
||||||
# Post new message as taker without offset, so response should not have messages.
|
# Post new message as taker without offset, so response should not have messages.
|
||||||
body = {"PGP_message": message + " 2", "order_id": trade.order_id}
|
trade.send_chat_message(message + " 2", trade.taker_index)
|
||||||
response = self.client.post(path, data=body, **taker_headers)
|
self.assertResponse(trade.response)
|
||||||
|
self.assertEqual(trade.response.status_code, 200)
|
||||||
|
self.assertEqual(trade.response.json()["messages"][0]["message"], message)
|
||||||
|
self.assertEqual(
|
||||||
|
trade.response.json()["messages"][1]["message"], message + " 2"
|
||||||
|
)
|
||||||
|
|
||||||
|
maker_headers = trade.get_robot_auth(trade.maker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **maker_headers)
|
||||||
self.assertResponse(response)
|
self.assertResponse(response)
|
||||||
self.assertEqual(response.status_code, 200)
|
notifications_data = list(response.json())
|
||||||
self.assertEqual(response.json(), {}) # Nothing in the response
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
# Does not receive notification because user is online
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"✅ Hey {maker_nick}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat.",
|
||||||
|
)
|
||||||
|
|
||||||
# Get the two chatroom messages as maker
|
# Get the two chatroom messages as maker
|
||||||
response = self.client.get(path + params, **maker_headers)
|
response = self.client.get(path + params, **maker_headers)
|
||||||
@ -946,6 +1083,25 @@ class TradeTest(BaseAPITestCase):
|
|||||||
|
|
||||||
self.assert_order_logs(data["id"])
|
self.assert_order_logs(data["id"])
|
||||||
|
|
||||||
|
maker_headers = trade.get_robot_auth(trade.maker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **maker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"⚖️ Hey {data['maker_nick']}, a dispute has been opened on your order with ID {str(trade.order_id)}.",
|
||||||
|
)
|
||||||
|
taker_headers = trade.get_robot_auth(trade.taker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **taker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"⚖️ Hey {data['taker_nick']}, a dispute has been opened on your order with ID {str(trade.order_id)}.",
|
||||||
|
)
|
||||||
|
|
||||||
def test_order_expires_after_only_maker_messaged(self):
|
def test_order_expires_after_only_maker_messaged(self):
|
||||||
"""
|
"""
|
||||||
Tests the expiration of an order in chat where taker never messaged
|
Tests the expiration of an order in chat where taker never messaged
|
||||||
@ -988,6 +1144,25 @@ class TradeTest(BaseAPITestCase):
|
|||||||
|
|
||||||
self.assert_order_logs(data["id"])
|
self.assert_order_logs(data["id"])
|
||||||
|
|
||||||
|
maker_headers = trade.get_robot_auth(trade.maker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **maker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"⚖️ Hey {data['maker_nick']}, a dispute has been opened on your order with ID {str(trade.order_id)}.",
|
||||||
|
)
|
||||||
|
taker_headers = trade.get_robot_auth(trade.taker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **taker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"⚖️ Hey {data['taker_nick']}, a dispute has been opened on your order with ID {str(trade.order_id)}.",
|
||||||
|
)
|
||||||
|
|
||||||
def test_withdraw_reward_after_unilateral_cancel(self):
|
def test_withdraw_reward_after_unilateral_cancel(self):
|
||||||
"""
|
"""
|
||||||
Tests withdraw rewards as taker after maker cancels order unilaterally
|
Tests withdraw rewards as taker after maker cancels order unilaterally
|
||||||
@ -1058,6 +1233,25 @@ class TradeTest(BaseAPITestCase):
|
|||||||
|
|
||||||
self.assert_order_logs(data["id"])
|
self.assert_order_logs(data["id"])
|
||||||
|
|
||||||
|
maker_headers = trade.get_robot_auth(trade.maker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **maker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"⚖️ Hey {data['maker_nick']}, a dispute has been opened on your order with ID {str(trade.order_id)}.",
|
||||||
|
)
|
||||||
|
taker_headers = trade.get_robot_auth(trade.taker_index)
|
||||||
|
response = self.client.get(reverse("notifications"), **taker_headers)
|
||||||
|
self.assertResponse(response)
|
||||||
|
notifications_data = list(response.json())
|
||||||
|
self.assertEqual(notifications_data[0]["order_id"], trade.order_id)
|
||||||
|
self.assertEqual(
|
||||||
|
notifications_data[0]["title"],
|
||||||
|
f"⚖️ Hey {data['taker_nick']}, a dispute has been opened on your order with ID {str(trade.order_id)}.",
|
||||||
|
)
|
||||||
|
|
||||||
def test_ticks(self):
|
def test_ticks(self):
|
||||||
"""
|
"""
|
||||||
Tests the historical ticks serving endpoint after creating a contract
|
Tests the historical ticks serving endpoint after creating a contract
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
from datetime import datetime
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from api.management.commands.clean_orders import Command as CleanOrders
|
from api.management.commands.clean_orders import Command as CleanOrders
|
||||||
@ -119,6 +119,14 @@ class Trade:
|
|||||||
body = {"action": "cancel"}
|
body = {"action": "cancel"}
|
||||||
self.response = self.client.post(path + params, body, **headers)
|
self.response = self.client.post(path + params, body, **headers)
|
||||||
|
|
||||||
|
@patch("api.tasks.send_notification.delay", send_notification)
|
||||||
|
def send_chat_message(self, message, robot_index=1):
|
||||||
|
path = reverse("chat")
|
||||||
|
headers = self.get_robot_auth(robot_index)
|
||||||
|
body = {"PGP_message": message, "order_id": self.order_id, "offset": 0}
|
||||||
|
self.response = self.client.post(path, data=body, **headers)
|
||||||
|
|
||||||
|
@patch("api.tasks.send_notification.delay", send_notification)
|
||||||
def pause_order(self, robot_index=1):
|
def pause_order(self, robot_index=1):
|
||||||
path = reverse("order")
|
path = reverse("order")
|
||||||
params = f"?order_id={self.order_id}"
|
params = f"?order_id={self.order_id}"
|
||||||
@ -126,11 +134,13 @@ class Trade:
|
|||||||
body = {"action": "pause"}
|
body = {"action": "pause"}
|
||||||
self.response = self.client.post(path + params, body, **headers)
|
self.response = self.client.post(path + params, body, **headers)
|
||||||
|
|
||||||
|
@patch("api.tasks.send_notification.delay", send_notification)
|
||||||
def follow_hold_invoices(self):
|
def follow_hold_invoices(self):
|
||||||
# A background thread checks every 5 second the status of invoices. We invoke directly during test.
|
# A background thread checks every 5 second the status of invoices. We invoke directly during test.
|
||||||
follower = FollowInvoices()
|
follower = FollowInvoices()
|
||||||
follower.follow_hold_invoices()
|
follower.follow_hold_invoices()
|
||||||
|
|
||||||
|
@patch("api.tasks.send_notification.delay", send_notification)
|
||||||
def clean_orders(self):
|
def clean_orders(self):
|
||||||
# A background thread checks every 5 second order expirations. We invoke directly during test.
|
# A background thread checks every 5 second order expirations. We invoke directly during test.
|
||||||
cleaner = CleanOrders()
|
cleaner = CleanOrders()
|
||||||
@ -160,6 +170,7 @@ class Trade:
|
|||||||
# Get order
|
# Get order
|
||||||
self.get_order()
|
self.get_order()
|
||||||
|
|
||||||
|
@patch("api.tasks.send_notification.delay", send_notification)
|
||||||
def take_order(self):
|
def take_order(self):
|
||||||
path = reverse("order")
|
path = reverse("order")
|
||||||
params = f"?order_id={self.order_id}"
|
params = f"?order_id={self.order_id}"
|
||||||
@ -167,6 +178,7 @@ class Trade:
|
|||||||
body = {"action": "take", "amount": self.take_amount}
|
body = {"action": "take", "amount": self.take_amount}
|
||||||
self.response = self.client.post(path + params, body, **headers)
|
self.response = self.client.post(path + params, body, **headers)
|
||||||
|
|
||||||
|
@patch("api.tasks.send_notification.delay", send_notification)
|
||||||
def lock_taker_bond(self):
|
def lock_taker_bond(self):
|
||||||
# Takers's first order fetch. Should trigger maker bond hold invoice generation.
|
# Takers's first order fetch. Should trigger maker bond hold invoice generation.
|
||||||
self.get_order(self.taker_index)
|
self.get_order(self.taker_index)
|
||||||
@ -181,6 +193,7 @@ class Trade:
|
|||||||
# Get order
|
# Get order
|
||||||
self.get_order(self.taker_index)
|
self.get_order(self.taker_index)
|
||||||
|
|
||||||
|
@patch("api.tasks.send_notification.delay", send_notification)
|
||||||
def lock_escrow(self, robot_index):
|
def lock_escrow(self, robot_index):
|
||||||
# Takers's order fetch. Should trigger trade escrow bond hold invoice generation.
|
# Takers's order fetch. Should trigger trade escrow bond hold invoice generation.
|
||||||
self.get_order(robot_index)
|
self.get_order(robot_index)
|
||||||
@ -195,6 +208,7 @@ class Trade:
|
|||||||
# Get order
|
# Get order
|
||||||
self.get_order()
|
self.get_order()
|
||||||
|
|
||||||
|
@patch("api.tasks.send_notification.delay", send_notification)
|
||||||
def submit_payout_address(self, robot_index=1):
|
def submit_payout_address(self, robot_index=1):
|
||||||
path = reverse("order")
|
path = reverse("order")
|
||||||
params = f"?order_id={self.order_id}"
|
params = f"?order_id={self.order_id}"
|
||||||
@ -213,6 +227,7 @@ class Trade:
|
|||||||
}
|
}
|
||||||
self.response = self.client.post(path + params, body, **headers)
|
self.response = self.client.post(path + params, body, **headers)
|
||||||
|
|
||||||
|
@patch("api.tasks.send_notification.delay", send_notification)
|
||||||
def submit_payout_invoice(self, robot_index=1, routing_budget=0):
|
def submit_payout_invoice(self, robot_index=1, routing_budget=0):
|
||||||
path = reverse("order")
|
path = reverse("order")
|
||||||
params = f"?order_id={self.order_id}"
|
params = f"?order_id={self.order_id}"
|
||||||
@ -234,6 +249,7 @@ class Trade:
|
|||||||
|
|
||||||
self.response = self.client.post(path + params, body, **headers)
|
self.response = self.client.post(path + params, body, **headers)
|
||||||
|
|
||||||
|
@patch("api.tasks.send_notification.delay", send_notification)
|
||||||
def confirm_fiat(self, robot_index=1):
|
def confirm_fiat(self, robot_index=1):
|
||||||
path = reverse("order")
|
path = reverse("order")
|
||||||
params = f"?order_id={self.order_id}"
|
params = f"?order_id={self.order_id}"
|
||||||
@ -241,9 +257,17 @@ class Trade:
|
|||||||
body = {"action": "confirm"}
|
body = {"action": "confirm"}
|
||||||
self.response = self.client.post(path + params, body, **headers)
|
self.response = self.client.post(path + params, body, **headers)
|
||||||
|
|
||||||
|
@patch("api.tasks.send_notification.delay", send_notification)
|
||||||
def undo_confirm_sent(self, robot_index=1):
|
def undo_confirm_sent(self, robot_index=1):
|
||||||
path = reverse("order")
|
path = reverse("order")
|
||||||
params = f"?order_id={self.order_id}"
|
params = f"?order_id={self.order_id}"
|
||||||
headers = self.get_robot_auth(robot_index)
|
headers = self.get_robot_auth(robot_index)
|
||||||
body = {"action": "undo_confirm"}
|
body = {"action": "undo_confirm"}
|
||||||
self.response = self.client.post(path + params, body, **headers)
|
self.response = self.client.post(path + params, body, **headers)
|
||||||
|
|
||||||
|
@patch("api.tasks.send_notification.delay", send_notification)
|
||||||
|
def expire_order(self):
|
||||||
|
# Change order expiry to now
|
||||||
|
order = Order.objects.get(id=self.order_id)
|
||||||
|
order.expires_at = datetime.now()
|
||||||
|
order.save()
|
||||||
|
Loading…
Reference in New Issue
Block a user