Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notifications api endpoint #1347

Merged
merged 10 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions api/management/commands/telegram_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.db import transaction

from api.models import Robot
from api.notifications import Telegram
from api.notifications import Notifications
from api.utils import get_session


Expand All @@ -17,7 +17,7 @@ class Command(BaseCommand):
bot_token = config("TELEGRAM_TOKEN")
updates_url = f"https://api.telegram.org/bot{bot_token}/getUpdates"
session = get_session()
telegram = Telegram()
notifications = Notifications()

def handle(self, *args, **options):
offset = 0
Expand Down Expand Up @@ -49,15 +49,15 @@ def handle(self, *args, **options):
continue
parts = message.split(" ")
if len(parts) < 2:
self.telegram.send_message(
self.notifications.send_telegram_message(
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.',
)
continue
token = parts[-1]
robot = Robot.objects.filter(telegram_token=token).first()
if not robot:
self.telegram.send_message(
self.notifications.send_telegram_message(
chat_id=result["message"]["from"]["id"],
text=f'Wops, invalid token! There is no Robot with telegram chat token "{token}"',
)
Expand All @@ -71,7 +71,7 @@ def handle(self, *args, **options):
robot.telegram_lang_code = result["message"]["from"][
"language_code"
]
self.telegram.welcome(robot.user)
self.notifications.welcome(robot.user)
robot.telegram_enabled = True
robot.save(
update_fields=[
Expand Down
26 changes: 26 additions & 0 deletions api/migrations/0047_notification.py
Original file line number Diff line number Diff line change
@@ -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')),
],
),
]
11 changes: 10 additions & 1 deletion api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,14 @@
from .onchain_payment import OnchainPayment
from .order import Order
from .robot import Robot
from .notification import Notification

__all__ = ["Currency", "LNPayment", "MarketTick", "OnchainPayment", "Order", "Robot"]
__all__ = [
"Currency",
"LNPayment",
"MarketTick",
"OnchainPayment",
"Order",
"Robot",
"Notification",
]
35 changes: 35 additions & 0 deletions api/models/notification.py
Original file line number Diff line number Diff line change
@@ -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}"
189 changes: 104 additions & 85 deletions api/notifications.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from secrets import token_urlsafe

from decouple import config

from api.models import Order
from api.models import (
Order,
Notification,
)
from api.utils import get_session


class Telegram:
class Notifications:
"""Simple telegram messages using TG's API"""

