mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-14 19:36:24 +00:00
Merge pull request #28 from Reckless-Satoshi/asynchronous-chat
Minimal chat. Just enough for it to work. Does not log any message, so admin won't be able to resolve disputes easily with this set up.
This commit is contained in:
commit
89c5f7a997
@ -4,6 +4,8 @@ LND_CERT_BASE64=''
|
||||
LND_MACAROON_BASE64=''
|
||||
LND_GRPC_HOST='127.0.0.1:10009'
|
||||
|
||||
REDIS_URL=''
|
||||
|
||||
# Market price public API
|
||||
MARKET_PRICE_API = 'https://blockchain.info/ticker'
|
||||
|
||||
|
@ -375,7 +375,8 @@ class Logics():
|
||||
# If there was no taker_bond object yet, generates one
|
||||
order.last_satoshis = cls.satoshis_now(order)
|
||||
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
|
||||
description = f"RoboSats - Taking '{str(order)}' - This is a taker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel."
|
||||
pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling'
|
||||
description = f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {order.amount} - This is a taker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel."
|
||||
|
||||
# Gen hold Invoice
|
||||
hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
|
||||
|
@ -4,6 +4,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator, validat
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django.utils.html import mark_safe
|
||||
import uuid
|
||||
|
||||
from decouple import config
|
||||
from pathlib import Path
|
||||
@ -40,6 +41,7 @@ class LNPayment(models.Model):
|
||||
|
||||
|
||||
# payment use details
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HOLD)
|
||||
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
|
||||
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
|
||||
@ -59,7 +61,7 @@ class LNPayment(models.Model):
|
||||
receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None)
|
||||
|
||||
def __str__(self):
|
||||
return (f'HTLC {self.id}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
|
||||
return (f'LN-{str(self.id)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
|
||||
|
||||
class Order(models.Model):
|
||||
|
||||
@ -203,7 +205,7 @@ class MarketTick(models.Model):
|
||||
maker and taker are commited with bonds (contract
|
||||
is finished and cancellation has a cost)
|
||||
'''
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)])
|
||||
volume = models.DecimalField(max_digits=8, decimal_places=8, default=None, null=True, validators=[MinValueValidator(0)])
|
||||
premium = models.DecimalField(max_digits=5, decimal_places=2, default=None, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
|
||||
@ -235,6 +237,6 @@ class MarketTick(models.Model):
|
||||
tick.save()
|
||||
|
||||
def __str__(self):
|
||||
return f'Tick: {self.id}'
|
||||
return f'Tick: {str(self.id)[:8]}'
|
||||
|
||||
|
||||
|
@ -116,6 +116,7 @@ class OrderView(viewsets.ViewSet):
|
||||
data['is_maker'] = order.maker == request.user
|
||||
data['is_taker'] = order.taker == request.user
|
||||
data['is_participant'] = data['is_maker'] or data['is_taker']
|
||||
data['ur_nick'] = request.user.username
|
||||
|
||||
# 3) If not a participant and order is not public, forbid.
|
||||
if not data['is_participant'] and order.status != Order.Status.PUB:
|
||||
|
0
chat/__init__.py
Normal file
0
chat/__init__.py
Normal file
3
chat/admin.py
Normal file
3
chat/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
chat/apps.py
Normal file
6
chat/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ChatConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'chat'
|
64
chat/consumers.py
Normal file
64
chat/consumers.py
Normal file
@ -0,0 +1,64 @@
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
from api.logics import Logics
|
||||
from api.models import Order
|
||||
|
||||
import json
|
||||
|
||||
class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
|
||||
async def connect(self):
|
||||
self.order_id = self.scope['url_route']['kwargs']['order_id']
|
||||
self.room_group_name = f'chat_order_{self.order_id}'
|
||||
self.user = self.scope["user"]
|
||||
self.user_nick = str(self.user)
|
||||
|
||||
# Forbit if user is not part of the order
|
||||
# Does not work Async
|
||||
# order = Order.objects.get(id=self.order_id)
|
||||
|
||||
# # Check if user is participant on the order.
|
||||
# if not (Logics.is_buyer(order[0], self.user) or Logics.is_seller(order[0], self.user)):
|
||||
# print ("Outta this chat")
|
||||
# return False
|
||||
|
||||
print(self.user_nick)
|
||||
print(self.order_id)
|
||||
|
||||
await self.channel_layer.group_add(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
await self.accept()
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
await self.channel_layer.group_discard(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
async def receive(self, text_data):
|
||||
text_data_json = json.loads(text_data)
|
||||
message = text_data_json['message']
|
||||
nick = text_data_json['nick']
|
||||
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'chatroom_message',
|
||||
'message': message,
|
||||
'nick': nick,
|
||||
}
|
||||
)
|
||||
|
||||
async def chatroom_message(self, event):
|
||||
message = event['message']
|
||||
nick = event['nick']
|
||||
|
||||
await self.send(text_data=json.dumps({
|
||||
'message': message,
|
||||
'user_nick': nick,
|
||||
}))
|
||||
|
||||
pass
|
3
chat/models.py
Normal file
3
chat/models.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
6
chat/routing.py
Normal file
6
chat/routing.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.urls import re_path
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/chat/(?P<order_id>\w+)/$', consumers.ChatRoomConsumer.as_asgi()),
|
||||
]
|
82
chat/templates/chatroom.html
Normal file
82
chat/templates/chatroom.html
Normal file
@ -0,0 +1,82 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
|
||||
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
|
||||
|
||||
<title>Hello, world!</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<div class="row d-flex justify-content-center">
|
||||
<div class="col-6">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="exampleFormControlTextarea1" class="h4 pt-5">Chatroom</label>
|
||||
<textarea class="form-control" id="chat-text" rows="10"></textarea><br>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input class="form-control" id="input" type="text"></br>
|
||||
</div>
|
||||
<input class="btn btn-secondary btn-lg btn-block" id="submit" type="button" value="Send">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ request.user.username|json_script:"user_username" }}
|
||||
{{ order_id|json_script:"order-id" }}
|
||||
|
||||
<script>
|
||||
const user_username = JSON.parse(document.getElementById('user_username').textContent);
|
||||
document.querySelector('#submit').onclick = function (e) {
|
||||
const messageInputDom = document.querySelector('#input');
|
||||
const message = messageInputDom.value;
|
||||
chatSocket.send(JSON.stringify({
|
||||
'message': message,
|
||||
'username': user_username,
|
||||
}));
|
||||
messageInputDom.value = '';
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const orderId = JSON.parse(document.getElementById('order-id').textContent);
|
||||
|
||||
const chatSocket = new WebSocket(
|
||||
'ws://' +
|
||||
window.location.host +
|
||||
'/ws/chat/' +
|
||||
orderId +
|
||||
'/'
|
||||
);
|
||||
|
||||
chatSocket.onmessage = function (e) {
|
||||
const data = JSON.parse(e.data);
|
||||
console.log(data)
|
||||
document.querySelector('#chat-text').value += (data.username + ': ' + data.message + '\n')
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Optional JavaScript -->
|
||||
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
|
||||
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous">
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"
|
||||
integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous">
|
||||
</script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"
|
||||
integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous">
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
3
chat/tests.py
Normal file
3
chat/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
8
chat/urls.py
Normal file
8
chat/urls.py
Normal file
@ -0,0 +1,8 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('<str:order_id>/', views.room, name='order_chat'),
|
||||
]
|
10
chat/views.py
Normal file
10
chat/views.py
Normal file
@ -0,0 +1,10 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
def index(request):
|
||||
return render(request, 'index.html', {})
|
||||
|
||||
def room(request, order_id):
|
||||
return render(request, 'chatroom.html', {
|
||||
'order_id': order_id
|
||||
})
|
130
frontend/package-lock.json
generated
130
frontend/package-lock.json
generated
@ -2889,6 +2889,14 @@
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
|
||||
},
|
||||
"bufferutil": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz",
|
||||
"integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==",
|
||||
"requires": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||
@ -3319,6 +3327,15 @@
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.19.tgz",
|
||||
"integrity": "sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ=="
|
||||
},
|
||||
"d": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
|
||||
"integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
|
||||
"requires": {
|
||||
"es5-ext": "^0.10.50",
|
||||
"type": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.10.7",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
|
||||
@ -3564,6 +3581,35 @@
|
||||
"integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"es5-ext": {
|
||||
"version": "0.10.53",
|
||||
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
|
||||
"integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
|
||||
"requires": {
|
||||
"es6-iterator": "~2.0.3",
|
||||
"es6-symbol": "~3.1.3",
|
||||
"next-tick": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"es6-iterator": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
||||
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
|
||||
"requires": {
|
||||
"d": "1",
|
||||
"es5-ext": "^0.10.35",
|
||||
"es6-symbol": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"es6-symbol": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
|
||||
"integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
|
||||
"requires": {
|
||||
"d": "^1.0.1",
|
||||
"ext": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
@ -3706,6 +3752,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ext": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz",
|
||||
"integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==",
|
||||
"requires": {
|
||||
"type": "^2.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz",
|
||||
"integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
@ -4329,6 +4390,11 @@
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"dev": true
|
||||
},
|
||||
"is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
|
||||
},
|
||||
"is-windows": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
|
||||
@ -5927,6 +5993,11 @@
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
||||
},
|
||||
"next-tick": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
|
||||
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
|
||||
},
|
||||
"nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
@ -5953,6 +6024,11 @@
|
||||
"whatwg-url": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node-gyp-build": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz",
|
||||
"integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q=="
|
||||
},
|
||||
"node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
@ -7902,11 +7978,24 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"type": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
|
||||
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
|
||||
},
|
||||
"type-fest": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
|
||||
"integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="
|
||||
},
|
||||
"typedarray-to-buffer": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
||||
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
|
||||
"requires": {
|
||||
"is-typedarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"uglify-es": {
|
||||
"version": "3.3.9",
|
||||
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
|
||||
@ -8123,6 +8212,14 @@
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.8.tgz",
|
||||
"integrity": "sha512-k4dW/Qja1BYDl2qD4tOMB9PFVha/UJtxTc1cXYOe3WwA/2m0Yn4qB7wLMpJyLJ/7DR0XnTut3HsCSzDT4ZvKgA==",
|
||||
"requires": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@ -8305,6 +8402,34 @@
|
||||
"integrity": "sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==",
|
||||
"dev": true
|
||||
},
|
||||
"websocket": {
|
||||
"version": "1.0.34",
|
||||
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz",
|
||||
"integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==",
|
||||
"requires": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"debug": "^2.2.0",
|
||||
"es5-ext": "^0.10.50",
|
||||
"typedarray-to-buffer": "^3.1.5",
|
||||
"utf-8-validate": "^5.0.2",
|
||||
"yaeti": "^0.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
}
|
||||
}
|
||||
},
|
||||
"whatwg-fetch": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz",
|
||||
@ -8440,6 +8565,11 @@
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
|
||||
},
|
||||
"yaeti": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
|
||||
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
|
||||
},
|
||||
"yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
|
@ -33,6 +33,7 @@
|
||||
"react-native": "^0.66.4",
|
||||
"react-native-svg": "^12.1.1",
|
||||
"react-qr-code": "^2.0.3",
|
||||
"react-router-dom": "^5.2.0"
|
||||
"react-router-dom": "^5.2.0",
|
||||
"websocket": "^1.0.34"
|
||||
}
|
||||
}
|
||||
|
112
frontend/src/components/Chat.js
Normal file
112
frontend/src/components/Chat.js
Normal file
@ -0,0 +1,112 @@
|
||||
import React, { Component } from 'react';
|
||||
import { w3cwebsocket as W3CWebSocket } from "websocket";
|
||||
import {Button, TextField, Link, Grid, Typography, Container, Card, CardHeader, Paper, Avatar} from "@mui/material";
|
||||
import { withStyles } from "@mui/material";
|
||||
|
||||
|
||||
|
||||
export default class Chat extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
state = {
|
||||
messages: [],
|
||||
value:'',
|
||||
orderId: 2,
|
||||
};
|
||||
|
||||
client = new W3CWebSocket('ws://' + window.location.host + '/ws/chat/' + this.props.data.orderId + '/');
|
||||
|
||||
componentDidMount() {
|
||||
this.client.onopen = () => {
|
||||
console.log('WebSocket Client Connected')
|
||||
console.log(this.props.data)
|
||||
}
|
||||
this.client.onmessage = (message) => {
|
||||
const dataFromServer = JSON.parse(message.data);
|
||||
console.log('Got reply!', dataFromServer.type);
|
||||
if (dataFromServer){
|
||||
this.setState((state) =>
|
||||
({
|
||||
messages: [...state.messages,
|
||||
{
|
||||
msg: dataFromServer.message,
|
||||
userNick: dataFromServer.user_nick,
|
||||
}],
|
||||
|
||||
})
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
onButtonClicked = (e) => {
|
||||
this.client.send(JSON.stringify({
|
||||
type: "message",
|
||||
message: this.state.value,
|
||||
nick: this.props.data.urNick,
|
||||
}));
|
||||
this.state.value = ''
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container component="main" maxWidth="xs">
|
||||
<Paper style={{ height: 300, maxHeight: 300, overflow: 'auto', boxShadow: 'none', }}>
|
||||
{this.state.messages.map(message => <>
|
||||
<Card elevation={5} align="left" >
|
||||
{/* If message sender is not our nick, gray color, if it is our nick, green color */}
|
||||
{message.userNick == this.props.data.urNick ?
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar
|
||||
alt={message.userNick}
|
||||
src={window.location.origin +'/static/assets/avatars/' + message.userNick + '.png'}
|
||||
/>
|
||||
}
|
||||
style={{backgroundColor: '#e8ffe6'}}
|
||||
title={message.userNick}
|
||||
subheader={message.msg}
|
||||
/>
|
||||
:
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar
|
||||
alt={message.userNick}
|
||||
src={window.location.origin +'/static/assets/avatars/' + message.userNick + '.png'}
|
||||
/>
|
||||
}
|
||||
style={{backgroundColor: '#fcfcfc'}}
|
||||
title={message.userNick}
|
||||
subheader={message.msg}
|
||||
/>}
|
||||
</Card>
|
||||
</>)}
|
||||
</Paper>
|
||||
<form noValidate onSubmit={this.onButtonClicked}>
|
||||
<Grid containter alignItems="stretch" style={{ display: "flex" }}>
|
||||
<Grid item alignItems="stretch" style={{ display: "flex" }}>
|
||||
<TextField
|
||||
label="Type a message"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={this.state.value}
|
||||
onChange={e => {
|
||||
this.setState({ value: e.target.value });
|
||||
this.value = this.state.value;
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item alignItems="stretch" style={{ display: "flex" }}>
|
||||
<Button type="submit" variant="contained" color="primary" > Send </Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
@ -167,7 +167,7 @@ export default class MakerPage extends Component {
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid container xs={11} align="center">
|
||||
<Grid containter xs={8} alignItems="stretch" style={{ display: "flex" }}>
|
||||
<TextField
|
||||
error={this.state.amount == 0}
|
||||
helperText={this.state.amount == 0 ? 'Must be more than 0' : null}
|
||||
|
@ -94,6 +94,7 @@ export default class OrderPage extends Component {
|
||||
satoshis: data.satoshis,
|
||||
makerId: data.maker,
|
||||
isParticipant: data.is_participant,
|
||||
urNick: data.ur_nick,
|
||||
makerNick: data.maker_nick,
|
||||
takerId: data.taker,
|
||||
takerNick: data.taker_nick,
|
||||
|
@ -2,6 +2,8 @@ import React, { Component } from "react";
|
||||
import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider} from "@mui/material"
|
||||
import QRCode from "react-qr-code";
|
||||
|
||||
import Chat from "./Chat"
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
@ -53,6 +55,7 @@ export default class TradeBox extends Component {
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<QRCode value={this.props.data.bondInvoice} size={305}/>
|
||||
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.bondInvoice)}} align="center"> 📋Copy to clipboard</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
@ -61,9 +64,10 @@ export default class TradeBox extends Component {
|
||||
size="small"
|
||||
defaultValue={this.props.data.bondInvoice}
|
||||
disabled="true"
|
||||
helperText="This is a hold invoice. It will not be charged if the order succeeds or expires.
|
||||
It will be charged if the order is cancelled or you lose a dispute."
|
||||
helperText="This is a hold invoice. It will simply freeze in your wallet.
|
||||
It will be charged only if you cancel the order or lose a dispute."
|
||||
color = "secondary"
|
||||
onClick = {this.copyCodeToClipboard}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@ -90,6 +94,7 @@ export default class TradeBox extends Component {
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<QRCode value={this.props.data.escrowInvoice} size={305}/>
|
||||
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.escrowInvoice)}} align="center"> 📋Copy to clipboard</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
@ -98,7 +103,7 @@ export default class TradeBox extends Component {
|
||||
size="small"
|
||||
defaultValue={this.props.data.escrowInvoice}
|
||||
disabled="true"
|
||||
helperText="This is a hold LN invoice. It will be charged once the buyer confirms he sent the fiat."
|
||||
helperText="This is a hold invoice. It will simply freeze in your wallet. It will be charged once the buyer confirms he sent the fiat."
|
||||
color = "secondary"
|
||||
/>
|
||||
</Grid>
|
||||
@ -218,6 +223,7 @@ export default class TradeBox extends Component {
|
||||
valid invoice for {pn(this.props.data.invoiceAmount)} Satoshis.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<form noValidate onSubmit={this.handleClickSubmitInvoiceButton}>
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
error={this.state.badInvoice}
|
||||
@ -232,8 +238,9 @@ export default class TradeBox extends Component {
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='primary' onClick={this.handleClickSubmitInvoiceButton}>Submit</Button>
|
||||
<Button variant='contained' color='primary'>Submit</Button>
|
||||
</Grid>
|
||||
</form>
|
||||
{this.showBondIsLocked()}
|
||||
</Grid>
|
||||
)
|
||||
@ -324,7 +331,7 @@ handleRatingChange=(e)=>{
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button defaultValue="confirm" variant='contained' color='primary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} sent</Button>
|
||||
<Button defaultValue="confirm" variant='contained' color='secondary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} sent</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
@ -335,7 +342,7 @@ handleRatingChange=(e)=>{
|
||||
// Ask for double confirmation.
|
||||
return(
|
||||
<Grid item xs={12} align="center">
|
||||
<Button defaultValue="confirm" variant='contained' color='primary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} received</Button>
|
||||
<Button defaultValue="confirm" variant='contained' color='secondary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} received</Button>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
@ -357,24 +364,25 @@ handleRatingChange=(e)=>{
|
||||
<b>Chatting with {this.props.data.isMaker ? this.props.data.takerNick : this.props.data.makerNick}</b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="left">
|
||||
<Grid item xs={12} align="center">
|
||||
{this.props.data.isSeller ?
|
||||
<Typography component="body2" variant="body2">
|
||||
Say hi to your peer robot! Be helpful and concise. Let him know how to send you {this.props.data.currencyCode}.
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
Say hi! Be helpful and concise. Let him know how to send you {this.props.data.currencyCode}.
|
||||
</Typography>
|
||||
:
|
||||
<Typography component="body2" variant="body2">
|
||||
Say hi to your peer robot! Ask for payment details and click 'Confirm {this.props.data.currencyCode} sent' as soon as you send the payment.
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
Say hi! Ask for payment details and click "Confirm Sent" as soon as the payment is sent.
|
||||
</Typography>
|
||||
}
|
||||
<Divider/>
|
||||
</Grid>
|
||||
<Grid item xs={12} style={{ width:330, height:360}}>
|
||||
CHAT PLACEHOLDER
|
||||
</Grid>
|
||||
|
||||
<Chat data={this.props.data}/>
|
||||
|
||||
<Grid item xs={12} align="center">
|
||||
{openDisputeButton ? this.showOpenDisputeButton() : ""}
|
||||
{sendFiatButton ? this.showFiatSentButton() : ""}
|
||||
{receivedFiatButton ? this.showFiatReceivedButton() : ""}
|
||||
{openDisputeButton ? this.showOpenDisputeButton() : ""}
|
||||
</Grid>
|
||||
{this.showBondIsLocked()}
|
||||
</Grid>
|
||||
|
13
robosats/routing.py
Normal file
13
robosats/routing.py
Normal file
@ -0,0 +1,13 @@
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
import chat.routing
|
||||
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
'websocket': AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
chat.routing.websocket_urlpatterns,
|
||||
# TODO add api.routing.websocket_urlpatterns when Order page works with websocket
|
||||
)
|
||||
),
|
||||
})
|
@ -39,7 +39,9 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'channels',
|
||||
'api',
|
||||
'chat',
|
||||
'frontend.apps.FrontendConfig',
|
||||
]
|
||||
|
||||
@ -120,6 +122,26 @@ USE_TZ = True
|
||||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
ASGI_APPLICATION = "robosats.routing.application"
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
"hosts": [config('REDIS_URL')],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": config('REDIS_URL'),
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
|
@ -19,5 +19,6 @@ from django.urls import path, include
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include('api.urls')),
|
||||
path('', include('frontend.urls'))
|
||||
path('chat/', include('chat.urls')),
|
||||
path('', include('frontend.urls')),
|
||||
]
|
||||
|
14
setup.md
14
setup.md
@ -33,6 +33,19 @@ source /usr/local/bin/virtualenvwrapper.sh
|
||||
## Install Django admin relational links
|
||||
`pip install django-admin-relation-links`
|
||||
|
||||
## Install dependencies for websockets
|
||||
Install Redis
|
||||
`apt-get install redis-server`
|
||||
Test redis-server
|
||||
`redis-cli ping`
|
||||
|
||||
Install python dependencies
|
||||
```
|
||||
pip install channels
|
||||
pip install django-redis
|
||||
pip install channels-redis
|
||||
```
|
||||
|
||||
*Django 4.0 at the time of writting*
|
||||
|
||||
### Launch the local development node
|
||||
@ -100,6 +113,7 @@ npm install react-native-svg
|
||||
npm install react-qr-code
|
||||
npm install @mui/material
|
||||
npm install react-markdown
|
||||
npm install websocket
|
||||
```
|
||||
Note we are using mostly MaterialUI V5 (@mui/material) but Image loading from V4 (@material-ui/core) extentions (so both V4 and V5 are needed)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user