diff --git a/.env-sample b/.env-sample index e625159f..54d6637c 100644 --- a/.env-sample +++ b/.env-sample @@ -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' diff --git a/api/logics.py b/api/logics.py index ab4063c3..c8da042a 100644 --- a/api/logics.py +++ b/api/logics.py @@ -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) diff --git a/api/models.py b/api/models.py index ace4cdf4..bc7edd9f 100644 --- a/api/models.py +++ b/api/models.py @@ -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]}' diff --git a/api/views.py b/api/views.py index 0b9d9721..46dbd2d3 100644 --- a/api/views.py +++ b/api/views.py @@ -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: diff --git a/chat/__init__.py b/chat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/chat/admin.py b/chat/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/chat/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/chat/apps.py b/chat/apps.py new file mode 100644 index 00000000..2fe899ad --- /dev/null +++ b/chat/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ChatConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'chat' diff --git a/chat/consumers.py b/chat/consumers.py new file mode 100644 index 00000000..2013b34a --- /dev/null +++ b/chat/consumers.py @@ -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 \ No newline at end of file diff --git a/chat/models.py b/chat/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/chat/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/chat/routing.py b/chat/routing.py new file mode 100644 index 00000000..a00dddf7 --- /dev/null +++ b/chat/routing.py @@ -0,0 +1,6 @@ +from django.urls import re_path +from . import consumers + +websocket_urlpatterns = [ + re_path(r'ws/chat/(?P\w+)/$', consumers.ChatRoomConsumer.as_asgi()), +] \ No newline at end of file diff --git a/chat/templates/chatroom.html b/chat/templates/chatroom.html new file mode 100644 index 00000000..f638de64 --- /dev/null +++ b/chat/templates/chatroom.html @@ -0,0 +1,82 @@ + + + + + + + + + + + Hello, world! + + + + +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ + {{ request.user.username|json_script:"user_username" }} + {{ order_id|json_script:"order-id" }} + + + + + + + + + + + \ No newline at end of file diff --git a/chat/tests.py b/chat/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/chat/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/chat/urls.py b/chat/urls.py new file mode 100644 index 00000000..4ed60135 --- /dev/null +++ b/chat/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path('', views.index, name='index'), + path('/', views.room, name='order_chat'), +] \ No newline at end of file diff --git a/chat/views.py b/chat/views.py new file mode 100644 index 00000000..c0f94a87 --- /dev/null +++ b/chat/views.py @@ -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 + }) \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1b34eb74..69c66f59 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 33d9d3f8..28aa9147 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" } } diff --git a/frontend/src/components/Chat.js b/frontend/src/components/Chat.js new file mode 100644 index 00000000..9a1f6a66 --- /dev/null +++ b/frontend/src/components/Chat.js @@ -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 ( + + + {this.state.messages.map(message => <> + + {/* If message sender is not our nick, gray color, if it is our nick, green color */} + {message.userNick == this.props.data.urNick ? + + } + style={{backgroundColor: '#e8ffe6'}} + title={message.userNick} + subheader={message.msg} + /> + : + + } + style={{backgroundColor: '#fcfcfc'}} + title={message.userNick} + subheader={message.msg} + />} + + )} + +
+ + + { + this.setState({ value: e.target.value }); + this.value = this.state.value; + }} + /> + + + + + +
+
+ ) + } +} diff --git a/frontend/src/components/MakerPage.js b/frontend/src/components/MakerPage.js index 2abc6e36..7d2da795 100644 --- a/frontend/src/components/MakerPage.js +++ b/frontend/src/components/MakerPage.js @@ -167,7 +167,7 @@ export default class MakerPage extends Component { - + + - @@ -90,6 +94,7 @@ export default class TradeBox extends Component { + @@ -218,22 +223,24 @@ export default class TradeBox extends Component { valid invoice for {pn(this.props.data.invoiceAmount)} Satoshis. - - - - - - +
+ + + + + + +
{this.showBondIsLocked()} ) @@ -324,7 +331,7 @@ handleRatingChange=(e)=>{ return( - + ) @@ -335,7 +342,7 @@ handleRatingChange=(e)=>{ // Ask for double confirmation. return( - + ) } @@ -357,24 +364,25 @@ handleRatingChange=(e)=>{ Chatting with {this.props.data.isMaker ? this.props.data.takerNick : this.props.data.makerNick} - + {this.props.data.isSeller ? - - Say hi to your peer robot! Be helpful and concise. Let him know how to send you {this.props.data.currencyCode}. + + Say hi! Be helpful and concise. Let him know how to send you {this.props.data.currencyCode}. : - - 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. + + Say hi! Ask for payment details and click "Confirm Sent" as soon as the payment is sent. } + - - CHAT PLACEHOLDER - + + + - {sendFiatButton ? this.showFiatSentButton() : ""} - {receivedFiatButton ? this.showFiatReceivedButton() : ""} - {openDisputeButton ? this.showOpenDisputeButton() : ""} + {openDisputeButton ? this.showOpenDisputeButton() : ""} + {sendFiatButton ? this.showFiatSentButton() : ""} + {receivedFiatButton ? this.showFiatReceivedButton() : ""} {this.showBondIsLocked()} diff --git a/robosats/routing.py b/robosats/routing.py new file mode 100644 index 00000000..c842b36c --- /dev/null +++ b/robosats/routing.py @@ -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 + ) + ), +}) \ No newline at end of file diff --git a/robosats/settings.py b/robosats/settings.py index 4e8946aa..d41f249b 100644 --- a/robosats/settings.py +++ b/robosats/settings.py @@ -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 diff --git a/robosats/urls.py b/robosats/urls.py index b84599cc..eb7315bd 100644 --- a/robosats/urls.py +++ b/robosats/urls.py @@ -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')), ] diff --git a/setup.md b/setup.md index 7a206f0e..f530c3d8 100644 --- a/setup.md +++ b/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)