import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, TextField, Grid, Paper, Typography } from '@mui/material'; import { encryptMessage, decryptMessage } from '../../../../pgp'; import { AuditPGPDialog } from '../../../Dialogs'; // Icons import CircularProgress from '@mui/material/CircularProgress'; import KeyIcon from '@mui/icons-material/Key'; import { useTheme } from '@mui/system'; import MessageCard from '../MessageCard'; import ChatHeader from '../ChatHeader'; import { type EncryptedChatMessage, type ServerMessage } from '..'; import { apiClient } from '../../../../services/api'; import ChatBottom from '../ChatBottom'; import { type UseAppStoreType, AppContext } from '../../../../contexts/AppContext'; import { type UseFederationStoreType, FederationContext, } from '../../../../contexts/FederationContext'; import { type UseGarageStoreType, GarageContext } from '../../../../contexts/GarageContext'; import { Order } from '../../../../models'; interface Props { order: Order; userNick: string; takerNick: string; takerHashId: string; makerHashId: string; chatOffset: number; messages: EncryptedChatMessage[]; setMessages: (messages: EncryptedChatMessage[]) => void; baseUrl: string; turtleMode: boolean; setTurtleMode: (state: boolean) => void; } const audioPath = window.NativeRobosats === undefined ? '/static/assets/sounds' : 'file:///android_asset/Web.bundle/assets/sounds'; const EncryptedTurtleChat: React.FC = ({ order, userNick, takerNick, takerHashId, makerHashId, chatOffset, messages, setMessages, baseUrl, setTurtleMode, turtleMode, }: Props): JSX.Element => { const { t } = useTranslation(); const theme = useTheme(); const { origin, hostUrl, settings } = useContext(AppContext); const { federation } = useContext(FederationContext); const { garage } = useContext(GarageContext); const [audio] = useState(() => new Audio(`${audioPath}/chat-open.mp3`)); const [peerConnected, setPeerConnected] = useState(false); const [peerPubKey, setPeerPubKey] = 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); const [error, setError] = useState(''); useEffect(() => { if (messages.length > messageCount) { void audio.play(); setMessageCount(messages.length); } }, [messages, messageCount]); useEffect(() => { if (serverMessages.length > 0 && peerPubKey !== undefined) { serverMessages.forEach(onMessage); } }, [serverMessages, peerPubKey]); useEffect(() => { if (chatOffset === 0 || chatOffset > lastIndex) { loadMessages(); } }, [chatOffset]); const loadMessages: () => void = () => { const shortAlias = garage.getSlot()?.activeShortAlias; if (!shortAlias) return; const { url, basePath } = federation .getCoordinator(shortAlias) .getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl); apiClient .get(url + basePath, `/api/chat/?order_id=${order.id}&offset=${lastIndex}`, { tokenSHA256: garage.getSlot()?.getRobot()?.tokenSHA256 ?? '', }) .then((results: any) => { if (results != null) { setPeerConnected(results.peer_connected); setPeerPubKey(results.peer_pubkey.split('\\').join('\n')); setServerMessages(results.messages); } }) .catch((error) => { setError(error.toString()); }); }; const createJsonFile = (): object => { return { credentials: { own_public_key: garage.getSlot()?.getRobot()?.pubKey, peer_public_key: peerPubKey, encrypted_private_key: garage.getSlot()?.getRobot()?.encPrivKey, passphrase: garage.getSlot()?.token, }, messages, }; }; const onMessage = (dataFromServer: ServerMessage): void => { const slot = garage.getSlot(); const robot = slot?.getRobot(); if (robot && dataFromServer != null) { // If we receive an encrypted message if (dataFromServer.message.substring(0, 27) === `-----BEGIN PGP MESSAGE-----`) { void decryptMessage( dataFromServer.message.split('\\').join('\n'), dataFromServer.nick === userNick ? robot.pubKey : peerPubKey, robot.encPrivKey, slot.token, ).then((decryptedData) => { setLastSent(decryptedData.decryptedMessage === lastSent ? '----BLANK----' : lastSent); setLastIndex(lastIndex < dataFromServer.index ? dataFromServer.index : lastIndex); setMessages((prev: EncryptedChatMessage[]) => { const existingMessage = prev.find((item) => item.index === dataFromServer.index); if (existingMessage != null) { return prev; } else { const x: EncryptedChatMessage = { index: dataFromServer.index, encryptedMessage: dataFromServer.message.split('\\').join('\n'), plainTextMessage: decryptedData.decryptedMessage, validSignature: decryptedData.validSignature, userNick: dataFromServer.nick, time: dataFromServer.time, }; return [...prev, x].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: EncryptedChatMessage[]) => { const existingMessage = prev.find( (item) => item.plainTextMessage === dataFromServer.message, ); if (existingMessage != null) { return prev; } else { const x: EncryptedChatMessage = { index: prev.length + 0.001, encryptedMessage: dataFromServer.message, plainTextMessage: dataFromServer.message, validSignature: false, userNick: dataFromServer.nick, time: new Date().toString(), }; return [...prev, x].sort((a, b) => a.index - b.index); } }); } } }; const onButtonClicked = (e: React.FormEvent): void => { const slot = garage.getSlot(); const robot = slot?.getRobot(); if (!robot) return; if (slot?.token && value.includes(slot.token)) { alert( `Aye! You just sent your own robot 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) === '#') { const { url, basePath } = federation .getCoordinator(garage.getSlot()?.activeShortAlias ?? '') .getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl); apiClient .post( url + basePath, `/api/chat/`, { PGP_message: value, order_id: order.id, offset: lastIndex, }, { tokenSHA256: robot?.tokenSHA256 ?? '' }, ) .then((response) => { if (response != null) { if (response.messages != null) { setPeerConnected(response.peer_connected); setServerMessages(response.messages); } } }) .finally(() => { setWaitingEcho(false); setValue(''); }); } // Else if message is not empty send message else if (value !== '' && Boolean(robot?.pubKey)) { setWaitingEcho(true); setLastSent(value); encryptMessage(value, robot?.pubKey, peerPubKey ?? '', robot?.encPrivKey, slot?.token) .then((encryptedMessage) => { const { url, basePath } = federation .getCoordinator(garage.getSlot()?.activeShortAlias ?? '') .getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl); apiClient .post( url + basePath, `/api/chat/`, { PGP_message: String(encryptedMessage).split('\n').join('\\'), order_id: order.id, offset: lastIndex, }, { tokenSHA256: robot?.tokenSHA256 }, ) .then((response) => { if (response != null) { setPeerConnected(response.peer_connected); if (response.messages != null) { setServerMessages(response.messages); } } }) .finally(() => { setWaitingEcho(false); setValue(''); }); }) .catch((error) => { setError(error.toString()); }); } e.preventDefault(); }; return ( { setAudit(false); }} orderId={Number(order.id)} messages={messages} ownPubKey={garage.getSlot()?.getRobot()?.pubKey ?? ''} ownEncPrivKey={garage.getSlot()?.getRobot()?.encPrivKey ?? ''} peerPubKey={peerPubKey ?? 'Not received yet'} passphrase={garage.getSlot()?.token ?? ''} onClickBack={() => { setAudit(false); }} /> {messages.map((message, index) => { const isTaker = takerNick === message.userNick; const userConnected = message.userNick === userNick ? true : peerConnected; return (
  • ); })}
    { if (messages.length > messageCount) el?.scrollIntoView(); }} />
    { if (e.key === 'Enter' && !e.shiftKey) { onButtonClicked(e); } }} value={value} onChange={(e) => { setValue(e.target.value); }} fullWidth={true} /> {error}
    ); }; export default EncryptedTurtleChat;