Merge pull request #53 from Reckless-Satoshi/telegram-notifications

Telegram notifications
This commit is contained in:
Reckless_Satoshi 2022-02-22 11:22:22 +00:00 committed by GitHub
commit 2eaa3cf7df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 280 additions and 51 deletions

View File

@ -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)'

View File

@ -4,10 +4,9 @@ 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
import math
import ast
@ -31,7 +30,7 @@ FIAT_EXCHANGE_DURATION = int(config("FIAT_EXCHANGE_DURATION"))
class Logics:
telegram = Telegram()
@classmethod
def validate_already_maker_or_taker(cls, user):
"""Validates if a use is already not part of an active order"""
@ -130,6 +129,7 @@ class Logics:
order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.TAK])
order.save()
cls.telegram.order_taken(order)
return True, None
def is_buyer(order, user):
@ -234,7 +234,8 @@ class Logics:
if maker_is_seller:
cls.settle_bond(order.maker_bond)
cls.return_bond(order.taker_bond)
try: # If seller is offline the escrow LNpayment does not even exist
# If seller is offline the escrow LNpayment does not exist
try:
cls.cancel_escrow(order)
except:
pass
@ -245,7 +246,8 @@ class Logics:
# If maker is buyer, settle the taker's bond order goes back to public
else:
cls.settle_bond(order.taker_bond)
try: # If seller is offline the escrow LNpayment does not even exist
# If seller is offline the escrow LNpayment does not even exist
try:
cls.cancel_escrow(order)
except:
pass
@ -569,7 +571,7 @@ class Logics:
When the second user asks for cancel. Order is totally cancelled.
Must have a small cost for both parties to prevent node DDOS."""
elif order.status in [
Order.Status.WFI, Order.Status.CHA, Order.Status.FSE
Order.Status.WFI, Order.Status.CHA
]:
# if the maker had asked, and now the taker does: cancel order, return everything
@ -1049,3 +1051,4 @@ class Logics:
user.profile.platform_rating = rating
user.profile.save()
return True, None

View File

@ -0,0 +1,51 @@
from django.core.management.base import BaseCommand, CommandError
from api.models import Profile
from api.messages import Telegram
from api.utils import get_tor_session
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'
session = get_tor_session()
telegram = Telegram()
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}
response = self.session.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]
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']
self.telegram.welcome(profile.user)
profile.telegram_enabled = True
profile.save()
offset = response['result'][-1]['update_id']

67
api/messages.py Normal file
View File

@ -0,0 +1,67 @@
from decouple import config
from secrets import token_urlsafe
from api.models import Order
from api.utils import get_tor_session
class Telegram():
''' Simple telegram messages by requesting to API'''
session = get_tor_session()
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(self, 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 = self.session.get(message_url).json()
print(response)
return
def welcome(self, 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)}."
self.send_message(user, text)
return
def order_taken(self, order):
user = order.maker
if not user.profile.telegram_enabled:
return
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.'
self.send_message(user, text)
return

View File

@ -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)

View File

@ -1,12 +1,18 @@
import requests, ring, os
from decouple import config
import numpy as np
import requests
from api.models import Order
def get_tor_session():
session = requests.session()
# Tor uses the 9050 port as the default socks port
session.proxies = {'http': 'socks5://127.0.0.1:9050',
'https': 'socks5://127.0.0.1:9050'}
return session
market_cache = {}
@ring.dict(market_cache, expire=3) # keeps in cache for 3 seconds
def get_exchange_rates(currencies):
"""
@ -15,6 +21,8 @@ def get_exchange_rates(currencies):
Returns the median price list.
"""
session = get_tor_session()
APIS = config("MARKET_PRICE_APIS",
cast=lambda v: [s.strip() for s in v.split(",")])
@ -22,7 +30,7 @@ def get_exchange_rates(currencies):
for api_url in APIS:
try: # If one API is unavailable pass
if "blockchain.info" in api_url:
blockchain_prices = requests.get(api_url).json()
blockchain_prices = session.get(api_url).json()
blockchain_rates = []
for currency in currencies:
try: # If a currency is missing place a None
@ -33,7 +41,7 @@ def get_exchange_rates(currencies):
api_rates.append(blockchain_rates)
elif "yadio.io" in api_url:
yadio_prices = requests.get(api_url).json()
yadio_prices = session.get(api_url).json()
yadio_rates = []
for currency in currencies:
try:
@ -74,8 +82,6 @@ def get_lnd_version():
robosats_commit_cache = {}
@ring.dict(robosats_commit_cache, expire=3600)
def get_commit_robosats():
@ -84,10 +90,7 @@ def get_commit_robosats():
return commit_hash
premium_percentile = {}
@ring.dict(premium_percentile, expire=300)
def compute_premium_percentile(order):
@ -105,4 +108,4 @@ def compute_premium_percentile(order):
float(similar_order.last_satoshis) / float(similar_order.amount))
rates = np.array(rates)
return round(np.sum(rates < order_rate) / len(rates), 2)
return round(np.sum(rates < order_rate) / len(rates), 2)

View File

@ -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):

View File

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

View File

@ -152,7 +152,9 @@ You have to copy or scan the invoice with your lightning wallet in order to lock
<img src="images/how-to-use/contract-box-8.png" width="370" />
</div>
Your order will be public for 6 hours. You can check the time left to expiration by checking the "Order" tab. It can be canceled at any time without penalty before it is taken by another robot. Keep the contract tab open to be notified [with this sound](https://github.com/Reckless-Satoshi/robosats/raw/main/frontend/static/assets/sounds/taker-found.mp3). It might be best to do this on a desktop computer and turn on the volume, so you do not miss when your order is taken. It might take long! Maybe you even forget! *Note: If you forget your order and a robot takes it and locks his fidelity bond, you risk losing your own fidelity bond by not fulfilling the next contract steps.*
Your order will be public for 6 hours. You can check the time left to expiration by checking the "Order" tab. It can be canceled at any time without penalty before it is taken by another robot. Keep the contract tab open to be notified [with this sound](https://github.com/Reckless-Satoshi/robosats/raw/main/frontend/static/assets/sounds/taker-found.mp3). It might be best to do this on a desktop computer and turn on the volume, so you do not miss when your order is taken. It might take long! Maybe you even forget! You can also enable telegram notifications by pressing "Enable Telegram Notification" and then pressing "Start" in the chat. You will receive a welcome message as confirmation of the enabled notifications. Another message will be sent once a taker for your order is found.
*Note: If you forget your order and a robot takes it and locks his fidelity bond, you risk losing your own fidelity bond by not fulfilling the next contract steps.*
In the contract tab you can also see how many other orders are public for the same currency. You can also see how well does your premium ranks among all other orders for the same currency.

View File

@ -154,7 +154,9 @@ Debes copiar o escanear la factura con tu billetera lightning para bloquear tu f
<img src="images/how-to-use/contract-box-8.png" width="370" />
</div>
Tu orden permanecerá publicada durante 6 horas. Puedes comprobar cuánto tiempo le queda consultando la pestaña "Order". Se puede cancelar en cualquier momento sin penalización antes de que otro robot tome tu orden. Mantén abierta la pestaña del contrato para recibir notificaciones [con este sonido](https://github.com/Reckless-Satoshi/robosats/raw/main/frontend/static/assets/sounds/taker-found.mp3). Es aconsejable hacer esto en un ordenador o portátil con el volumen encendido para enterarte cuando alguien tome tu orden porque puede transcurrir bastante tiempo. ¡Quizás incluso olvides que tienes publicada una orden! *Nota: Si no estás pendiente de tu orden y un robot la toma y bloquea su fianza de fidelidad, corres el riesgo de perder la fianza de fidelidad que depositaste por no cumplir con los siguientes pasos del contrato.*
Tu orden permanecerá publicada durante 6 horas. Puedes comprobar cuánto tiempo le queda consultando la pestaña "Order". Se puede cancelar en cualquier momento sin penalización antes de que otro robot tome tu orden. Mantén abierta la pestaña del contrato para recibir notificaciones [con este sonido](https://github.com/Reckless-Satoshi/robosats/raw/main/frontend/static/assets/sounds/taker-found.mp3). Es aconsejable hacer esto en un ordenador o portátil con el volumen encendido para enterarte cuando alguien tome tu orden porque puede transcurrir bastante tiempo. ¡Quizás incluso olvides que tienes publicada una orden! También puedes activar las notificaciones de telegram. Simplemente pulsa en "Enable Telegram Notifications" y presiona "Start" en la conversación con el bot de RoboSats. Te llegará un mensaje de bienvenida y cuando alguien tome la orden te avisará con un mensaje.
*Nota: Si no estás pendiente de tu orden y un robot la toma y bloquea su fianza, corres el riesgo de perder tu fianza por no cumplir con los siguientes pasos del contrato.*
En la pestaña del contrato también puedes ver cuántas órdenes hay publicadas para la misma moneda. También puedes en qué posición (en porcentaje) se sitúa la prima de tu oferta con respecto a las demás publicadas con la misma moneda.

View File

@ -180,21 +180,21 @@ export default class BottomBar extends Component {
<List>
<Divider/>
<ListItemButton component="a" href="https://t.me/robosats">
<ListItemButton component="a" target="_blank" href="https://t.me/robosats">
<ListItemIcon><SendIcon/></ListItemIcon>
<ListItemText primary="Join the RoboSats group"
secondary="Telegram (English / Main)"/>
</ListItemButton>
<Divider/>
<ListItemButton component="a" href="https://t.me/robosats_es">
<ListItemButton component="a" target="_blank" href="https://t.me/robosats_es">
<ListItemIcon><SendIcon/></ListItemIcon>
<ListItemText primary="Unase al grupo RoboSats"
secondary="Telegram (Español)"/>
</ListItemButton>
<Divider/>
<ListItemButton component="a" href="https://github.com/Reckless-Satoshi/robosats/issues">
<ListItemButton component="a" target="_blank" href="https://github.com/Reckless-Satoshi/robosats/issues">
<ListItemIcon><GitHubIcon/></ListItemIcon>
<ListItemText primary="Tell us about a new feature or a bug"
secondary="Github Issues - The Robotic Satoshis Open Source Project"/>

View File

@ -10,6 +10,7 @@ import QrReader from 'react-qr-reader'
import PercentIcon from '@mui/icons-material/Percent';
import BookIcon from '@mui/icons-material/Book';
import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner';
import SendIcon from '@mui/icons-material/Send';
function getCookie(name) {
let cookieValue = null;
@ -41,6 +42,7 @@ export default class TradeBox extends Component {
this.state = {
openConfirmFiatReceived: false,
openConfirmDispute: false,
openEnableTelegram: false,
badInvoice: false,
badStatement: false,
qrscanner: false,
@ -93,7 +95,7 @@ export default class TradeBox extends Component {
<DialogContent>
<DialogContentText id="alert-dialog-description">
The RoboSats staff will examine the statements and evidence provided. You need to build
a complete case, as the staff cannot read the chat. You MUST provide a burner contact
a complete case, as the staff cannot read the chat. It is best to provide a burner contact
method with your statement. The satoshis in the trade escrow will be sent to the dispute winner,
while the dispute loser will lose the bond.
</DialogContentText>
@ -246,11 +248,50 @@ export default class TradeBox extends Component {
);
}
handleClickOpenTelegramDialog = () => {
this.setState({openEnableTelegram: true});
};
handleClickCloseEnableTelegramDialog = () => {
this.setState({openEnableTelegram: false});
};
handleClickEnableTelegram = () =>{
window.open("https://t.me/"+this.props.data.tg_bot_name+'?start='+this.props.data.tg_token, '_blank').focus()
this.handleClickCloseEnableTelegramDialog();
};
EnableTelegramDialog =() =>{
return(
<Dialog
open={this.state.openEnableTelegram}
onClose={this.handleClickCloseEnableTelegramDialog}
aria-labelledby="enable-telegram-dialog-title"
aria-describedby="enable-telegram-dialog-description"
>
<DialogTitle id="open-dispute-dialog-title">
Enable TG Notifications
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
You will be taken to a conversation with RoboSats telegram bot.
Simply open the chat and press "Start". Note that by enabling
telegram notifications you might lower your level of anonimity.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClickCloseEnableTelegramDialog}>Go back</Button>
<Button onClick={this.handleClickEnableTelegram} autoFocus> Enable </Button>
</DialogActions>
</Dialog>
)
}
showMakerWait=()=>{
return (
<Grid container spacing={1}>
{/* Make confirmation sound for HTLC received. */}
<this.Sound soundFileName="locked-invoice"/>
<this.EnableTelegramDialog/>
<Grid item xs={12} align="center">
<Typography component="subtitle1" variant="subtitle1">
<b> Your order is public. Wait for a taker. </b>
@ -264,11 +305,20 @@ export default class TradeBox extends Component {
<Typography component="body2" variant="body2" align="left">
<p>Be patient while robots check the book.
It might take some time. This box will ring 🔊 once a robot takes your order. </p>
<p>Please note that if your premium is too high, or if your currency or payment
<p>Please note that if your premium is excessive, or your currency or payment
methods are not popular, your order might expire untaken. Your bond will
return to you (no action needed).</p>
</Typography>
</ListItem>
<Grid item xs={12} align="center">
{this.props.data.tg_enabled ?
<Typography color='primary' component="h6" variant="h6" align="center"> Telegram enabled</Typography>
:
<Button color="primary" onClick={this.handleClickOpenTelegramDialog}>
<SendIcon/>Enable Telegram Notifications
</Button>
}
</Grid>
{/* TODO API sends data for a more confortable wait */}
<Divider/>
<ListItem>
@ -446,7 +496,7 @@ export default class TradeBox extends Component {
<Grid item xs={12} align="left">
<Typography component="body2" variant="body2">
Please, submit your statement. Be clear and specific about what happened and provide the necessary
evidence. You MUST provide a burner email, XMPP or telegram username to follow up with the staff.
evidence. It is best to provide a burner email, XMPP or telegram username to follow up with the staff.
Disputes are solved at the discretion of real robots <i>(aka humans)</i>, so be as helpful
as possible to ensure a fair outcome. Max 5000 chars.
</Typography>

View File

@ -28,13 +28,29 @@ export default class UserGenPage extends Component {
constructor(props) {
super(props);
this.state = {
token: this.genBase62Token(36),
openInfo: false,
loadingRobot: true,
tokenHasChanged: false,
};
this.props.setAppState({avatarLoaded: false, nickname: null, token: null});
this.getGeneratedUser(this.state.token);
//this.props.setAppState({avatarLoaded: false, nickname: null, token: null});
// Checks in parent HomePage if there is already a nick and token
// Displays the existing one
if (this.props.nickname != null){
this.state = {
nickname: this.props.nickname,
token: this.props.token? this.props.token : null,
avatar_url: 'static/assets/avatars/' + this.props.nickname + '.png',
loadingRobot: false
}
}
else{
var newToken = this.genBase62Token(36)
this.state = {
token: newToken
}
this.getGeneratedUser(newToken);
}
}
// sort of cryptographically strong function to generate Base62 token client-side

File diff suppressed because one or more lines are too long

View File

@ -22,4 +22,5 @@ robohash==1.1
scipy==1.8.0
gunicorn==20.1.0
psycopg2==2.9.3
SQLAlchemy==1.4.31
SQLAlchemy==1.4.31
requests[socks]