session = get_session()
Expand All @@ -29,13 +31,24 @@ def get_context(user):

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"""

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}"

# if it fails, it should keep trying
while True:
try:
Expand All @@ -49,119 +62,127 @@ def welcome(self, user):
lang = user.robot.telegram_lang_code

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:
text = f"🔔 Hey {user.username}, I will send you notifications about your RoboSats orders."
self.send_message(user.robot.telegram_chat_id, text)
title = f"🔔 Hey {user.username}, I will send you notifications about your RoboSats orders."
self.send_telegram_message(user.robot.telegram_chat_id, title)
user.robot.telegram_welcomed = True
user.robot.save(update_fields=["telegram_welcomed"])
return

def order_taken_confirmed(self, order):
if order.maker.robot.telegram_enabled:
lang = order.maker.robot.telegram_lang_code
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."
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."
self.send_message(order.maker.robot.telegram_chat_id, text)
lang = order.maker.robot.telegram_lang_code
if lang == "es":
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:
title = f"✅ Hey {order.maker.username}, your order was taken by {order.taker.username}!🥳"
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
if lang == "es":
text = f"✅ Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}."
else:
text = f"✅ Hey {order.taker.username}, you just took the order with ID {order.id}."
self.send_message(order.taker.robot.telegram_chat_id, text)
lang = order.taker.robot.telegram_lang_code
if lang == "es":
title = f"✅ Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}."
else:
title = f"✅ Hey {order.taker.username}, you just took the order with ID {order.id}."
self.send_message(order, order.taker.robot, title)

return

def fiat_exchange_starts(self, order):
for user in [order.maker, order.taker]:
if user.robot.telegram_enabled:
lang = user.robot.telegram_lang_code
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."
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."
self.send_message(user.robot.telegram_chat_id, text)
lang = user.robot.telegram_lang_code
if lang == "es":
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:
title = f"✅ Hey {user.username}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat."
description = f"Visit http://{self.site}/order/{order.id} to talk with your counterpart."
self.send_message(order, user.robot, title, description)
return

def order_expired_untaken(self, order):
if order.maker.robot.telegram_enabled:
lang = order.maker.robot.telegram_lang_code
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."
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."
self.send_message(order.maker.robot.telegram_chat_id, text)
lang = order.maker.robot.telegram_lang_code
if lang == "es":
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:
title = f"😪 Hey {order.maker.username}, your order with ID {order.id} has expired without a taker."
description = f"Visit http://{self.site}/order/{order.id} to renew it."
self.send_message(order, order.maker.robot, title, description)
return

def trade_successful(self, order):
for user in [order.maker, order.taker]:
if user.robot.telegram_enabled:
lang = user.robot.telegram_lang_code
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."
else:
text = f"🥳 Your order with ID {order.id} has finished successfully!⚡ Join us @robosats and help us improve."
self.send_message(user.robot.telegram_chat_id, text)
lang = user.robot.telegram_lang_code
if lang == "es":
title = f"🥳 ¡Tu orden con ID {order.id} ha finalizado exitosamente!"
description = (
"⚡ Únete a nosotros en @robosats_es y ayúdanos a mejorar."
)
else:
title = f"🥳 Your order with ID {order.id} has finished successfully!"
description = "⚡ Join us @robosats and help us improve."
self.send_message(order, user.robot, title, description)
return

def public_order_cancelled(self, order):
if order.maker.robot.telegram_enabled:
lang = order.maker.robot.telegram_lang_code
if lang == "es":
text = f"❌ Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}."
else:
text = 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)
lang = order.maker.robot.telegram_lang_code
if lang == "es":
title = f"❌ Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}."
else:
title = f"❌ Hey {order.maker.username}, you have cancelled your public order with ID {order.id}."
self.send_message(order, order.maker.robot, title)
return

def collaborative_cancelled(self, order):
for user in [order.maker, order.taker]:
if user.robot.telegram_enabled:
lang = user.robot.telegram_lang_code
if lang == "es":
text = f"❌ Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente."
else:
text = f"❌ Hey {user.username}, your order with ID {str(order.id)} has been collaboratively cancelled."
self.send_message(user.robot.telegram_chat_id, text)
lang = user.robot.telegram_lang_code
if lang == "es":
title = f"❌ Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente."
else:
title = f"❌ Hey {user.username}, your order with ID {str(order.id)} has been collaboratively cancelled."
self.send_message(order, user.robot, title)
return

def dispute_opened(self, order):
for user in [order.maker, order.taker]:
if user.robot.telegram_enabled:
lang = user.robot.telegram_lang_code
if lang == "es":
text = f"⚖️ Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa."
else:
text = 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)
lang = user.robot.telegram_lang_code
if lang == "es":
title = f"⚖️ Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa."
else:
title = f"⚖️ Hey {user.username}, a dispute has been opened on your order with ID {str(order.id)}."
self.send_message(order, user.robot, title)

admin_chat_id = config("TELEGRAM_COORDINATOR_CHAT_ID")

if len(admin_chat_id) == 0:
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."
self.send_message(admin_chat_id, coordinator_text)
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

def order_published(self, order):
if order.maker.robot.telegram_enabled:
lang = order.maker.robot.telegram_lang_code
# In weird cases the order cannot be found (e.g. it is cancelled)
queryset = Order.objects.filter(maker=order.maker)
if len(queryset) == 0:
return
order = queryset.last()
if lang == "es":
text = f"✅ Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes."
else:
text = 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)
lang = order.maker.robot.telegram_lang_code
# In weird cases the order cannot be found (e.g. it is cancelled)
queryset = Order.objects.filter(maker=order.maker)
if len(queryset) == 0:
return
order = queryset.last()
if lang == "es":
title = f"✅ Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes."
else:
title = f"✅ Hey {order.maker.username}, your order with ID {str(order.id)} is public in the order book."
self.send_message(order, order.maker.robot, title)
return

def new_chat_message(self, order, chat_message):
Expand Down Expand Up @@ -189,14 +210,12 @@ def new_chat_message(self, order, chat_message):
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
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}"
self.send_message(user.robot.telegram_chat_id, text)
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)}."
self.send_message(order, user.robot, title, notification_reason)

return

def coordinator_cancelled(self, order):
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."
self.send_message(order.maker.robot.telegram_chat_id, text)
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, order.maker.robot, title)
return
Loading
Loading