diff --git a/chat/serializers.py b/chat/serializers.py index 76d95454..b1f5dbfc 100644 --- a/chat/serializers.py +++ b/chat/serializers.py @@ -18,7 +18,7 @@ class ChatSerializer(serializers.ModelSerializer): class PostMessageSerializer(serializers.ModelSerializer): class Meta: model = Message - fields = ("PGP_message", "order", "offset") + fields = ("PGP_message", "order_id", "offset") depth = 0 offset = serializers.IntegerField( diff --git a/chat/views.py b/chat/views.py index 1b11617d..4dbffff8 100644 --- a/chat/views.py +++ b/chat/views.py @@ -58,6 +58,7 @@ class ChatView(viewsets.ViewSet): chatroom.maker_connected = True chatroom.save() peer_connected = chatroom.taker_connected + peer_public_key = order.taker.profile.public_key elif chatroom.taker == request.user: chatroom.maker_connected = order.maker_last_seen > ( timezone.now() - timedelta(minutes=1) @@ -65,8 +66,13 @@ class ChatView(viewsets.ViewSet): chatroom.taker_connected = True chatroom.save() peer_connected = chatroom.maker_connected + peer_public_key = order.maker.profile.public_key messages = [] + peer_public_key_data = { + "message": peer_public_key + } + messages.append(peer_public_key_data) for message in queryset: d = ChatSerializer(message).data print(d) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4c5ddc67..7e209c8a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "0.2.2", + "version": "0.2.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "frontend", - "version": "0.2.2", + "version": "0.2.3", "license": "ISC", "dependencies": { "@babel/plugin-proposal-class-properties": "^7.16.7", diff --git a/frontend/src/basic/OrderPage/index.js b/frontend/src/basic/OrderPage/index.js index 26cc59db..13da6879 100644 --- a/frontend/src/basic/OrderPage/index.js +++ b/frontend/src/basic/OrderPage/index.js @@ -66,6 +66,8 @@ class OrderPage extends Component { openStoreToken: false, tabValue: 1, orderId: this.props.match.params.orderId, + chat_offset: 0, + turtle_mode: false, }; // Refresh delays according to Order status @@ -109,6 +111,7 @@ class OrderPage extends Component { currencyCode: this.getCurrencyCode(newStateVars.currency), penalty: newStateVars.penalty, // in case penalty time has finished, it goes back to null invoice_expired: newStateVars.invoice_expired, // in case invoice had expired, it goes back to null when it is valid again + chat_offset: this.state.chat_offset + newStateVars?.chat.messages.length, }; const completeStateVars = Object.assign({}, newStateVars, otherStateVars); @@ -117,7 +120,13 @@ class OrderPage extends Component { getOrderDetails = (id) => { this.setState({ orderId: id }); - apiClient.get('/api/order/?order_id=' + id).then(this.orderDetailsReceived); + + let path = '/api/order/?order_id=' + id; + if (this.state.turtle_mode) { + path += '&offset=' + this.state.chat_offset; + } + + apiClient.get(path).then(this.orderDetailsReceived); }; orderDetailsReceived = (data) => { diff --git a/frontend/src/components/TradeBox/EncryptedChat/ChatBottom/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/ChatBottom/index.tsx new file mode 100644 index 00000000..bc1ab971 --- /dev/null +++ b/frontend/src/components/TradeBox/EncryptedChat/ChatBottom/index.tsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import { Grid, useTheme, Tooltip, Button } from '@mui/material'; +import { ExportIcon } from '../../../Icons'; +import KeyIcon from '@mui/icons-material/Key'; +import { useTranslation } from 'react-i18next'; +import { saveAsJson } from '../../../../utils'; + +interface Props { + orderId: number; + setAudit: (audit: boolean) => void; + audit: boolean; +} + +const ChatBottom: React.FC = ({ orderId, setAudit, audit }) => { + const { t } = useTranslation(); + const theme = useTheme(); + + return ( + <> + + + + + + + + + + + + + ); +}; + +export default ChatBottom; +function createJsonFile(): any { + throw new Error('Function not implemented.'); +} diff --git a/frontend/src/components/TradeBox/EncryptedChat/ChatHeader/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/ChatHeader/index.tsx new file mode 100644 index 00000000..5a0a07fe --- /dev/null +++ b/frontend/src/components/TradeBox/EncryptedChat/ChatHeader/index.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Grid, Paper, Typography, useTheme } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +interface Props { + connected: boolean; + peerConnected: boolean; +} + +const ChatHeader: React.FC = ({ connected, peerConnected }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const connectedColor = theme.palette.mode === 'light' ? '#b5e3b7' : '#153717'; + const connectedTextColor = theme.palette.getContrastText(connectedColor); + + return ( + + + + + + {t('You') + ': '} + {connected ? t('connected') : t('disconnected')} + + + + + + + + {t('Peer') + ': '} + {peerConnected ? t('connected') : t('disconnected')} + + + + + + ); +}; + +export default ChatHeader; diff --git a/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx new file mode 100644 index 00000000..80a9560e --- /dev/null +++ b/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx @@ -0,0 +1,321 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Tooltip, TextField, Grid, Container, Paper, Typography } from '@mui/material'; +import { encryptMessage, decryptMessage } from '../../../../pgp'; +import { saveAsJson } from '../../../../utils'; +import { AuditPGPDialog } from '../../../Dialogs'; +import { systemClient } from '../../../../services/System'; +import { websocketClient, WebsocketConnection } from '../../../../services/Websocket'; + +// Icons +import CircularProgress from '@mui/material/CircularProgress'; +import KeyIcon from '@mui/icons-material/Key'; +import { ExportIcon } from '../../../Icons'; +import { useTheme } from '@mui/system'; +import MessageCard from '../MessageCard'; +import ChatHeader from '../ChatHeader'; +import { EncryptedChatMessage, ServerMessage } from '..'; +import ChatBottom from '../ChatBottom'; + +interface Props { + orderId: number; + userNick: string; + takerNick: string; +} + +const EncryptedSocketChat: React.FC = ({ + orderId, + userNick, + takerNick, +}: Props): JSX.Element => { + const { t } = useTranslation(); + const theme = useTheme(); + + const audio = new Audio(`/static/assets/sounds/chat-open.mp3`); + const [connected, setConnected] = useState(false); + const [peerConnected, setPeerConnected] = useState(false); + const [ownPubKey] = useState( + (systemClient.getCookie('pub_key') ?? '').split('\\').join('\n'), + ); + const [ownEncPrivKey] = useState( + (systemClient.getCookie('enc_priv_key') ?? '').split('\\').join('\n'), + ); + const [peerPubKey, setPeerPubKey] = useState(); + const [token] = useState(systemClient.getCookie('robot_token') || ''); + const [messages, setMessages] = useState([]); + const [serverMessages, setServerMessages] = useState([]); + const [value, setValue] = useState(''); + const [connection, setConnection] = useState(); + const [audit, setAudit] = useState(false); + const [waitingEcho, setWaitingEcho] = useState(false); + const [lastSent, setLastSent] = useState('---BLANK---'); + const [messageCount, setMessageCount] = useState(0); + const [receivedIndexes, setReceivedIndexes] = useState([]); + + useEffect(() => { + if (!connected) { + connectWebsocket(); + } + }, [connected]); + + useEffect(() => { + if (messages.length > messageCount) { + audio.play(); + setMessageCount(messages.length); + } + }, [messages, messageCount]); + + useEffect(() => { + if (serverMessages) { + serverMessages.forEach(onMessage); + } + }, [serverMessages]); + + const connectWebsocket = () => { + websocketClient.open(`ws://${window.location.host}/ws/chat/${orderId}/`).then((connection) => { + setConnection(connection); + setConnected(true); + + connection.send({ + message: ownPubKey, + nick: userNick, + }); + + connection.onMessage((message) => setServerMessages((prev) => [...prev, message])); + connection.onClose(() => setConnected(false)); + connection.onError(() => setConnected(false)); + }); + }; + + const createJsonFile: () => object = () => { + return { + credentials: { + own_public_key: ownPubKey, + peer_public_key: peerPubKey, + encrypted_private_key: ownEncPrivKey, + passphrase: token, + }, + messages: messages, + }; + }; + + const onMessage: (message: any) => void = (message) => { + const dataFromServer = JSON.parse(message.data); + + if (dataFromServer && !receivedIndexes.includes(dataFromServer.index)) { + setReceivedIndexes((prev) => [...prev, dataFromServer.index]); + setPeerConnected(dataFromServer.peer_connected); + // If we receive a public key other than ours (our peer key!) + if ( + connection && + dataFromServer.message.substring(0, 36) == `-----BEGIN PGP PUBLIC KEY BLOCK-----` && + dataFromServer.message != ownPubKey + ) { + setPeerPubKey(dataFromServer.message); + connection.send({ + message: `-----SERVE HISTORY-----`, + nick: userNick, + }); + } + // If we receive an encrypted message + else if (dataFromServer.message.substring(0, 27) == `-----BEGIN PGP MESSAGE-----`) { + decryptMessage( + dataFromServer.message.split('\\').join('\n'), + dataFromServer.user_nick == userNick ? ownPubKey : peerPubKey, + ownEncPrivKey, + token, + ).then((decryptedData) => { + setWaitingEcho(waitingEcho ? decryptedData.decryptedMessage !== lastSent : false); + setLastSent(decryptedData.decryptedMessage === lastSent ? '----BLANK----' : lastSent); + setMessages((prev) => { + const existingMessage = prev.find((item) => item.index === dataFromServer.index); + if (existingMessage) { + return prev; + } else { + return [ + ...prev, + { + index: dataFromServer.index, + encryptedMessage: dataFromServer.message.split('\\').join('\n'), + plainTextMessage: decryptedData.decryptedMessage, + validSignature: decryptedData.validSignature, + userNick: dataFromServer.user_nick, + time: dataFromServer.time, + } as EncryptedChatMessage, + ].sort((a, b) => a.index - b.index); + } + }); + }); + } + // We allow plaintext communication. The user must write # to start + // If we receive an plaintext message + else if (dataFromServer.message.substring(0, 1) == '#') { + setMessages((prev) => { + const existingMessage = prev.find( + (item) => item.plainTextMessage === dataFromServer.message, + ); + if (existingMessage) { + return prev; + } else { + return [ + ...prev, + { + index: prev.length + 0.001, + encryptedMessage: dataFromServer.message, + plainTextMessage: dataFromServer.message, + validSignature: false, + userNick: dataFromServer.user_nick, + time: new Date().toString(), + } as EncryptedChatMessage, + ].sort((a, b) => a.index - b.index); + } + }); + } + } + }; + + const onButtonClicked = (e: any) => { + if (token && value.indexOf(token) !== -1) { + alert( + `Aye! You just sent your own robot token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`, + ); + setValue(''); + } + // If input string contains '#' send unencrypted and unlogged message + else if (connection && value.substring(0, 1) == '#') { + connection.send({ + message: value, + nick: userNick, + }); + setValue(''); + } + + // Else if message is not empty send message + else if (value != '') { + setValue(''); + setWaitingEcho(true); + setLastSent(value); + encryptMessage(value, ownPubKey, peerPubKey, ownEncPrivKey, token).then( + (encryptedMessage) => { + if (connection) { + connection.send({ + message: encryptedMessage.toString().split('\n').join('\\'), + nick: userNick, + }); + } + }, + ); + } + e.preventDefault(); + }; + + return ( + + +
+ + {messages.map((message, index) => { + const isTaker = takerNick === message.userNick; + const userConnected = message.userNick === userNick ? connected : peerConnected; + + return ( +
  • + +
  • + ); + })} +
    { + if (messages.length > messageCount) el?.scrollIntoView(); + }} + /> + +
    + + + { + setValue(e.target.value); + }} + sx={{ width: '13.7em' }} + /> + + + + + +
    +
    + +
    + + + setAudit(false)} + orderId={Number(orderId)} + messages={messages} + own_pub_key={ownPubKey || ''} + own_enc_priv_key={ownEncPrivKey || ''} + peer_pub_key={peerPubKey || 'Not received yet'} + passphrase={token || ''} + onClickBack={() => setAudit(false)} + /> + + + + + ); +}; + +export default EncryptedSocketChat; diff --git a/frontend/src/components/TradeBox/EncryptedChat/EncryptedTrutleChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/EncryptedTrutleChat/index.tsx new file mode 100644 index 00000000..d5a2baac --- /dev/null +++ b/frontend/src/components/TradeBox/EncryptedChat/EncryptedTrutleChat/index.tsx @@ -0,0 +1,308 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Tooltip, TextField, Grid, Container, Paper, Typography } from '@mui/material'; +import { encryptMessage, decryptMessage } from '../../../../pgp'; +import { saveAsJson } from '../../../../utils'; +import { AuditPGPDialog } from '../../../Dialogs'; +import { systemClient } from '../../../../services/System'; +import { websocketClient, WebsocketConnection } from '../../../../services/Websocket'; + +// Icons +import CircularProgress from '@mui/material/CircularProgress'; +import KeyIcon from '@mui/icons-material/Key'; +import { ExportIcon } from '../../../Icons'; +import { useTheme } from '@mui/system'; +import MessageCard from '../MessageCard'; +import ChatHeader from '../ChatHeader'; +import { EncryptedChatMessage, ServerMessage } from '..'; +import { apiClient } from '../../../../services/api'; +import ChatBottom from '../ChatBottom'; + +interface Props { + orderId: number; + userNick: string; + takerNick: string; + chatOffset: number; +} + +const EncryptedTurtleChat: React.FC = ({ + orderId, + userNick, + takerNick, + chatOffset, +}: Props): JSX.Element => { + const { t } = useTranslation(); + const theme = useTheme(); + + const audio = new Audio(`/static/assets/sounds/chat-open.mp3`); + const [peerConnected, setPeerConnected] = useState(false); + const [ownPubKey] = useState( + (systemClient.getCookie('pub_key') ?? '').split('\\').join('\n'), + ); + const [ownEncPrivKey] = useState( + (systemClient.getCookie('enc_priv_key') ?? '').split('\\').join('\n'), + ); + const [peerPubKey, setPeerPubKey] = useState( + '-----BEGIN PGP PUBLIC KEY BLOCK-----mDMEY0sj/RYJKwYBBAHaRw8BAQdALiPlUEfnW9k+pVMHuUstKpdfJRNN07+Huam7jP96vj20TFJvYm9TYXRzIElEIDFlMWUzM2VkMTc0ZTgzNTYzM2JkOTY5N2UyNTBhYTllNzgzYTBjMjUyZDMwYWU3ZDI4ZWFjNzUxMzFkMDI4ZjGIjAQQFgoAPgUCY0sj/QQLCQcICRBvpYjDqTP2jQMVCAoEFgACAQIZAQIbAwIeARYhBLm0oAYSSNiZp+gd32+liMOpM/aNAADR6AD/Yrlucc5F+rQxzKZSDcGvubK4lapfyYbgN+pgvRE9bX8A/jwDgWI07mR5bK1JPKDGzGdX4rnG1RPFkGY0n/XpTigGuDgEY0sj/RIKKwYBBAGXVQEFAQEHQK/+5UIZE6WWvPpPF4BAPnPDyEpAm82bDuaB3iup8+EXAwEIB4h4BBgWCAAqBQJjSyP9CRBvpYjDqTP2jQIbDBYhBLm0oAYSSNiZp+gd32+liMOpM/aNAABzuQD9F6/YNXr4hoDHYnVQR0n0LSKyhTV8FDusOuWrMzw3BcIBAMbTMHP1ykB7xTivGVvypRKsS5oMloqv59bJx01fzLEL=H0Iy-----END PGP PUBLIC KEY BLOCK-----', + ); + const [token] = useState(systemClient.getCookie('robot_token') || ''); + const [messages, setMessages] = useState([]); + const [value, setValue] = useState(''); + const [audit, setAudit] = useState(false); + const [waitingEcho, setWaitingEcho] = useState(false); + const [lastSent, setLastSent] = useState('---BLANK---'); + const [messageCount, setMessageCount] = useState(0); + const [serverMessages, setServerMessages] = useState([]); + const [lastIndex, setLastIndex] = useState(0); + + useEffect(() => { + if (messages.length > messageCount) { + audio.play(); + setMessageCount(messages.length); + } + }, [messages, messageCount]); + + useEffect(() => { + if (serverMessages.length > 0 && peerPubKey) { + serverMessages.forEach(onMessage); + } + }, [serverMessages, peerPubKey]); + + useEffect(() => { + if (chatOffset > lastIndex) { + loadMessages(); + } + }, [chatOffset]); + + const loadMessages: () => void = () => { + apiClient.get(`/api/chat?order_id=${orderId}&offset=${lastIndex}`).then((results: any) => { + if (results) { + setPeerConnected(results.peer_connected); + storePeerPubKey(results.messages); + setServerMessages(results.messages); + } + }); + }; + + const createJsonFile: () => object = () => { + return { + credentials: { + own_public_key: ownPubKey, + peer_public_key: peerPubKey, + encrypted_private_key: ownEncPrivKey, + passphrase: token, + }, + messages: messages, + }; + }; + + const storePeerPubKey: (dataFromServer: ServerMessage[]) => void = (dataFromServer) => { + dataFromServer.forEach((data) => { + if ( + data.message.substring(0, 36) == `-----BEGIN PGP PUBLIC KEY BLOCK-----` && + data.message != ownPubKey + ) { + setPeerPubKey(data.message); + } + }); + }; + + const onMessage: (dataFromServer: ServerMessage) => void = (dataFromServer) => { + if (dataFromServer) { + // If we receive an encrypted message + if (dataFromServer.message.substring(0, 27) == `-----BEGIN PGP MESSAGE-----`) { + decryptMessage( + dataFromServer.message.split('\\').join('\n'), + dataFromServer.nick == userNick ? ownPubKey : peerPubKey, + ownEncPrivKey, + token, + ).then((decryptedData) => { + setLastSent(decryptedData.decryptedMessage === lastSent ? '----BLANK----' : lastSent); + setLastIndex(lastIndex < dataFromServer.index ? dataFromServer.index : lastIndex); + setMessages((prev) => { + const existingMessage = prev.find((item) => item.index === dataFromServer.index); + if (existingMessage) { + return prev; + } else { + return [ + ...prev, + { + index: dataFromServer.index, + encryptedMessage: dataFromServer.message.split('\\').join('\n'), + plainTextMessage: decryptedData.decryptedMessage, + validSignature: decryptedData.validSignature, + userNick: dataFromServer.nick, + time: dataFromServer.time, + } as EncryptedChatMessage, + ].sort((a, b) => a.index - b.index); + } + }); + }); + } + // We allow plaintext communication. The user must write # to start + // If we receive an plaintext message + else if (dataFromServer.message.substring(0, 1) == '#') { + setMessages((prev) => { + const existingMessage = prev.find( + (item) => item.plainTextMessage === dataFromServer.message, + ); + if (existingMessage) { + return prev; + } else { + return [ + ...prev, + { + index: prev.length + 0.001, + encryptedMessage: dataFromServer.message, + plainTextMessage: dataFromServer.message, + validSignature: false, + userNick: dataFromServer.nick, + time: new Date().toString(), + } as EncryptedChatMessage, + ].sort((a, b) => a.index - b.index); + } + }); + } + } + }; + + const onButtonClicked = (e: any) => { + if (token && value.indexOf(token) !== -1) { + alert( + `Aye! You just sent your own robot token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`, + ); + setValue(''); + } + // If input string contains '#' send unencrypted and unlogged message + else if (value.substring(0, 1) == '#') { + apiClient + .post(`/api/chat`, { + PGP_message: value, + }) + .finally(() => { + setWaitingEcho(false); + setValue(''); + }); + } + // Else if message is not empty send message + else if (value != '') { + setWaitingEcho(true); + setLastSent(value); + encryptMessage(value, ownPubKey, peerPubKey, ownEncPrivKey, token).then( + (encryptedMessage) => { + apiClient + .post(`/api/chat`, { + PGP_message: encryptedMessage.toString().split('\n').join('\\'), + }) + .finally(() => { + setWaitingEcho(false); + setValue(''); + }); + }, + ); + } + e.preventDefault(); + }; + + return ( + + +
    + + {messages.map((message, index) => { + const isTaker = takerNick === message.userNick; + const userConnected = message.userNick === userNick ? true : peerConnected; + + return ( +
  • + +
  • + ); + })} +
    { + if (messages.length > messageCount) el?.scrollIntoView(); + }} + /> + +
    + + + { + setValue(e.target.value); + }} + sx={{ width: '13.7em' }} + /> + + + + + +
    +
    + +
    + + + setAudit(false)} + orderId={Number(orderId)} + messages={messages} + own_pub_key={ownPubKey || ''} + own_enc_priv_key={ownEncPrivKey || ''} + peer_pub_key={peerPubKey || 'Not received yet'} + passphrase={token || ''} + onClickBack={() => setAudit(false)} + /> + + + + ); +}; + +export default EncryptedTurtleChat; diff --git a/frontend/src/components/TradeBox/EncryptedChat/MessageCard/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/MessageCard/index.tsx new file mode 100644 index 00000000..b89ae749 --- /dev/null +++ b/frontend/src/components/TradeBox/EncryptedChat/MessageCard/index.tsx @@ -0,0 +1,142 @@ +import React, { useState } from 'react'; +import { IconButton, Tooltip, Card, CardHeader, useTheme } from '@mui/material'; +import RobotAvatar from '../../../RobotAvatar'; +import { systemClient } from '../../../../services/System'; +import { useTranslation } from 'react-i18next'; + +// Icons +import CheckIcon from '@mui/icons-material/Check'; +import CloseIcon from '@mui/icons-material/Close'; +import ContentCopy from '@mui/icons-material/ContentCopy'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import { EncryptedChatMessage } from '..'; + +interface Props { + message: EncryptedChatMessage; + isTaker: boolean; + userConnected: boolean; +} + +const MessageCard: React.FC = ({ message, isTaker, userConnected }) => { + const [showPGP, setShowPGP] = useState(); + const { t } = useTranslation(); + const theme = useTheme(); + + const takerCardColor = theme.palette.mode === 'light' ? '#d1e6fa' : '#082745'; + const makerCardColor = theme.palette.mode === 'light' ? '#f2d5f6' : '#380d3f'; + const cardColor = isTaker ? takerCardColor : makerCardColor; + + return ( + + + } + style={{ backgroundColor: cardColor }} + title={ + +
    +
    + {message.userNick} + {message.validSignature ? ( + + ) : ( + + )} +
    +
    + setShowPGP(!showPGP)} + > + + +
    +
    + + + systemClient.copyToClipboard( + showPGP ? message.encryptedMessage : message.plainTextMessage, + ) + } + > + + + +
    +
    +
    + } + subheader={ + showPGP ? ( + + {' '} + {message.time}
    {'Valid signature: ' + message.validSignature}
    {' '} + {message.encryptedMessage}{' '} +
    + ) : ( + message.plainTextMessage + ) + } + subheaderTypographyProps={{ + sx: { + wordWrap: 'break-word', + width: '14.3em', + position: 'relative', + right: '1.5em', + textAlign: 'left', + fontSize: showPGP ? theme.typography.fontSize * 0.78 : null, + }, + }} + /> +
    + ); +}; + +export default MessageCard; diff --git a/frontend/src/components/TradeBox/EncryptedChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/index.tsx index 53041e7f..61e1d9b2 100644 --- a/frontend/src/components/TradeBox/EncryptedChat/index.tsx +++ b/frontend/src/components/TradeBox/EncryptedChat/index.tsx @@ -1,40 +1,16 @@ -import React, { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - Button, - IconButton, - Tooltip, - TextField, - Grid, - Container, - Card, - CardHeader, - Paper, - Typography, -} from '@mui/material'; -import { encryptMessage, decryptMessage } from '../../../pgp'; -import { saveAsJson } from '../../../utils'; -import { AuditPGPDialog } from '../../Dialogs'; -import RobotAvatar from '../../RobotAvatar'; -import { systemClient } from '../../../services/System'; -import { websocketClient, WebsocketConnection } from '../../../services/Websocket'; - -// Icons -import CheckIcon from '@mui/icons-material/Check'; -import CloseIcon from '@mui/icons-material/Close'; -import ContentCopy from '@mui/icons-material/ContentCopy'; -import VisibilityIcon from '@mui/icons-material/Visibility'; -import CircularProgress from '@mui/material/CircularProgress'; -import KeyIcon from '@mui/icons-material/Key'; -import { ExportIcon } from '../../Icons'; -import { useTheme } from '@mui/system'; +import React from 'react'; +import EncryptedSocketChat from './EncryptedSocketChat'; +import EncryptedTrutleChat from './EncryptedTrutleChat'; interface Props { + turtleMode: boolean; orderId: number; + takerNick: string; + makerNick: string; userNick: string; + chatOffset: number; } - -interface EncryptedChatMessage { +export interface EncryptedChatMessage { userNick: string; validSignature: boolean; plainTextMessage: string; @@ -42,481 +18,29 @@ interface EncryptedChatMessage { time: string; index: number; } +export interface ServerMessage { + message: string; + time: string; + index: number; + nick: string; +} -const EncryptedChat: React.FC = ({ orderId, userNick }: Props): JSX.Element => { - const { t } = useTranslation(); - const theme = useTheme(); - - const audio = new Audio(`/static/assets/sounds/chat-open.mp3`); - const [connected, setConnected] = useState(false); - const [peerConnected, setPeerConnected] = useState(false); - const [ownPubKey] = useState( - (systemClient.getCookie('pub_key') ?? '').split('\\').join('\n'), - ); - const [ownEncPrivKey] = useState( - (systemClient.getCookie('enc_priv_key') ?? '').split('\\').join('\n'), - ); - const [peerPubKey, setPeerPubKey] = useState(); - const [token] = useState(systemClient.getCookie('robot_token') || ''); - const [messages, setMessages] = useState([]); - const [serverMessages, setServerMessages] = useState([]); - const [value, setValue] = useState(''); - const [connection, setConnection] = useState(); - const [audit, setAudit] = useState(false); - const [showPGP, setShowPGP] = useState([]); - const [waitingEcho, setWaitingEcho] = useState(false); - const [lastSent, setLastSent] = useState('---BLANK---'); - const [messageCount, setMessageCount] = useState(0); - const [receivedIndexes, setReceivedIndexes] = useState([]); - - useEffect(() => { - if (!connected) { - connectWebsocket(); - } - }, [connected]); - - useEffect(() => { - if (messages.length > messageCount) { - audio.play(); - setMessageCount(messages.length); - } - }, [messages, messageCount]); - - useEffect(() => { - if (serverMessages) { - serverMessages.forEach(onMessage); - } - }, [serverMessages]); - - const connectWebsocket = () => { - websocketClient.open(`ws://${window.location.host}/ws/chat/${orderId}/`).then((connection) => { - setConnection(connection); - setConnected(true); - - connection.send({ - message: ownPubKey, - nick: userNick, - }); - - connection.onMessage((message) => setServerMessages((prev) => [...prev, message])); - connection.onClose(() => setConnected(false)); - connection.onError(() => setConnected(false)); - }); - }; - - const createJsonFile: () => object = () => { - return { - credentials: { - own_public_key: ownPubKey, - peer_public_key: peerPubKey, - encrypted_private_key: ownEncPrivKey, - passphrase: token, - }, - messages: messages, - }; - }; - - const onMessage: (message: any) => void = (message) => { - const dataFromServer = JSON.parse(message.data); - - if (dataFromServer && !receivedIndexes.includes(dataFromServer.index)) { - setReceivedIndexes((prev) => [...prev, dataFromServer.index]); - setPeerConnected(dataFromServer.peer_connected); - // If we receive a public key other than ours (our peer key!) - if ( - connection && - dataFromServer.message.substring(0, 36) == `-----BEGIN PGP PUBLIC KEY BLOCK-----` && - dataFromServer.message != ownPubKey - ) { - setPeerPubKey(dataFromServer.message); - connection.send({ - message: `-----SERVE HISTORY-----`, - nick: userNick, - }); - } - // If we receive an encrypted message - else if (dataFromServer.message.substring(0, 27) == `-----BEGIN PGP MESSAGE-----`) { - decryptMessage( - dataFromServer.message.split('\\').join('\n'), - dataFromServer.user_nick == userNick ? ownPubKey : peerPubKey, - ownEncPrivKey, - token, - ).then((decryptedData) => { - setWaitingEcho(waitingEcho ? decryptedData.decryptedMessage !== lastSent : false); - setLastSent(decryptedData.decryptedMessage === lastSent ? '----BLANK----' : lastSent); - setMessages((prev) => { - const existingMessage = prev.find((item) => item.index === dataFromServer.index); - if (existingMessage) { - return prev; - } else { - return [ - ...prev, - { - index: dataFromServer.index, - encryptedMessage: dataFromServer.message.split('\\').join('\n'), - plainTextMessage: decryptedData.decryptedMessage, - validSignature: decryptedData.validSignature, - userNick: dataFromServer.user_nick, - time: dataFromServer.time, - } as EncryptedChatMessage, - ].sort((a, b) => a.index - b.index); - } - }); - }); - } - // We allow plaintext communication. The user must write # to start - // If we receive an plaintext message - else if (dataFromServer.message.substring(0, 1) == '#') { - setMessages((prev) => { - const existingMessage = prev.find( - (item) => item.plainTextMessage === dataFromServer.message, - ); - if (existingMessage) { - return prev; - } else { - return [ - ...prev, - { - index: prev.length + 0.001, - encryptedMessage: dataFromServer.message, - plainTextMessage: dataFromServer.message, - validSignature: false, - userNick: dataFromServer.user_nick, - time: new Date().toString(), - } as EncryptedChatMessage, - ].sort((a, b) => a.index - b.index); - } - }); - } - } - }; - - const onButtonClicked = (e: any) => { - if (token && value.indexOf(token) !== -1) { - alert( - `Aye! You just sent your own robot token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`, - ); - setValue(''); - } - // If input string contains '#' send unencrypted and unlogged message - else if (connection && value.substring(0, 1) == '#') { - connection.send({ - message: value, - nick: userNick, - }); - setValue(''); - } - - // Else if message is not empty send message - else if (value != '') { - setValue(''); - setWaitingEcho(true); - setLastSent(value); - encryptMessage(value, ownPubKey, peerPubKey, ownEncPrivKey, token).then( - (encryptedMessage) => { - if (connection) { - connection.send({ - message: encryptedMessage.toString().split('\n').join('\\'), - nick: userNick, - }); - } - }, - ); - } - e.preventDefault(); - }; - - const messageCard: ( - message: EncryptedChatMessage, - index: number, - cardColor: string, - userConnected: boolean, - ) => JSX.Element = (message, index, cardColor, userConnected) => { - return ( - - - } - style={{ backgroundColor: cardColor }} - title={ - -
    -
    - {message.userNick} - {message.validSignature ? ( - - ) : ( - - )} -
    -
    - { - const newShowPGP = [...showPGP]; - newShowPGP[index] = !newShowPGP[index]; - setShowPGP(newShowPGP); - }} - > - - -
    -
    - - - systemClient.copyToClipboard( - showPGP[index] ? message.encryptedMessage : message.plainTextMessage, - ) - } - > - - - -
    -
    -
    - } - subheader={ - showPGP[index] ? ( - - {' '} - {message.time}
    {'Valid signature: ' + message.validSignature}
    {' '} - {message.encryptedMessage}{' '} -
    - ) : ( - message.plainTextMessage - ) - } - subheaderTypographyProps={{ - sx: { - wordWrap: 'break-word', - width: '14.3em', - position: 'relative', - right: '1.5em', - textAlign: 'left', - fontSize: showPGP[index] ? theme.typography.fontSize * 0.78 : null, - }, - }} - /> -
    - ); - }; - - const connectedColor = theme.palette.mode === 'light' ? '#b5e3b7' : '#153717'; - const connectedTextColor = theme.palette.getContrastText(connectedColor); - const ownCardColor = theme.palette.mode === 'light' ? '#d1e6fa' : '#082745'; - const peerCardColor = theme.palette.mode === 'light' ? '#f2d5f6' : '#380d3f'; - - return ( - - - - - - - {t('You') + ': '} - {connected ? t('connected') : t('disconnected')} - - - - - - - - {t('Peer') + ': '} - {peerConnected ? t('connected') : t('disconnected')} - - - - - -
    - - {messages.map((message, index) => ( -
  • - {message.userNick == userNick - ? messageCard(message, index, ownCardColor, connected) - : messageCard(message, index, peerCardColor, peerConnected)} -
  • - ))} -
    { - if (messages.length > messageCount) el?.scrollIntoView(); - }} - /> - -
    - - - { - setValue(e.target.value); - }} - sx={{ width: '13.7em' }} - /> - - - - - -
    -
    - -
    - - - setAudit(false)} - orderId={Number(orderId)} - messages={messages} - own_pub_key={ownPubKey || ''} - own_enc_priv_key={ownEncPrivKey || ''} - peer_pub_key={peerPubKey || 'Not received yet'} - passphrase={token || ''} - onClickBack={() => setAudit(false)} - /> - - - - - - - - - - - - - - +const EncryptedChat: React.FC = ({ + turtleMode, + orderId, + takerNick, + userNick, + chatOffset, +}: Props): JSX.Element => { + return turtleMode ? ( + + ) : ( + ); }; diff --git a/frontend/src/components/TradeBox/index.js b/frontend/src/components/TradeBox/index.js index c962bd65..51712928 100644 --- a/frontend/src/components/TradeBox/index.js +++ b/frontend/src/components/TradeBox/index.js @@ -1469,7 +1469,15 @@ class TradeBox extends Component { )} - + {showDisputeButton ? this.showOpenDisputeButton() : ''} {showSendButton ? this.showFiatSentButton() : ''}