diff --git a/.env-sample b/.env-sample index fefdead8..ae56b081 100644 --- a/.env-sample +++ b/.env-sample @@ -40,6 +40,10 @@ ONION_LOCATION = '' ALTERNATIVE_SITE = 'RoboSats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion' ALTERNATIVE_NAME = 'RoboSats Mainnet' +# Telegram bot token +TELEGRAM_TOKEN = '' +TELEGRAM_BOT_NAME = '' + # Lightning node open info, url to amboss and 1ML NETWORK = 'testnet' NODE_ALIAS = '🤖RoboSats⚡(RoboDevs)' diff --git a/api/logics.py b/api/logics.py index 1fd0cdbe..cc88aec0 100644 --- a/api/logics.py +++ b/api/logics.py @@ -4,6 +4,7 @@ from api.lightning.node import LNNode from django.db.models import Q from api.models import Order, LNPayment, MarketTick, User, Currency +from api.messages import Telegram from decouple import config from api.tasks import follow_send_payment @@ -130,6 +131,7 @@ class Logics: order.expires_at = timezone.now() + timedelta( seconds=Order.t_to_expire[Order.Status.TAK]) order.save() + Telegram.order_taken(order) return True, None def is_buyer(order, user): @@ -1051,3 +1053,4 @@ class Logics: user.profile.platform_rating = rating user.profile.save() return True, None + diff --git a/api/management/commands/telegram_watcher.py b/api/management/commands/telegram_watcher.py new file mode 100644 index 00000000..31006057 --- /dev/null +++ b/api/management/commands/telegram_watcher.py @@ -0,0 +1,50 @@ +from django.core.management.base import BaseCommand, CommandError + +from api.models import Profile +from api.messages import Telegram +from decouple import config +import requests +import time + +class Command(BaseCommand): + + help = "Polls telegram /getUpdates method" + rest = 3 # seconds between consecutive polls + + bot_token = config('TELEGRAM_TOKEN') + updates_url = f'https://api.telegram.org/bot{bot_token}/getUpdates' + + def handle(self, *args, **options): + """Infinite loop to check for telegram updates. + If it finds a new user (/start), enables it's taker found + notification and sends a 'Hey {username} {order_id}' message back""" + + offset = 0 + while True: + time.sleep(self.rest) + + params = {'offset' : offset + 1 , 'timeout' : 5} + print(params) + response = requests.get(self.updates_url, params=params).json() + if len(list(response['result'])) == 0: + continue + for result in response['result']: + text = result['message']['text'] + splitted_text = text.split(' ') + if splitted_text[0] == '/start': + token = splitted_text[-1] + print(token) + try : + profile = Profile.objects.get(telegram_token=token) + except: + print(f'No profile with token {token}') + continue + profile.telegram_chat_id = result['message']['from']['id'] + profile.telegram_lang_code = result['message']['from']['language_code'] + Telegram.welcome(profile.user) + profile.telegram_enabled = True + profile.save() + + offset = response['result'][-1]['update_id'] + + diff --git a/api/messages.py b/api/messages.py new file mode 100644 index 00000000..fe87cbe5 --- /dev/null +++ b/api/messages.py @@ -0,0 +1,63 @@ +from decouple import config +from secrets import token_urlsafe +from api.models import Order +import requests + +class Telegram(): + ''' Simple telegram messages by requesting to API''' + + def get_context(user): + """returns context needed to enable TG notifications""" + context = {} + if user.profile.telegram_enabled : + context['tg_enabled'] = True + else: + context['tg_enabled'] = False + + if user.profile.telegram_token == None: + user.profile.telegram_token = token_urlsafe(15) + user.profile.save() + + context['tg_token'] = user.profile.telegram_token + context['tg_bot_name'] = config("TELEGRAM_BOT_NAME") + + return context + + def send_message(user, text): + """ sends a message to a user with telegram notifications enabled""" + + bot_token=config('TELEGRAM_TOKEN') + + chat_id = user.profile.telegram_chat_id + message_url = f'https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}&text={text}' + + response = requests.get(message_url).json() + print(response) + + return + + @classmethod + def welcome(cls, user): + lang = user.profile.telegram_lang_code + order = Order.objects.get(maker=user) + print(str(order.id)) + if lang == 'es': + text = f'Hola {user.username}, te enviaré un mensaje cuando tu orden con ID {str(order.id)} haya sido tomada.' + else: + text = f"Hey {user.username}, I will send you a message when someone takes your order with ID {str(order.id)}." + cls.send_message(user, text) + return + + @classmethod + def order_taken(cls, order): + user = order.maker + lang = user.profile.telegram_lang_code + taker_nick = order.taker.username + site = config('HOST_NAME') + if lang == 'es': + text = f'Tu orden con ID {order.id} ha sido tomada por {taker_nick}!🎉 Visita http://{site}/order/{order.id} para continuar.' + else: + text = f'Your order with ID {order.id} was taken by {taker_nick}!🎉 Visit http://{site}/order/{order.id} to proceed with the trade.' + + cls.send_message(user, text) + return \ No newline at end of file diff --git a/api/models.py b/api/models.py index 7cb50230..dc3f0469 100644 --- a/api/models.py +++ b/api/models.py @@ -250,12 +250,11 @@ class Order(models.Model): ) # unique = True, a taker can only take one order maker_last_seen = models.DateTimeField(null=True, default=None, blank=True) taker_last_seen = models.DateTimeField(null=True, default=None, blank=True) - maker_asked_cancel = models.BooleanField( - default=False, null=False - ) # When collaborative cancel is needed and one partner has cancelled. - taker_asked_cancel = models.BooleanField( - default=False, null=False - ) # When collaborative cancel is needed and one partner has cancelled. + + # When collaborative cancel is needed and one partner has cancelled. + maker_asked_cancel = models.BooleanField(default=False, null=False) + taker_asked_cancel = models.BooleanField(default=False, null=False) + is_fiat_sent = models.BooleanField(default=False, null=False) # in dispute @@ -372,7 +371,7 @@ class Profile(models.Model): default=None, validators=[validate_comma_separated_integer_list], blank=True, - ) # Will only store latest ratings + ) # Will only store latest rating avg_rating = models.DecimalField( max_digits=4, decimal_places=1, @@ -382,7 +381,30 @@ class Profile(models.Model): MaxValueValidator(100)], blank=True, ) - + # Used to deep link telegram chat in case telegram notifications are enabled + telegram_token = models.CharField( + max_length=20, + null=True, + blank=True + ) + telegram_chat_id = models.BigIntegerField( + null=True, + default=None, + blank=True + ) + telegram_enabled = models.BooleanField( + default=False, + null=False + ) + telegram_lang_code = models.CharField( + max_length=4, + null=True, + blank=True + ) + telegram_welcomed = models.BooleanField( + default=False, + null=False + ) # Disputes num_disputes = models.PositiveIntegerField(null=False, default=0) lost_disputes = models.PositiveIntegerField(null=False, default=0) diff --git a/api/utils.py b/api/utils.py index 03c8a983..cce1ef7d 100644 --- a/api/utils.py +++ b/api/utils.py @@ -3,6 +3,7 @@ from decouple import config import numpy as np from api.models import Order +from secrets import token_urlsafe market_cache = {} @@ -86,8 +87,6 @@ def get_commit_robosats(): premium_percentile = {} - - @ring.dict(premium_percentile, expire=300) def compute_premium_percentile(order): @@ -106,3 +105,28 @@ def compute_premium_percentile(order): rates = np.array(rates) return round(np.sum(rates < order_rate) / len(rates), 2) + + +def get_telegram_context(user): + """returns context needed to enable TG notifications""" + context = {} + if user.profile.telegram_enabled : + context['tg_enabled'] = True + else: + context['tg_enabled'] = False + + if user.profile.telegram_token == None: + user.profile.telegram_token = token_urlsafe(15) + + context['tg_token'] = user.profile.telegram_token + context['tg_bot_name'] = config("TELEGRAM_BOT_NAME") + + return context + +def send_telegram_notification(user, text): + bot_token=config('TELEGRAM_TOKEN') + chat_id = user.profile.telegram_chat_id + message_url = f'https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}&text={text}' + response = requests.get(message_url).json() + print(response) + return \ No newline at end of file diff --git a/api/views.py b/api/views.py index 09bd85d5..4b7e73bd 100644 --- a/api/views.py +++ b/api/views.py @@ -9,10 +9,11 @@ from rest_framework.response import Response from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User -from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer -from .models import LNPayment, MarketTick, Order, Currency -from .logics import Logics -from .utils import get_lnd_version, get_commit_robosats, compute_premium_percentile +from api.serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer +from api.models import LNPayment, MarketTick, Order, Currency +from api.logics import Logics +from api.messages import Telegram +from api.utils import get_lnd_version, get_commit_robosats, compute_premium_percentile from .nick_generator.nick_generator import NickGenerator from robohash import Robohash @@ -182,15 +183,17 @@ class OrderView(viewsets.ViewSet): # 3.b If order is between public and WF2 if order.status >= Order.Status.PUB and order.status < Order.Status.WF2: - data["price_now"], data[ - "premium_now"] = Logics.price_and_premium_now(order) + data["price_now"], data["premium_now"] = Logics.price_and_premium_now(order) - # 3. c) If maker and Public, add num robots in book, premium percentile and num similar orders. + # 3. c) If maker and Public, add num robots in book, premium percentile + # num similar orders, and maker information to enable telegram notifications. if data["is_maker"] and order.status == Order.Status.PUB: data["premium_percentile"] = compute_premium_percentile(order) data["num_similar_orders"] = len( Order.objects.filter(currency=order.currency, status=Order.Status.PUB)) + # Adds/generate telegram token and whether it is enabled + data = {**data,**Telegram.get_context(request.user)} # 4) Non participants can view details (but only if PUB) elif not data["is_participant"] and order.status != Order.Status.PUB: @@ -518,8 +521,7 @@ class UserView(APIView): # Sends the welcome back message, only if created +3 mins ago if request.user.date_joined < (timezone.now() - timedelta(minutes=3)): - context[ - "found"] = "We found your Robot avatar. Welcome back!" + context["found"] = "We found your Robot avatar. Welcome back!" return Response(context, status=status.HTTP_202_ACCEPTED) else: # It is unlikely, but maybe the nickname is taken (1 in 20 Billion change) @@ -612,7 +614,6 @@ class BookView(ListAPIView): return Response(book_data, status=status.HTTP_200_OK) - class InfoView(ListAPIView): def get(self, request): diff --git a/docker-compose.yml b/docker-compose.yml index f6235567..d6bf52ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,6 @@ services: DEVELOPMENT: 1 volumes: - .:/usr/src/robosats - - /mnt/development/database:/usr/src/database - /mnt/development/lnd:/lnd network_mode: service:tor @@ -38,7 +37,6 @@ services: command: python3 manage.py clean_orders volumes: - .:/usr/src/robosats - - /mnt/development/database:/usr/src/database network_mode: service:tor follow-invoices: @@ -51,7 +49,16 @@ services: command: python3 manage.py follow_invoices volumes: - .:/usr/src/robosats - - /mnt/development/database:/usr/src/database + - /mnt/development/lnd:/lnd + network_mode: service:tor + + telegram-watcher: + build: . + container_name: tg-dev + restart: always + command: python3 manage.py telegram_watcher + volumes: + - .:/usr/src/robosats - /mnt/development/lnd:/lnd network_mode: service:tor