update chat views from upstream, support sending logs

Co-authored-by: jmacxx <47253594+jmacxx@users.noreply.github.com>
This commit is contained in:
woodser 2024-03-20 18:20:24 -04:00
parent 833cdb3b84
commit 1647a582f5
23 changed files with 1691 additions and 206 deletions

View File

@ -26,6 +26,7 @@ import org.apache.commons.io.IOUtils;
import javax.annotation.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@ -35,6 +36,7 @@ import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Scanner;
@Slf4j
public class FileUtil {
@ -240,4 +242,14 @@ public class FileUtil {
renameFile(storageFile, corruptedFile);
}
}
public static boolean doesFileContainKeyword(File file, String keyword) throws FileNotFoundException {
Scanner s = new Scanner(file);
while (s.hasNextLine()) {
if (s.nextLine().contains(keyword)) {
return true;
}
}
return false;
}
}

View File

@ -60,7 +60,7 @@ public class CoreNotificationService {
sendNotification(NotificationMessage.newBuilder()
.setType(NotificationType.CHAT_MESSAGE)
.setTimestamp(System.currentTimeMillis())
.setChatMessage(chatMessage.toProtoChatMessageBuilder())
.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage())
.build());
}

View File

@ -55,6 +55,7 @@ import haveno.core.trade.messages.SignContractResponse;
import haveno.network.p2p.AckMessage;
import haveno.network.p2p.BundleOfEnvelopes;
import haveno.network.p2p.CloseConnectionMessage;
import haveno.network.p2p.FileTransferPart;
import haveno.network.p2p.PrefixedSealedAndSignedMessage;
import haveno.network.p2p.peers.getdata.messages.GetDataResponse;
import haveno.network.p2p.peers.getdata.messages.GetUpdatedDataRequest;
@ -178,6 +179,9 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
case GET_INVENTORY_RESPONSE:
return GetInventoryResponse.fromProto(proto.getGetInventoryResponse(), messageVersion);
case FILE_TRANSFER_PART:
return FileTransferPart.fromProto(proto.getFileTransferPart(), messageVersion);
default:
throw new ProtobufferException("Unknown proto message case (PB.NetworkEnvelope). messageCase=" +
proto.getMessageCase() + "; proto raw data=" + proto.toString());

View File

@ -30,8 +30,13 @@ import haveno.core.locale.Res;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.proto.CoreProtoResolver;
import haveno.core.support.SupportType;
import haveno.core.support.dispute.mediation.FileTransferReceiver;
import haveno.core.support.dispute.mediation.FileTransferSender;
import haveno.core.support.dispute.mediation.FileTransferSession;
import haveno.core.support.messages.ChatMessage;
import haveno.core.trade.Contract;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.network.NetworkNode;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
@ -49,6 +54,8 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@ -151,6 +158,25 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
private transient final BooleanProperty isClosedProperty = new SimpleBooleanProperty();
private transient final IntegerProperty badgeCountProperty = new SimpleIntegerProperty();
private transient FileTransferReceiver fileTransferSession = null;
public FileTransferReceiver createOrGetFileTransferReceiver(NetworkNode networkNode,
NodeAddress peerNodeAddress,
FileTransferSession.FtpCallback callback) throws IOException {
// the receiver stores its state temporarily here in the dispute
// this method gets called to retrieve the session each time a part of the log files is received
if (fileTransferSession == null) {
fileTransferSession = new FileTransferReceiver(networkNode, peerNodeAddress, this.tradeId, this.traderId, this.getRoleStringForLogFile(), callback);
}
return fileTransferSession;
}
public FileTransferSender createFileTransferSender(NetworkNode networkNode,
NodeAddress peerNodeAddress,
FileTransferSession.FtpCallback callback) {
return new FileTransferSender(networkNode, peerNodeAddress, this.tradeId, this.traderId, this.getRoleStringForLogFile(), false, callback);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -478,6 +504,11 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
}
}
public String getRoleStringForLogFile() {
return (disputeOpenerIsBuyer ? "BUYER" : "SELLER") + "_"
+ (disputeOpenerIsMaker ? "MAKER" : "TAKER");
}
@Nullable
public PaymentAccountPayload getBuyerPaymentAccountPayload() {
return contract.isBuyerMakerAndSellerTaker() ? makerPaymentAccountPayload : takerPaymentAccountPayload;

View File

@ -940,7 +940,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
return new Tuple2<>(peerNodeAddress, receiverPubKeyRing);
}
private boolean isAgent(Dispute dispute) {
public boolean isAgent(Dispute dispute) {
return keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing());
}
@ -1038,6 +1038,20 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
requestPersistence();
}
protected void addMediationLogsReceivedMessage(Dispute dispute, String logsIdentifier) {
String logsReceivedMessage = Res.get("support.mediatorReceivedLogs", logsIdentifier);
ChatMessage chatMessage = new ChatMessage(
getSupportType(),
dispute.getTradeId(),
keyRing.hashCode(),
false,
logsReceivedMessage,
p2PService.getAddress());
chatMessage.setSystemMessage(true);
dispute.addAndPersistChatMessage(chatMessage);
requestPersistence();
}
// If price was going down between take offer time and open dispute time the buyer has an incentive to
// not send the payment but to try to make a new trade with the better price. We risks to lose part of the
// security deposit (in mediation we will always get back 0.003 BTC to keep some incentive to accept mediated

View File

@ -43,6 +43,7 @@ import haveno.common.UserThread;
import haveno.common.app.Version;
import haveno.common.config.Config;
import haveno.common.crypto.KeyRing;
import haveno.common.proto.network.NetworkEnvelope;
import haveno.core.api.XmrConnectionService;
import haveno.core.api.CoreNotificationService;
import haveno.core.locale.Res;
@ -55,6 +56,9 @@ import haveno.core.support.dispute.DisputeResult;
import haveno.core.support.dispute.DisputeResult.Winner;
import haveno.core.support.dispute.DisputeSummaryVerification;
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import haveno.core.support.dispute.mediation.FileTransferReceiver;
import haveno.core.support.dispute.mediation.FileTransferSender;
import haveno.core.support.dispute.mediation.FileTransferSession;
import haveno.core.support.dispute.messages.DisputeClosedMessage;
import haveno.core.support.dispute.messages.DisputeOpenedMessage;
import haveno.core.support.messages.ChatMessage;
@ -67,8 +71,11 @@ import haveno.core.trade.TradeManager;
import haveno.core.xmr.wallet.TradeWalletService;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.AckMessageSourceType;
import haveno.network.p2p.FileTransferPart;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
import haveno.network.p2p.network.Connection;
import haveno.network.p2p.network.MessageListener;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroDestination;
@ -76,6 +83,7 @@ import monero.wallet.model.MoneroMultisigSignResult;
import monero.wallet.model.MoneroTxSet;
import monero.wallet.model.MoneroTxWallet;
import java.io.IOException;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.HashSet;
@ -88,7 +96,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@Singleton
public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeList> {
public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeList> implements MessageListener, FileTransferSession.FtpCallback {
private final ArbitratorManager arbitratorManager;
@ -116,6 +124,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
openOfferManager, keyRing, arbitrationDisputeListService, config, priceFeedService);
this.arbitratorManager = arbitratorManager;
HavenoUtils.arbitrationManager = this; // TODO: storing static reference, better way?
p2PService.getNetworkNode().addMessageListener(this); // listening for FileTransferPart message
}
@ -497,4 +506,60 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
}
}
}
public FileTransferSender initLogUpload(FileTransferSession.FtpCallback callback,
String tradeId,
int traderId) throws IOException {
Dispute dispute = findDispute(tradeId, traderId)
.orElseThrow(() -> new IOException("could not locate Dispute for tradeId/traderId"));
return dispute.createFileTransferSender(p2PService.getNetworkNode(),
dispute.getContract().getArbitratorNodeAddress(), callback);
}
private void processFilePartReceived(FileTransferPart ftp) {
if (!ftp.isInitialRequest()) {
return; // existing sessions are processed by FileTransferSession object directly
}
// we create a new session which is related to an open dispute from our list
Optional<Dispute> dispute = findDispute(ftp.getTradeId(), ftp.getTraderId());
if (dispute.isEmpty()) {
log.error("Received log upload request for unknown TradeId/TraderId {}/{}", ftp.getTradeId(), ftp.getTraderId());
return;
}
if (dispute.get().isClosed()) {
log.error("Received a file transfer request for closed dispute {}", ftp.getTradeId());
return;
}
try {
FileTransferReceiver session = dispute.get().createOrGetFileTransferReceiver(
p2PService.getNetworkNode(), ftp.getSenderNodeAddress(), this);
session.processFilePartReceived(ftp);
} catch (IOException e) {
log.error("Unable to process a received file message" + e);
}
}
@Override
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
if (networkEnvelope instanceof FileTransferPart) { // mediator receiving log file data
FileTransferPart ftp = (FileTransferPart) networkEnvelope;
processFilePartReceived(ftp);
}
}
@Override
public void onFtpProgress(double progressPct) {
log.trace("ftp progress: {}", progressPct);
}
@Override
public void onFtpComplete(FileTransferSession session) {
Optional<Dispute> dispute = findDispute(session.getFullTradeId(), session.getTraderId());
dispute.ifPresent(d -> addMediationLogsReceivedMessage(d, session.getZipId()));
}
@Override
public void onFtpTimeout(String statusMsg, FileTransferSession session) {
session.resetSession();
}
}

View File

@ -0,0 +1,126 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.support.dispute.mediation;
import haveno.network.p2p.AckMessage;
import haveno.network.p2p.AckMessageSourceType;
import haveno.network.p2p.FileTransferPart;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.network.NetworkNode;
import haveno.common.UserThread;
import haveno.common.config.Config;
import haveno.common.util.Utilities;
import java.nio.file.FileSystems;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public class FileTransferReceiver extends FileTransferSession {
protected final String zipFilePath;
public FileTransferReceiver(NetworkNode networkNode,
NodeAddress peerNodeAddress,
String tradeId,
int traderId,
String traderRole,
@Nullable FileTransferSession.FtpCallback callback) throws IOException {
super(networkNode, peerNodeAddress, tradeId, traderId, traderRole, callback);
zipFilePath = ensureReceivingDirectoryExists().getAbsolutePath() + FileSystems.getDefault().getSeparator() + zipId + ".zip";
}
public void processFilePartReceived(FileTransferPart ftp) {
checkpointLastActivity();
// check that the supplied sequence number is in line with what we are expecting
if (currentBlockSeqNum < 0) {
// we have not yet started receiving a file, validate this ftp packet as the initiation request
initReceiveSession(ftp.uid, ftp.seqNumOrFileLength);
} else if (currentBlockSeqNum == ftp.seqNumOrFileLength) {
// we are in the middle of receiving a file; add the block of data to the file
processReceivedBlock(ftp, networkNode, peerNodeAddress);
} else {
log.error("ftp sequence num mismatch, expected {} received {}", currentBlockSeqNum, ftp.seqNumOrFileLength);
resetSession(); // aborts the file transfer
}
}
public void initReceiveSession(String uid, long expectedFileBytes) {
networkNode.addMessageListener(this);
this.expectedFileLength = expectedFileBytes;
fileOffsetBytes = 0;
currentBlockSeqNum = 0;
initSessionTimer();
log.info("Received a start file transfer request, tradeId={}, traderId={}, size={}", fullTradeId, traderId, expectedFileBytes);
log.info("New file will be written to {}", zipFilePath);
UserThread.execute(() -> ackReceivedPart(uid, networkNode, peerNodeAddress));
}
private void processReceivedBlock(FileTransferPart ftp, NetworkNode networkNode, NodeAddress peerNodeAddress) {
try {
RandomAccessFile file = new RandomAccessFile(zipFilePath, "rwd");
file.seek(fileOffsetBytes);
file.write(ftp.messageData.toByteArray(), 0, ftp.messageData.size());
fileOffsetBytes = fileOffsetBytes + ftp.messageData.size();
log.info("Sequence number {} for {}, received data {} / {}",
ftp.seqNumOrFileLength, Utilities.getShortId(ftp.tradeId), fileOffsetBytes, expectedFileLength);
currentBlockSeqNum++;
UserThread.runAfter(() -> {
ackReceivedPart(ftp.uid, networkNode, peerNodeAddress);
if (fileOffsetBytes >= expectedFileLength) {
log.info("Success! We have reached the EOF, received {} expected {}", fileOffsetBytes, expectedFileLength);
ftpCallback.ifPresent(c -> c.onFtpComplete(this));
resetSession();
}
}, 100, TimeUnit.MILLISECONDS);
} catch (IOException e) {
log.error(e.toString());
e.printStackTrace();
}
}
private void ackReceivedPart(String uid, NetworkNode networkNode, NodeAddress peerNodeAddress) {
AckMessage ackMessage = new AckMessage(peerNodeAddress,
AckMessageSourceType.LOG_TRANSFER,
FileTransferPart.class.getSimpleName(),
uid,
Utilities.getShortId(fullTradeId),
true, // result
null); // errorMessage
log.info("Send AckMessage for {} to peer {}. id={}, uid={}",
ackMessage.getSourceMsgClassName(), peerNodeAddress, ackMessage.getSourceId(), ackMessage.getSourceUid());
sendMessage(ackMessage, networkNode, peerNodeAddress);
}
private static File ensureReceivingDirectoryExists() throws IOException {
File directory = new File(Config.appDataDir() + "/clientLogs");
if (!directory.exists() && !directory.mkdirs()) {
log.error("Could not create directory {}", directory.getAbsolutePath());
throw new IOException("Could not create directory: " + directory.getAbsolutePath());
}
return directory;
}
}

View File

@ -0,0 +1,198 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.support.dispute.mediation;
import haveno.network.p2p.FileTransferPart;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.network.NetworkNode;
import haveno.common.UserThread;
import haveno.common.config.Config;
import haveno.common.util.Utilities;
import com.google.protobuf.ByteString;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static haveno.common.file.FileUtil.doesFileContainKeyword;
@Slf4j
public class FileTransferSender extends FileTransferSession {
protected final String zipFilePath;
private final boolean isTest;
public FileTransferSender(NetworkNode networkNode,
NodeAddress peerNodeAddress,
String tradeId,
int traderId,
String traderRole,
boolean isTest,
@Nullable FileTransferSession.FtpCallback callback) {
super(networkNode, peerNodeAddress, tradeId, traderId, traderRole, callback);
zipFilePath = Utilities.getUserDataDir() + FileSystems.getDefault().getSeparator() + zipId + ".zip";
this.isTest = isTest;
updateProgress();
}
public void createZipFileToSend() {
createZipFileOfLogs(zipFilePath, zipId, fullTradeId);
}
public static void createZipFileOfLogs(String zipFilePath, String zipId, String fullTradeId) {
try {
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI uri = URI.create("jar:file:///" + zipFilePath
.replace('\\', '/')
.replaceAll(" ", "%20"));
FileSystem zipfs = FileSystems.newFileSystem(uri, env);
Files.createDirectory(zipfs.getPath(zipId)); // store logfiles in a usefully-named subdir
Stream<Path> paths = Files.walk(Paths.get(Config.appDataDir().toString()), 1);
paths.filter(Files::isRegularFile).forEach(externalTxtFile -> {
try {
// always include haveno.log; and other .log files if they contain the TradeId
if (externalTxtFile.getFileName().toString().equals("haveno.log") ||
(fullTradeId == null && externalTxtFile.getFileName().toString().matches(".*.log")) ||
(externalTxtFile.getFileName().toString().matches(".*.log") &&
doesFileContainKeyword(externalTxtFile.toFile(), fullTradeId))) {
Path pathInZipfile = zipfs.getPath(zipId + "/" + externalTxtFile.getFileName().toString());
log.info("adding {} to zip file {}", pathInZipfile, zipfs);
Files.copy(externalTxtFile, pathInZipfile, StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
log.error(e.toString());
e.printStackTrace();
}
});
zipfs.close();
} catch (IOException | IllegalArgumentException ex) {
log.error(ex.toString());
ex.printStackTrace();
}
}
public void initSend() throws IOException {
initSessionTimer();
networkNode.addMessageListener(this);
RandomAccessFile file = new RandomAccessFile(zipFilePath, "r");
expectedFileLength = file.length();
file.close();
// an empty block is sent as request to initiate file transfer, peer must ACK for transfer to continue
dataAwaitingAck = Optional.of(new FileTransferPart(networkNode.getNodeAddress(), fullTradeId, traderId, UUID.randomUUID().toString(), expectedFileLength, ByteString.EMPTY));
uploadData();
}
public void sendNextBlock() throws IOException, IllegalStateException {
if (dataAwaitingAck.isPresent()) {
log.warn("prepNextBlockToSend invoked, but we are still waiting for a previous ACK");
throw new IllegalStateException("prepNextBlockToSend invoked, but we are still waiting for a previous ACK");
}
RandomAccessFile file = new RandomAccessFile(zipFilePath, "r");
file.seek(fileOffsetBytes);
byte[] buff = new byte[FILE_BLOCK_SIZE];
int nBytesRead = file.read(buff, 0, FILE_BLOCK_SIZE);
file.close();
if (nBytesRead < 0) {
log.info("Success! We have reached the EOF, {} bytes sent. Removing zip file {}", fileOffsetBytes, zipFilePath);
Files.delete(Paths.get(zipFilePath));
ftpCallback.ifPresent(c -> c.onFtpComplete(this));
UserThread.runAfter(this::resetSession, 1);
return;
}
dataAwaitingAck = Optional.of(new FileTransferPart(networkNode.getNodeAddress(), fullTradeId, traderId, UUID.randomUUID().toString(), currentBlockSeqNum, ByteString.copyFrom(buff, 0, nBytesRead)));
uploadData();
}
public void retrySend() {
if (transferIsInProgress()) {
log.info("Retry send of current block");
initSessionTimer();
uploadData();
} else {
UserThread.runAfter(() -> ftpCallback.ifPresent((f) -> f.onFtpTimeout("Could not re-send", this)), 1);
}
}
protected void uploadData() {
if (dataAwaitingAck.isEmpty()) {
return;
}
FileTransferPart ftp = dataAwaitingAck.get();
log.info("Send FileTransferPart seq {} length {} to peer {}, UID={}",
ftp.seqNumOrFileLength, ftp.messageData.size(), peerNodeAddress, ftp.uid);
sendMessage(ftp, networkNode, peerNodeAddress);
}
public boolean processAckForFilePart(String ackUid) {
if (dataAwaitingAck.isEmpty()) {
log.warn("We received an ACK we were not expecting. {}", ackUid);
return false;
}
if (!dataAwaitingAck.get().uid.equals(ackUid)) {
log.warn("We received an ACK that has a different UID to what we were expecting. We ignore and wait for the correct ACK");
log.info("Received {} expecting {}", ackUid, dataAwaitingAck.get().uid);
return false;
}
// fileOffsetBytes gets incremented by the size of the block that was ack'd
fileOffsetBytes += dataAwaitingAck.get().messageData.size();
currentBlockSeqNum++;
dataAwaitingAck = Optional.empty();
checkpointLastActivity();
updateProgress();
if (isTest) {
return true;
}
UserThread.runAfter(() -> { // to trigger continuing the file transfer
try {
sendNextBlock();
} catch (IOException e) {
log.error(e.toString());
e.printStackTrace();
}
}, 100, TimeUnit.MILLISECONDS);
return true;
}
public void updateProgress() {
double progressPct = expectedFileLength > 0 ?
((double) fileOffsetBytes / expectedFileLength) : 0.0;
ftpCallback.ifPresent(c -> c.onFtpProgress(progressPct));
log.info("ftp progress: {}", String.format("%.0f%%", progressPct * 100));
}
}

View File

@ -0,0 +1,174 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.support.dispute.mediation;
import haveno.network.p2p.AckMessage;
import haveno.network.p2p.AckMessageSourceType;
import haveno.network.p2p.FileTransferPart;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.network.Connection;
import haveno.network.p2p.network.MessageListener;
import haveno.network.p2p.network.NetworkNode;
import haveno.common.UserThread;
import haveno.common.proto.network.NetworkEnvelope;
import haveno.common.util.Utilities;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import static haveno.network.p2p.network.Connection.getPermittedMessageSize;
@Slf4j
public abstract class FileTransferSession implements MessageListener {
protected static final int FTP_SESSION_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(60);
protected static final int FILE_BLOCK_SIZE = getPermittedMessageSize() - 1024; // allowing space for protobuf
public interface FtpCallback {
void onFtpProgress(double progressPct);
void onFtpComplete(FileTransferSession session);
void onFtpTimeout(String statusMsg, FileTransferSession session);
}
@Getter
protected final String fullTradeId;
@Getter
protected final int traderId;
@Getter
protected final String zipId;
protected final Optional<FtpCallback> ftpCallback;
protected final NetworkNode networkNode; // for sending network messages
protected final NodeAddress peerNodeAddress;
protected Optional<FileTransferPart> dataAwaitingAck;
protected long fileOffsetBytes;
protected long currentBlockSeqNum;
protected long expectedFileLength;
protected long lastActivityTime;
public FileTransferSession(NetworkNode networkNode,
NodeAddress peerNodeAddress,
String tradeId,
int traderId,
String traderRole,
@Nullable FileTransferSession.FtpCallback callback) {
this.networkNode = networkNode;
this.peerNodeAddress = peerNodeAddress;
this.fullTradeId = tradeId;
this.traderId = traderId;
this.ftpCallback = Optional.ofNullable(callback);
this.zipId = Utilities.getShortId(fullTradeId) + "_" + traderRole.toUpperCase() + "_"
+ new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
resetSession();
}
public void resetSession() {
lastActivityTime = 0;
currentBlockSeqNum = -1;
fileOffsetBytes = 0;
expectedFileLength = 0;
dataAwaitingAck = Optional.empty();
networkNode.removeMessageListener(this);
log.info("Ftp session parameters have been reset.");
}
@Override
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
if (networkEnvelope instanceof FileTransferPart) {
// mediator receiving log file data
FileTransferPart ftp = (FileTransferPart) networkEnvelope;
if (this instanceof FileTransferReceiver) {
((FileTransferReceiver) this).processFilePartReceived(ftp);
}
} else if (networkEnvelope instanceof AckMessage) {
AckMessage ackMessage = (AckMessage) networkEnvelope;
if (ackMessage.getSourceType() == AckMessageSourceType.LOG_TRANSFER) {
if (ackMessage.isSuccess()) {
log.info("Received AckMessage for {} with id {} and uid {}",
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
if (this instanceof FileTransferSender) {
((FileTransferSender) this).processAckForFilePart(ackMessage.getSourceUid());
}
} else {
log.warn("Received AckMessage with error state for {} with id {} and errorMessage={}",
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage());
}
}
}
}
protected void checkpointLastActivity() {
lastActivityTime = System.currentTimeMillis();
}
protected void initSessionTimer() {
UserThread.runAfter(() -> {
if (!transferIsInProgress()) // transfer may have finished before this timer executes
return;
if (System.currentTimeMillis() - lastActivityTime < FTP_SESSION_TIMEOUT_MILLIS) {
log.info("Last activity was {}, we have not yet timed out.", new Date(lastActivityTime));
initSessionTimer();
} else {
log.warn("File transfer session timed out. expected: {} received: {}", expectedFileLength, fileOffsetBytes);
ftpCallback.ifPresent((e) -> e.onFtpTimeout("Timed out during send", this));
}
}, FTP_SESSION_TIMEOUT_MILLIS / 4, TimeUnit.MILLISECONDS); // check more frequently than the timeout
}
protected boolean transferIsInProgress() {
return fileOffsetBytes != expectedFileLength;
}
protected void sendMessage(NetworkEnvelope message, NetworkNode networkNode, NodeAddress nodeAddress) {
SettableFuture<Connection> future = networkNode.sendMessage(nodeAddress, message);
if (future != null) { // is null when testing with Mockito
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(Connection connection) {
}
@Override
public void onFailure(@NotNull Throwable throwable) {
String errorSend = "Sending " + message.getClass().getSimpleName() +
" to " + nodeAddress.getFullAddress() +
" failed. That is expected if the peer is offline.\n\t" +
".\n\tException=" + throwable.getMessage();
log.warn(errorSend);
ftpCallback.ifPresent((f) -> f.onFtpTimeout("Peer offline", FileTransferSession.this));
resetSession();
}
}, MoreExecutors.directExecutor());
}
}
}

View File

@ -17,25 +17,25 @@
package haveno.core.support.messages;
import haveno.common.app.Version;
import haveno.common.util.Utilities;
import haveno.core.locale.Res;
import haveno.core.support.SupportType;
import haveno.core.support.dispute.Attachment;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeResult;
import haveno.network.p2p.NodeAddress;
import haveno.common.UserThread;
import haveno.common.app.Version;
import haveno.common.util.Utilities;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@ -44,13 +44,22 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.lang.ref.WeakReference;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
/* Message for direct communication between two nodes. Originally built for trader to
* arbitrator communication as no other direct communication was allowed. Arbitrator is
* considered as the server and trader as the client in arbitration chats
*
* For trader to trader communication the maker is considered to be the server
* and the taker is considered as the client.
* */
*/
@EqualsAndHashCode(callSuper = true) // listener is transient and therefore excluded anyway
@Getter
@Slf4j
@ -84,14 +93,14 @@ public final class ChatMessage extends SupportMessage {
private final StringProperty sendMessageErrorProperty;
private final StringProperty ackErrorProperty;
transient private Listener listener;
transient private WeakReference<Listener> listener;
public ChatMessage(SupportType supportType,
String tradeId,
int traderId,
boolean senderIsTrader,
String message,
NodeAddress senderNodeAddress) {
String tradeId,
int traderId,
boolean senderIsTrader,
String message,
NodeAddress senderNodeAddress) {
this(supportType,
tradeId,
traderId,
@ -111,12 +120,12 @@ public final class ChatMessage extends SupportMessage {
}
public ChatMessage(SupportType supportType,
String tradeId,
int traderId,
boolean senderIsTrader,
String message,
NodeAddress senderNodeAddress,
ArrayList<Attachment> attachments) {
String tradeId,
int traderId,
boolean senderIsTrader,
String message,
NodeAddress senderNodeAddress,
ArrayList<Attachment> attachments) {
this(supportType,
tradeId,
traderId,
@ -136,12 +145,12 @@ public final class ChatMessage extends SupportMessage {
}
public ChatMessage(SupportType supportType,
String tradeId,
int traderId,
boolean senderIsTrader,
String message,
NodeAddress senderNodeAddress,
long date) {
String tradeId,
int traderId,
boolean senderIsTrader,
String message,
NodeAddress senderNodeAddress,
long date) {
this(supportType,
tradeId,
traderId,
@ -198,7 +207,9 @@ public final class ChatMessage extends SupportMessage {
notifyChangeListener();
}
public protobuf.ChatMessage.Builder toProtoChatMessageBuilder() {
// We cannot rename protobuf definition because it would break backward compatibility
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.ChatMessage.Builder builder = protobuf.ChatMessage.newBuilder()
.setType(SupportType.toProtoMessage(supportType))
.setTradeId(tradeId)
@ -216,14 +227,6 @@ public final class ChatMessage extends SupportMessage {
.setWasDisplayed(wasDisplayed);
Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError);
Optional.ofNullable(ackErrorProperty.get()).ifPresent(builder::setAckError);
return builder;
}
// We cannot rename protobuf definition because it would break backward compatibility
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.ChatMessage.Builder builder = toProtoChatMessageBuilder();
return getNetworkEnvelopeBuilder()
.setChatMessage(builder)
.build();
@ -296,6 +299,16 @@ public final class ChatMessage extends SupportMessage {
notifyChangeListener();
}
// each chat message notifies the user if an ACK is not received in time
public void startAckTimer() {
UserThread.runAfter(() -> {
if (!this.getAcknowledgedProperty().get() && !this.getStoredInMailboxProperty().get()) {
this.setArrived(false);
this.setAckError(Res.get("support.errorTimeout"));
}
}, 60, TimeUnit.SECONDS);
}
public ReadOnlyBooleanProperty acknowledgedProperty() {
return acknowledgedProperty;
}
@ -327,12 +340,8 @@ public final class ChatMessage extends SupportMessage {
return Utilities.getShortId(tradeId);
}
public void addChangeListener(Listener listener) {
this.listener = listener;
}
public void removeChangeListener() {
this.listener = null;
public void addWeakMessageStateListener(Listener listener) {
this.listener = new WeakReference<>(listener);
}
public boolean isResultMessage(Dispute dispute) {
@ -352,7 +361,10 @@ public final class ChatMessage extends SupportMessage {
private void notifyChangeListener() {
if (listener != null) {
listener.onMessageStateChanged();
Listener listener = this.listener.get();
if (listener != null) {
listener.onMessageStateChanged();
}
}
}

View File

@ -1174,11 +1174,28 @@ support.chat=Chat
support.requested=Requested
support.closed=Closed
support.open=Open
support.moreButton=MORE...
support.sendLogFiles=Send Log Files
support.uploadTraderChat=Upload Trader Chat
support.process=Process
support.buyerMaker=XMR Buyer/Maker
support.sellerMaker=XMR Seller/Maker
support.buyerTaker=XMR Buyer/Taker
support.sellerTaker=XMR Seller/Taker
support.sendLogs.title=Send Log Files
support.sendLogs.backgroundInfo=When you experience a bug, arbitrators and support staff will often request copies of the your log files to diagnose the issue.\n\n\
Upon pressing 'Send', your log files will be compressed and transmitted directly to the arbitrator.
support.sendLogs.step1=Create Zip Archive of Log Files
support.sendLogs.step2=Connection Request to Arbitrator
support.sendLogs.step3=Upload Archived Log Data
support.sendLogs.send=Send
support.sendLogs.cancel=Cancel
support.sendLogs.init=Initializing
support.sendLogs.retry=Retrying send
support.sendLogs.stopped=Transfer stopped
support.sendLogs.progress=Transfer progress: %.0f%%
support.sendLogs.finished=Transfer complete!
support.sendLogs.command=Press 'Send' to retry, or 'Stop' to abort
support.txKeyImages=Key Images
support.txHash=Transaction Hash
support.txHex=Transaction Hex
@ -2312,6 +2329,7 @@ peerInfoIcon.tooltip.trade.traded={0} onion address: {1}\nYou have already trade
peerInfoIcon.tooltip.trade.notTraded={0} onion address: {1}\nYou have not traded with that peer so far.\n{2}
peerInfoIcon.tooltip.age=Payment account created {0} ago.
peerInfoIcon.tooltip.unknownAge=Payment account age not known.
peerInfoIcon.tooltip.dispute={0}\nNumber of disputes: {1}.\n{2}
tooltip.openPopupForDetails=Open popup for details
tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information

View File

@ -0,0 +1,241 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.support.dispute.mediation;
import haveno.network.p2p.FileTransferPart;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.network.NetworkNode;
import haveno.common.config.Config;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class FileTransferSessionTest implements FileTransferSession.FtpCallback {
double notedProgressPct = -1.0;
int progressInvocations = 0;
boolean ftpCompleteStatus = false;
String testTradeId = "foo";
int testTraderId = 123;
String testClientId = "bar";
NetworkNode networkNode;
NodeAddress counterpartyNodeAddress;
@BeforeEach
public void setUp() throws Exception {
new Config(); // static methods like Config.appDataDir() require config to be created once
networkNode = mock(NetworkNode.class);
when(networkNode.getNodeAddress()).thenReturn(new NodeAddress("null:0000"));
counterpartyNodeAddress = new NodeAddress("null:0000");
}
@Test
public void testSendCreate() {
new FileTransferSender(networkNode, counterpartyNodeAddress, testTradeId, testTraderId, testClientId, true, this);
assertEquals(0.0, notedProgressPct, 0.0);
assertEquals(1, progressInvocations);
}
@Test
public void testCreateZip() {
FileTransferSender sender = new FileTransferSender(networkNode, counterpartyNodeAddress, testTradeId, testTraderId, testClientId, true, this);
assertEquals(0.0, notedProgressPct, 0.0);
assertEquals(1, progressInvocations);
sender.createZipFileToSend();
File file = new File(sender.zipFilePath);
assertTrue(file.getAbsoluteFile().exists());
assertTrue(file.getAbsoluteFile().length() > 0);
file.deleteOnExit();
}
@Test
public void testSendInitialize() {
// checks that the initial send request packet contains correct information
try {
int testVerifyDataSize = 13;
FileTransferSender session = initializeSession(testVerifyDataSize);
session.initSend();
FileTransferPart ftp = session.dataAwaitingAck.get();
assertEquals(ftp.tradeId, testTradeId);
assertTrue(ftp.uid.length() > 0);
assertEquals(0, ftp.messageData.size());
assertEquals(ftp.seqNumOrFileLength, testVerifyDataSize);
assertEquals(-1, session.currentBlockSeqNum);
return;
} catch (IOException e) {
e.printStackTrace();
}
fail();
}
@Test
public void testSendSmallFile() {
try {
int testVerifyDataSize = 13;
FileTransferSender session = initializeSession(testVerifyDataSize);
// the first block contains zero data, as it is a "request to send"
session.initSend();
simulateAckFromPeerAndVerify(session, 0, 0, 2);
// the second block contains all the test file data (because it is a small file)
session.sendNextBlock();
simulateAckFromPeerAndVerify(session, testVerifyDataSize, 1, 3);
// the final invocation sends no data, and wraps up the session
session.sendNextBlock();
assertEquals(1, session.currentBlockSeqNum);
assertEquals(3, progressInvocations);
assertEquals(1.0, notedProgressPct, 0.0);
assertTrue(ftpCompleteStatus);
} catch (IOException ioe) {
ioe.printStackTrace();
fail();
}
}
@Test
public void testSendOneFullBlock() {
try {
int testVerifyDataSize = FileTransferSession.FILE_BLOCK_SIZE;
FileTransferSender session = initializeSession(testVerifyDataSize);
// the first block contains zero data, as it is a "request to send"
session.initSend();
simulateAckFromPeerAndVerify(session, 0, 0, 2);
// the second block contains all the test file data (because it is a small file)
session.sendNextBlock();
simulateAckFromPeerAndVerify(session, testVerifyDataSize, 1, 3);
// the final invocation sends no data, and wraps up the session
session.sendNextBlock();
assertEquals(1, session.currentBlockSeqNum);
assertEquals(3, progressInvocations);
assertEquals(1.0, notedProgressPct, 0.0);
assertTrue(ftpCompleteStatus);
} catch (IOException ioe) {
ioe.printStackTrace();
fail();
}
}
@Test
public void testSendTwoFullBlocks() {
try {
int testVerifyDataSize = FileTransferSession.FILE_BLOCK_SIZE * 2;
FileTransferSender session = initializeSession(testVerifyDataSize);
// the first block contains zero data, as it is a "request to send"
session.initSend();
simulateAckFromPeerAndVerify(session, 0, 0, 2);
// the second block contains half of the test file data
session.sendNextBlock();
simulateAckFromPeerAndVerify(session, testVerifyDataSize / 2, 1, 3);
// the third block contains half of the test file data
session.sendNextBlock();
simulateAckFromPeerAndVerify(session, testVerifyDataSize / 2, 2, 4);
// the final invocation sends no data, and wraps up the session
session.sendNextBlock();
assertEquals(2, session.currentBlockSeqNum);
assertEquals(4, progressInvocations);
assertEquals(1.0, notedProgressPct, 0.0);
assertTrue(ftpCompleteStatus);
} catch (IOException ioe) {
ioe.printStackTrace();
fail();
}
}
@Test
public void testSendTwoFullBlocksPlusOneByte() {
try {
int testVerifyDataSize = 1 + FileTransferSession.FILE_BLOCK_SIZE * 2;
FileTransferSender session = initializeSession(testVerifyDataSize);
// the first block contains zero data, as it is a "request to send"
session.initSend();
simulateAckFromPeerAndVerify(session, 0, 0, 2);
session.sendNextBlock();
simulateAckFromPeerAndVerify(session, FileTransferSession.FILE_BLOCK_SIZE, 1, 3);
session.sendNextBlock();
simulateAckFromPeerAndVerify(session, FileTransferSession.FILE_BLOCK_SIZE, 2, 4);
// the fourth block contains one byte
session.sendNextBlock();
simulateAckFromPeerAndVerify(session, 1, 3, 5);
// the final invocation sends no data, and wraps up the session
session.sendNextBlock();
assertEquals(3, session.currentBlockSeqNum);
assertEquals(5, progressInvocations);
assertEquals(1.0, notedProgressPct, 0.0);
assertTrue(ftpCompleteStatus);
} catch (IOException ioe) {
ioe.printStackTrace();
fail();
}
}
private FileTransferSender initializeSession(int testSize) {
try {
FileTransferSender session = new FileTransferSender(networkNode, counterpartyNodeAddress, testTradeId, testTraderId, testClientId, true, this);
// simulate a file for sending
FileWriter fileWriter = new FileWriter(session.zipFilePath);
char[] buf = new char[testSize];
for (int x = 0; x < testSize; x++)
buf[x] = 'A';
fileWriter.write(buf);
fileWriter.close();
assertFalse(ftpCompleteStatus);
assertEquals(1, progressInvocations);
assertEquals(0.0, notedProgressPct, 0.0);
assertFalse(session.processAckForFilePart("not_expected_uid"));
return session;
} catch (IOException e) {
e.printStackTrace();
}
fail();
return null;
}
private void simulateAckFromPeerAndVerify(FileTransferSender session, int expectedDataSize, long expectedSeqNum, int expectedProgressInvocations) {
FileTransferPart ftp = session.dataAwaitingAck.get();
assertEquals(expectedDataSize, ftp.messageData.size());
assertTrue(session.processAckForFilePart(ftp.uid));
assertEquals(expectedSeqNum, session.currentBlockSeqNum);
assertEquals(expectedProgressInvocations, progressInvocations);
}
@Override
public void onFtpProgress(double progressPct) {
notedProgressPct = progressPct;
progressInvocations++;
}
@Override
public void onFtpComplete(FileTransferSession session) {
ftpCompleteStatus = true;
}
@Override
public void onFtpTimeout(String status, FileTransferSession session) {
}
}

View File

@ -17,40 +17,46 @@
package haveno.desktop.components;
import com.google.common.base.Charsets;
import haveno.desktop.main.overlays.editor.PeerInfoWithTagEditor;
import haveno.desktop.util.DisplayUtils;
import haveno.core.alert.PrivateNotificationManager;
import haveno.core.locale.Res;
import haveno.core.offer.Offer;
import haveno.core.trade.Trade;
import haveno.core.user.Preferences;
import haveno.desktop.main.overlays.editor.PeerInfoWithTagEditor;
import haveno.desktop.util.DisplayUtils;
import haveno.network.p2p.NodeAddress;
import javafx.geometry.Point2D;
import com.google.common.base.Charsets;
import javafx.scene.Group;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import lombok.Setter;
import javafx.geometry.Point2D;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
@Slf4j
public class PeerInfoIcon extends Group {
public interface notify {
void avatarTagUpdated();
}
@Setter
private notify callback;
protected Preferences preferences;
protected final String fullAddress;
protected String tooltipText;
@ -59,10 +65,12 @@ public class PeerInfoIcon extends Group {
protected Pane tagPane;
protected Pane numTradesPane;
protected int numTrades = 0;
private final StringProperty tag;
public PeerInfoIcon(NodeAddress nodeAddress, Preferences preferences) {
this.preferences = preferences;
this.fullAddress = nodeAddress != null ? nodeAddress.getFullAddress() : "";
this.tag = new SimpleStringProperty("");
}
protected void createAvatar(Color ringColor) {
@ -162,23 +170,24 @@ public class PeerInfoIcon extends Group {
Res.get("peerInfo.unknownAge") :
null;
setOnMouseClicked(e -> new PeerInfoWithTagEditor(privateNotificationManager, trade, offer, preferences, useDevPrivilegeKeys)
.fullAddress(fullAddress)
.numTrades(numTrades)
.accountAge(accountAgeFormatted)
.signAge(signAgeFormatted)
.accountAgeInfo(peersAccountAgeInfo)
.signAgeInfo(peersSignAgeInfo)
.accountSigningState(accountSigningState)
.position(localToScene(new Point2D(0, 0)))
.onSave(newTag -> {
preferences.setTagForPeer(fullAddress, newTag);
updatePeerInfoIcon();
if (callback != null) {
callback.avatarTagUpdated();
}
})
.show());
setOnMouseClicked(e -> {
if (e.getButton().equals(MouseButton.PRIMARY)) {
new PeerInfoWithTagEditor(privateNotificationManager, trade, offer, preferences, useDevPrivilegeKeys)
.fullAddress(fullAddress)
.numTrades(numTrades)
.accountAge(accountAgeFormatted)
.signAge(signAgeFormatted)
.accountAgeInfo(peersAccountAgeInfo)
.signAgeInfo(peersSignAgeInfo)
.accountSigningState(accountSigningState)
.position(localToScene(new Point2D(0, 0)))
.onSave(newTag -> {
preferences.setTagForPeer(fullAddress, newTag);
tag.set(newTag);
})
.show();
}
});
}
protected double getScaleFactor() {
@ -192,20 +201,6 @@ public class PeerInfoIcon extends Group {
}
protected void updatePeerInfoIcon() {
String tag;
Map<String, String> peerTagMap = preferences.getPeerTagMap();
if (peerTagMap.containsKey(fullAddress)) {
tag = peerTagMap.get(fullAddress);
final String text = !tag.isEmpty() ? Res.get("peerInfoIcon.tooltip", tooltipText, tag) : tooltipText;
Tooltip.install(this, new Tooltip(text));
} else {
tag = "";
Tooltip.install(this, new Tooltip(tooltipText));
}
if (!tag.isEmpty())
tagLabel.setText(tag.substring(0, 1));
if (numTrades > 0) {
numTradesLabel.setText(numTrades > 99 ? "*" : String.valueOf(numTrades));
@ -216,9 +211,27 @@ public class PeerInfoIcon extends Group {
numTradesLabel.relocate(scaleFactor * 5, scaleFactor * 1);
}
}
numTradesPane.setVisible(numTrades > 0);
tagPane.setVisible(!tag.isEmpty());
refreshTag();
}
protected void refreshTag() {
Map<String, String> peerTagMap = preferences.getPeerTagMap();
if (peerTagMap.containsKey(fullAddress)) {
tag.set(peerTagMap.get(fullAddress));
}
Tooltip.install(this, new Tooltip(!tag.get().isEmpty() ?
Res.get("peerInfoIcon.tooltip", tooltipText, tag.get()) : tooltipText));
if (!tag.get().isEmpty()) {
tagLabel.setText(tag.get().substring(0, 1));
}
tagPane.setVisible(!tag.get().isEmpty());
}
protected StringProperty tagProperty() {
return tag;
}
}

View File

@ -40,8 +40,4 @@ public class PeerInfoIconDispute extends PeerInfoIcon {
addMouseListener(numTrades, null, null, null, preferences, false,
false, accountAge, 0L, null, null, null);
}
public void refreshTag() {
updatePeerInfoIcon();
}
}

View File

@ -0,0 +1,48 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import java.util.HashMap;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class PeerInfoIconMap extends HashMap<String, PeerInfoIcon> implements ChangeListener<String> {
@Override
public PeerInfoIcon put(String key, PeerInfoIcon icon) {
icon.tagProperty().addListener(this);
return super.put(key, icon);
}
@Override
public void changed(ObservableValue<? extends String> o, String oldVal, String newVal) {
log.info("Updating avatar tags, the avatar map size is {}", size());
forEach((key, icon) -> {
// We update all avatars, as some could be sharing the same tag.
// We also temporarily remove listeners to prevent firing of
// events while each icon's tagProperty is being reset.
icon.tagProperty().removeListener(this);
icon.refreshTag();
icon.tagProperty().addListener(this);
});
}
}

View File

@ -0,0 +1,257 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.main.overlays.windows;
import haveno.desktop.components.AutoTooltipButton;
import haveno.desktop.main.overlays.Overlay;
import haveno.desktop.main.portfolio.pendingtrades.steps.TradeWizardItem;
import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep1View;
import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep2View;
import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep3View;
import haveno.desktop.util.Layout;
import haveno.core.locale.Res;
import haveno.core.support.dispute.arbitration.ArbitrationManager;
import haveno.core.support.dispute.mediation.FileTransferSender;
import haveno.core.support.dispute.mediation.FileTransferSession;
import haveno.common.UserThread;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.Separator;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import static haveno.desktop.util.FormBuilder.addMultilineLabel;
@Slf4j
public class SendLogFilesWindow extends Overlay<SendLogFilesWindow> implements FileTransferSession.FtpCallback {
private final String tradeId;
private final int traderId;
private final ArbitrationManager arbitrationManager;
private Label statusLabel;
private Button sendButton, stopButton;
private final DoubleProperty ftpProgress = new SimpleDoubleProperty(-1);
TradeWizardItem step1, step2, step3;
private FileTransferSender fileTransferSender;
public SendLogFilesWindow(String tradeId, int traderId,
ArbitrationManager arbitrationManager) {
this.tradeId = tradeId;
this.traderId = traderId;
this.arbitrationManager = arbitrationManager;
type = Type.Attention;
}
public void show() {
headLine = Res.get("support.sendLogs.title");
width = 668;
createGridPane();
addHeadLine();
addContent();
addButtons();
applyStyles();
display();
}
@Override
protected void createGridPane() {
gridPane = new GridPane();
gridPane.setHgap(5);
gridPane.setVgap(5);
gridPane.setPadding(new Insets(64, 64, 64, 64));
gridPane.setPrefWidth(width);
}
void addWizardsToGridPane(TradeWizardItem tradeWizardItem) {
GridPane.setRowIndex(tradeWizardItem, rowIndex++);
GridPane.setColumnIndex(tradeWizardItem, 0);
GridPane.setHalignment(tradeWizardItem, HPos.LEFT);
gridPane.getChildren().add(tradeWizardItem);
}
void addLineSeparatorToGridPane() {
final Separator separator = new Separator(Orientation.VERTICAL);
separator.setMinHeight(22);
GridPane.setMargin(separator, new Insets(0, 0, 0, 13));
GridPane.setHalignment(separator, HPos.LEFT);
GridPane.setRowIndex(separator, rowIndex++);
gridPane.getChildren().add(separator);
}
void addRegionToGridPane() {
final Region region = new Region();
region.setMinHeight(22);
GridPane.setMargin(region, new Insets(0, 0, 0, 13));
GridPane.setRowIndex(region, rowIndex++);
gridPane.getChildren().add(region);
}
private void addContent() {
this.hideCloseButton = true;
addMultilineLabel(gridPane, ++rowIndex, Res.get("support.sendLogs.backgroundInfo"), 0);
addRegionToGridPane();
step1 = new TradeWizardItem(BuyerStep1View.class, Res.get("support.sendLogs.step1"), "1");
step2 = new TradeWizardItem(BuyerStep2View.class, Res.get("support.sendLogs.step2"), "2");
step3 = new TradeWizardItem(BuyerStep3View.class, Res.get("support.sendLogs.step3"), "3");
addRegionToGridPane();
addRegionToGridPane();
addWizardsToGridPane(step1);
addLineSeparatorToGridPane();
addWizardsToGridPane(step2);
addLineSeparatorToGridPane();
addWizardsToGridPane(step3);
addRegionToGridPane();
ProgressBar progressBar = new ProgressBar();
progressBar.setMinHeight(19);
progressBar.setMaxHeight(19);
progressBar.setPrefWidth(9305);
progressBar.setVisible(false);
progressBar.progressProperty().bind(ftpProgress);
gridPane.add(progressBar, 0, ++rowIndex);
statusLabel = addMultilineLabel(gridPane, ++rowIndex, "", -Layout.FLOATING_LABEL_DISTANCE);
statusLabel.getStyleClass().add("sub-info");
addRegionToGridPane();
sendButton = new AutoTooltipButton(Res.get("support.sendLogs.send"));
stopButton = new AutoTooltipButton(Res.get("support.sendLogs.cancel"));
stopButton.setDisable(true);
closeButton = new AutoTooltipButton(Res.get("shared.close"));
sendButton.setOnAction(e -> {
try {
progressBar.setVisible(true);
if (fileTransferSender == null) {
setActiveStep(1);
statusLabel.setText(Res.get("support.sendLogs.init"));
fileTransferSender = arbitrationManager.initLogUpload(this, tradeId, traderId);
UserThread.runAfter(() -> {
fileTransferSender.createZipFileToSend();
setActiveStep(2);
UserThread.runAfter(() -> {
setActiveStep(3);
try {
fileTransferSender.initSend();
} catch (IOException ioe) {
log.error(ioe.toString());
statusLabel.setText(ioe.toString());
ioe.printStackTrace();
}
}, 1);
}, 1);
sendButton.setDisable(true);
stopButton.setDisable(false);
} else {
// resend the latest block in the event of a timeout
statusLabel.setText(Res.get("support.sendLogs.retry"));
fileTransferSender.retrySend();
sendButton.setDisable(true);
}
} catch (IOException ex) {
log.error(ex.toString());
statusLabel.setText(ex.toString());
ex.printStackTrace();
}
});
stopButton.setOnAction(e -> {
if (fileTransferSender != null) {
fileTransferSender.resetSession();
statusLabel.setText(Res.get("support.sendLogs.stopped"));
stopButton.setDisable(true);
}
});
closeButton.setOnAction(e -> {
hide();
closeHandlerOptional.ifPresent(Runnable::run);
});
HBox hBox = new HBox();
hBox.setSpacing(10);
hBox.setAlignment(Pos.CENTER_RIGHT);
GridPane.setRowIndex(hBox, ++rowIndex);
GridPane.setColumnSpan(hBox, 2);
GridPane.setColumnIndex(hBox, 0);
hBox.getChildren().addAll(sendButton, stopButton, closeButton);
gridPane.getChildren().add(hBox);
GridPane.setMargin(hBox, new Insets(10, 0, 0, 0));
}
void setActiveStep(int step) {
if (step < 1) {
step1.setDisabled();
step2.setDisabled();
step3.setDisabled();
} else if (step == 1) {
step1.setActive();
} else if (step == 2) {
step1.setCompleted();
step2.setActive();
} else if (step == 3) {
step2.setCompleted();
step3.setActive();
} else {
step3.setCompleted();
}
}
@Override
public void onFtpProgress(double progressPct) {
UserThread.execute(() -> {
if (progressPct > 0.0) {
statusLabel.setText(String.format(Res.get("support.sendLogs.progress"), progressPct * 100));
sendButton.setDisable(true);
}
ftpProgress.set(progressPct);
});
}
@Override
public void onFtpComplete(FileTransferSession session) {
UserThread.execute(() -> {
setActiveStep(4); // all finished
statusLabel.setText(Res.get("support.sendLogs.finished"));
stopButton.setDisable(true);
});
}
@Override
public void onFtpTimeout(String statusMsg, FileTransferSession session) {
UserThread.execute(() -> {
statusLabel.setText(statusMsg + "\r\n" + Res.get("support.sendLogs.command"));
sendButton.setDisable(false);
});
}
}

View File

@ -443,7 +443,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
model.dataModel.getTradeManager().requestPersistence();
tradeIdOfOpenChat = trade.getId();
ChatView chatView = new ChatView(traderChatManager, formatter, Res.get("offerbook.trader"));
ChatView chatView = new ChatView(traderChatManager, Res.get("offerbook.trader"));
chatView.setAllowAttachments(false);
chatView.setDisplayHeader(false);
chatView.initialize();

View File

@ -17,35 +17,36 @@
package haveno.desktop.main.shared;
import com.google.common.io.ByteStreams;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.util.Utilities;
import haveno.desktop.components.AutoTooltipButton;
import haveno.desktop.components.AutoTooltipLabel;
import haveno.desktop.components.HavenoTextArea;
import haveno.desktop.components.BusyAnimation;
import haveno.desktop.components.TableGroupHeadline;
import haveno.desktop.main.overlays.notifications.Notification;
import haveno.desktop.main.overlays.popups.Popup;
import haveno.desktop.util.DisplayUtils;
import haveno.desktop.util.GUIUtil;
import haveno.core.locale.Res;
import haveno.core.support.SupportManager;
import haveno.core.support.SupportSession;
import haveno.core.support.dispute.Attachment;
import haveno.core.support.messages.ChatMessage;
import haveno.core.util.coin.CoinFormatter;
import haveno.desktop.components.AutoTooltipButton;
import haveno.desktop.components.AutoTooltipLabel;
import haveno.desktop.components.BusyAnimation;
import haveno.desktop.components.HavenoTextArea;
import haveno.desktop.components.TableGroupHeadline;
import haveno.desktop.components.TextFieldWithIcon;
import haveno.desktop.main.overlays.popups.Popup;
import haveno.desktop.util.DisplayUtils;
import haveno.desktop.util.GUIUtil;
import haveno.network.p2p.network.Connection;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.util.Utilities;
import com.google.common.io.ByteStreams;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import javafx.stage.FileChooser;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
@ -61,22 +62,31 @@ import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
import javafx.stage.FileChooser;
import javafx.util.Callback;
import lombok.Getter;
import lombok.Setter;
import javafx.geometry.Insets;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.util.Callback;
import java.net.MalformedURLException;
import java.net.URL;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
@ -84,8 +94,14 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public class ChatView extends AnchorPane {
public static final Logger log = LoggerFactory.getLogger(TextFieldWithIcon.class);
// UI
private TextArea inputTextArea;
@ -98,7 +114,7 @@ public class ChatView extends AnchorPane {
// Options
@Getter
Button extraButton;
Node extraButton;
@Getter
private ReadOnlyDoubleProperty widthProperty;
@Setter
@ -112,18 +128,16 @@ public class ChatView extends AnchorPane {
private ListChangeListener<ChatMessage> disputeDirectMessageListListener;
private Subscription inputTextAreaTextSubscription;
private final List<Attachment> tempAttachments = new ArrayList<>();
private ChangeListener<Boolean> storedInMailboxPropertyListener, arrivedPropertyListener;
private ChangeListener<Boolean> storedInMailboxPropertyListener, acknowledgedPropertyListener;
private ChangeListener<String> sendMessageErrorPropertyListener;
protected final CoinFormatter formatter;
private EventHandler<KeyEvent> keyEventEventHandler;
private SupportManager supportManager;
private Optional<SupportSession> optionalSupportSession = Optional.empty();
private String counterpartyName;
public ChatView(SupportManager supportManager, CoinFormatter formatter, String counterpartyName) {
public ChatView(SupportManager supportManager, String counterpartyName) {
this.supportManager = supportManager;
this.formatter = formatter;
this.counterpartyName = counterpartyName;
allowAttachments = true;
displayHeader = true;
@ -157,7 +171,7 @@ public class ChatView extends AnchorPane {
}
public void display(SupportSession supportSession,
@Nullable Button extraButton,
@Nullable Node extraButton,
ReadOnlyDoubleProperty widthProperty) {
optionalSupportSession = Optional.of(supportSession);
removeListenersOnSessionChange();
@ -201,6 +215,10 @@ public class ChatView extends AnchorPane {
Button uploadButton = new AutoTooltipButton(Res.get("support.addAttachments"));
uploadButton.setOnAction(e -> onRequestUpload());
Button clipboardButton = new AutoTooltipButton(Res.get("shared.copyToClipboard"));
clipboardButton.setOnAction(e -> copyChatMessagesToClipboard(clipboardButton));
uploadButton.setStyle("-fx-pref-width: 125; -fx-padding: 3 3 3 3;");
clipboardButton.setStyle("-fx-pref-width: 125; -fx-padding: 3 3 3 3;");
sendMsgInfoLabel = new AutoTooltipLabel();
sendMsgInfoLabel.setVisible(false);
@ -216,12 +234,11 @@ public class ChatView extends AnchorPane {
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
if (allowAttachments)
buttonBox.getChildren().addAll(sendButton, uploadButton, sendMsgBusyAnimation, sendMsgInfoLabel);
buttonBox.getChildren().addAll(sendButton, uploadButton, clipboardButton, sendMsgBusyAnimation, sendMsgInfoLabel);
else
buttonBox.getChildren().addAll(sendButton, sendMsgBusyAnimation, sendMsgInfoLabel);
if (extraButton != null) {
extraButton.setDefaultButton(true);
Pane spacer = new Pane();
HBox.setHgrow(spacer, Priority.ALWAYS);
buttonBox.getChildren().addAll(spacer, extraButton);
@ -329,7 +346,7 @@ public class ChatView extends AnchorPane {
bg.setId("message-bubble-green");
messageLabel.getStyleClass().add("my-message");
copyIcon.getStyleClass().add("my-message");
message.addChangeListener(() -> updateMsgState(message));
message.addWeakMessageStateListener(() -> updateMsgState(message));
updateMsgState(message);
} else if (isMyMsg) {
headerLabel.getStyleClass().add("my-message-header");
@ -350,7 +367,7 @@ public class ChatView extends AnchorPane {
};
sendMsgBusyAnimation.isRunningProperty().addListener(sendMsgBusyAnimationListener);
message.addChangeListener(() -> updateMsgState(message));
message.addWeakMessageStateListener(() -> updateMsgState(message));
updateMsgState(message);
} else {
headerLabel.getStyleClass().add("message-header");
@ -401,13 +418,13 @@ public class ChatView extends AnchorPane {
String metaData = DisplayUtils.formatDateTime(new Date(message.getDate()));
if (!message.isSystemMessage())
metaData = (isMyMsg ? "Sent " : "Received ") + metaData
+ (isMyMsg ? "" : " from " + counterpartyName);
+ (isMyMsg ? "" : " from " + counterpartyName);
headerLabel.setText(metaData);
messageLabel.setText(message.getMessage());
attachmentsBox.getChildren().clear();
if (allowAttachments &&
message.getAttachments() != null &&
!message.getAttachments().isEmpty()) {
message.getAttachments().size() > 0) {
AnchorPane.setBottomAnchor(messageLabel, bottomBorder + attachmentsBoxHeight + 10);
attachmentsBox.getChildren().add(new AutoTooltipLabel(Res.get("support.attachments") + " ") {{
setPadding(new Insets(0, 0, 3, 0));
@ -466,6 +483,10 @@ public class ChatView extends AnchorPane {
visible = true;
icon = AwesomeIcon.OK_SIGN;
text = Res.get("support.acknowledged");
} else if (message.storedInMailboxProperty().get()) {
visible = true;
icon = AwesomeIcon.ENVELOPE;
text = Res.get("support.savedInMailbox");
} else if (message.ackErrorProperty().get() != null) {
visible = true;
icon = AwesomeIcon.EXCLAMATION_SIGN;
@ -474,12 +495,8 @@ public class ChatView extends AnchorPane {
statusInfoLabel.getStyleClass().add("error-text");
} else if (message.arrivedProperty().get()) {
visible = true;
icon = AwesomeIcon.OK;
text = Res.get("support.arrived");
} else if (message.storedInMailboxProperty().get()) {
visible = true;
icon = AwesomeIcon.ENVELOPE;
text = Res.get("support.savedInMailbox");
icon = AwesomeIcon.MAIL_REPLY;
text = Res.get("support.transient");
} else {
visible = false;
log.debug("updateMsgState called but no msg state available. message={}", message);
@ -529,7 +546,7 @@ public class ChatView extends AnchorPane {
int maxMsgSize = Connection.getPermittedMessageSize();
int maxSizeInKB = maxMsgSize / 1024;
fileChooser.setTitle(Res.get("support.openFile", maxSizeInKB));
/* if (Utilities.isUnix())
/* if (Utilities.isUnix())
fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/
File result = fileChooser.showOpenDialog(getScene().getWindow());
if (result != null) {
@ -561,13 +578,51 @@ public class ChatView extends AnchorPane {
}
}
public void onAttachText(String textAttachment, String name) {
if (!allowAttachments)
return;
try {
byte[] filesAsBytes = textAttachment.getBytes("UTF8");
int size = filesAsBytes.length;
int maxMsgSize = Connection.getPermittedMessageSize();
int maxSizeInKB = maxMsgSize / 1024;
if (size > maxMsgSize) {
new Popup().warning(Res.get("support.attachmentTooLarge", (size / 1024), maxSizeInKB)).show();
} else {
tempAttachments.add(new Attachment(name, filesAsBytes));
inputTextArea.setText(inputTextArea.getText() + "\n[" + Res.get("support.attachment") + " " + name + "]");
}
} catch (Exception e) {
log.error(e.toString());
e.printStackTrace();
}
}
private void copyChatMessagesToClipboard(Button sourceBtn) {
optionalSupportSession.ifPresent(session -> {
StringBuilder stringBuilder = new StringBuilder();
chatMessages.forEach(i -> {
String metaData = DisplayUtils.formatDateTime(new Date(i.getDate()));
metaData = metaData + (i.isSystemMessage() ? " (System message)" :
(i.isSenderIsTrader() ? " (from Trader)" : " (from Agent)"));
stringBuilder.append(metaData).append("\n").append(i.getMessage()).append("\n\n");
});
Utilities.copyToClipboard(stringBuilder.toString());
new Notification()
.notification(Res.get("shared.copiedToClipboard"))
.hideCloseButton()
.autoClose()
.show();
});
}
private void onOpenAttachment(Attachment attachment) {
if (!allowAttachments)
return;
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(Res.get("support.save"));
fileChooser.setInitialFileName(attachment.getFileName());
/* if (Utilities.isUnix())
/* if (Utilities.isUnix())
fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/
File file = fileChooser.showSaveDialog(getScene().getWindow());
if (file != null) {
@ -582,7 +637,7 @@ public class ChatView extends AnchorPane {
private void onSendMessage(String inputText) {
if (chatMessage != null) {
chatMessage.arrivedProperty().removeListener(arrivedPropertyListener);
chatMessage.acknowledgedProperty().removeListener(acknowledgedPropertyListener);
chatMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener);
chatMessage.sendMessageErrorProperty().removeListener(sendMessageErrorPropertyListener);
}
@ -594,6 +649,8 @@ public class ChatView extends AnchorPane {
inputTextArea.setDisable(true);
inputTextArea.clear();
chatMessage.startAckTimer();
Timer timer = UserThread.runAfter(() -> {
sendMsgInfoLabel.setVisible(true);
sendMsgInfoLabel.setManaged(true);
@ -602,8 +659,9 @@ public class ChatView extends AnchorPane {
sendMsgBusyAnimation.play();
}, 500, TimeUnit.MILLISECONDS);
arrivedPropertyListener = (observable, oldValue, newValue) -> {
acknowledgedPropertyListener = (observable, oldValue, newValue) -> {
if (newValue) {
sendMsgInfoLabel.setVisible(false);
hideSendMsgInfo(timer);
}
};
@ -624,7 +682,7 @@ public class ChatView extends AnchorPane {
}
};
if (chatMessage != null) {
chatMessage.arrivedProperty().addListener(arrivedPropertyListener);
chatMessage.acknowledgedProperty().addListener(acknowledgedPropertyListener);
chatMessage.storedInMailboxProperty().addListener(storedInMailboxPropertyListener);
chatMessage.sendMessageErrorProperty().addListener(sendMessageErrorPropertyListener);
}
@ -697,15 +755,12 @@ public class ChatView extends AnchorPane {
}
private void removeListenersOnSessionChange() {
if (chatMessages != null) {
if (disputeDirectMessageListListener != null) chatMessages.removeListener(disputeDirectMessageListListener);
chatMessages.forEach(ChatMessage::removeChangeListener);
}
if (chatMessages != null && disputeDirectMessageListListener != null)
chatMessages.removeListener(disputeDirectMessageListListener);
if (chatMessage != null) {
chatMessage.removeChangeListener();
if (arrivedPropertyListener != null)
chatMessage.arrivedProperty().removeListener(arrivedPropertyListener);
if (acknowledgedPropertyListener != null)
chatMessage.arrivedProperty().removeListener(acknowledgedPropertyListener);
if (storedInMailboxPropertyListener != null)
chatMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener);
}

View File

@ -1,5 +1,5 @@
/*
* This file is part of Bisq.
* This file is part of haveno.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
@ -12,56 +12,68 @@
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
* along with haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.main.support.dispute;
import haveno.common.UserThread;
import haveno.desktop.components.AutoTooltipButton;
import haveno.desktop.main.MainView;
import haveno.desktop.main.shared.ChatView;
import haveno.desktop.util.CssTheme;
import haveno.desktop.util.DisplayUtils;
import haveno.core.locale.Res;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeList;
import haveno.core.support.dispute.DisputeManager;
import haveno.core.support.dispute.DisputeSession;
import haveno.core.support.messages.ChatMessage;
import haveno.core.user.Preferences;
import haveno.core.util.coin.CoinFormatter;
import haveno.desktop.components.AutoTooltipButton;
import haveno.desktop.main.MainView;
import haveno.desktop.main.shared.ChatView;
import haveno.desktop.util.CssTheme;
import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import haveno.common.UserThread;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.beans.value.ChangeListener;
import java.util.Date;
import java.util.List;
import lombok.Getter;
public class DisputeChatPopup {
public interface ChatCallback {
void onCloseDisputeFromChatWindow(Dispute dispute);
void onSendLogsFromChatWindow(Dispute dispute);
}
private Stage chatPopupStage;
protected final DisputeManager<? extends DisputeList<Dispute>> disputeManager;
protected final CoinFormatter formatter;
protected final Preferences preferences;
private ChatCallback chatCallback;
private final ChatCallback chatCallback;
private double chatPopupStageXPosition = -1;
private double chatPopupStageYPosition = -1;
private ChangeListener<Number> xPositionListener;
private ChangeListener<Number> yPositionListener;
@Getter private Dispute selectedDispute;
DisputeChatPopup(DisputeManager<? extends DisputeList<Dispute>> disputeManager,
CoinFormatter formatter,
Preferences preferences,
ChatCallback chatCallback) {
CoinFormatter formatter,
Preferences preferences,
ChatCallback chatCallback) {
this.disputeManager = disputeManager;
this.formatter = formatter;
this.preferences = preferences;
@ -84,7 +96,7 @@ public class DisputeChatPopup {
selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true));
disputeManager.requestPersistence();
ChatView chatView = new ChatView(disputeManager, formatter, counterpartyName);
ChatView chatView = new ChatView(disputeManager, counterpartyName);
chatView.setAllowAttachments(true);
chatView.setDisplayHeader(false);
chatView.initialize();
@ -96,12 +108,27 @@ public class DisputeChatPopup {
AnchorPane.setTopAnchor(chatView, -20d);
AnchorPane.setBottomAnchor(chatView, 10d);
pane.getStyleClass().add("dispute-chat-border");
Button closeDisputeButton = null;
if (!selectedDispute.isClosed() && !disputeManager.isTrader(selectedDispute)) {
closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket"));
closeDisputeButton.setOnAction(e -> chatCallback.onCloseDisputeFromChatWindow(selectedDispute));
if (selectedDispute.isClosed()) {
chatView.display(concreteDisputeSession, null, pane.widthProperty());
} else {
if (disputeManager.isAgent(selectedDispute)) {
Button closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket"));
closeDisputeButton.setDefaultButton(true);
closeDisputeButton.setOnAction(e -> chatCallback.onCloseDisputeFromChatWindow(selectedDispute));
chatView.display(concreteDisputeSession, closeDisputeButton, pane.widthProperty());
} else {
MenuButton menuButton = new MenuButton(Res.get("support.moreButton"));
MenuItem menuItem1 = new MenuItem(Res.get("support.uploadTraderChat"));
MenuItem menuItem2 = new MenuItem(Res.get("support.sendLogFiles"));
menuItem1.setOnAction(e -> doTextAttachment(chatView));
setChatUploadEnabledState(menuItem1);
menuItem2.setOnAction(e -> chatCallback.onSendLogsFromChatWindow(selectedDispute));
menuButton.getItems().addAll(menuItem1, menuItem2);
menuButton.getStyleClass().add("jfx-button");
menuButton.setStyle("-fx-padding: 0 10 0 10;");
chatView.display(concreteDisputeSession, menuButton, pane.widthProperty());
}
}
chatView.display(concreteDisputeSession, closeDisputeButton, pane.widthProperty());
chatView.activate();
chatView.scrollToBottom();
chatPopupStage = new Stage();
@ -132,9 +159,9 @@ public class DisputeChatPopup {
chatPopupStage.setOpacity(0);
chatPopupStage.show();
xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue;
ChangeListener<Number> xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue;
chatPopupStage.xProperty().addListener(xPositionListener);
yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue;
ChangeListener<Number> yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue;
chatPopupStage.yProperty().addListener(yPositionListener);
if (chatPopupStageXPosition == -1) {
@ -149,8 +176,33 @@ public class DisputeChatPopup {
// Delay display to next render frame to avoid that the popup is first quickly displayed in default position
// and after a short moment in the correct position
UserThread.execute(() -> {
if (chatPopupStage != null) chatPopupStage.setOpacity(1);
UserThread.execute(() -> chatPopupStage.setOpacity(1));
}
private void doTextAttachment(ChatView chatView) {
disputeManager.findTrade(selectedDispute).ifPresent(t -> {
List<ChatMessage> chatMessages = t.getChatMessages();
if (chatMessages.size() > 0) {
StringBuilder stringBuilder = new StringBuilder();
chatMessages.forEach(i -> {
boolean isMyMsg = i.isSenderIsTrader();
String metaData = DisplayUtils.formatDateTime(new Date(i.getDate()));
if (!i.isSystemMessage())
metaData = (isMyMsg ? "Sent " : "Received ") + metaData
+ (isMyMsg ? "" : " from Trader");
stringBuilder.append(metaData).append("\n").append(i.getMessage()).append("\n\n");
});
String fileName = selectedDispute.getShortTradeId() + "_" + selectedDispute.getRoleStringForLogFile() + "_TraderChat.txt";
chatView.onAttachText(stringBuilder.toString(), fileName);
}
});
}
private void setChatUploadEnabledState(MenuItem menuItem) {
disputeManager.findTrade(selectedDispute).ifPresentOrElse(t -> {
menuItem.setDisable(t.getChatMessages().size() == 0);
}, () -> {
menuItem.setDisable(true);
});
}
}

View File

@ -51,6 +51,7 @@ import haveno.core.support.dispute.DisputeManager;
import haveno.core.support.dispute.DisputeResult;
import haveno.core.support.dispute.DisputeSession;
import haveno.core.support.dispute.agent.DisputeAgentLookupMap;
import haveno.core.support.dispute.arbitration.ArbitrationManager;
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import haveno.core.support.dispute.mediation.MediationManager;
import haveno.core.support.messages.ChatMessage;
@ -67,9 +68,12 @@ import haveno.desktop.components.AutoTooltipLabel;
import haveno.desktop.components.AutoTooltipTableColumn;
import haveno.desktop.components.HyperlinkWithIcon;
import haveno.desktop.components.InputTextField;
import haveno.desktop.components.PeerInfoIconDispute;
import haveno.desktop.components.PeerInfoIconMap;
import haveno.desktop.main.overlays.popups.Popup;
import haveno.desktop.main.overlays.windows.ContractWindow;
import haveno.desktop.main.overlays.windows.DisputeSummaryWindow;
import haveno.desktop.main.overlays.windows.SendLogFilesWindow;
import haveno.desktop.main.overlays.windows.SendPrivateNotificationWindow;
import haveno.desktop.main.overlays.windows.TradeDetailsWindow;
import haveno.desktop.main.overlays.windows.VerifyDisputeResultSignatureWindow;
@ -119,7 +123,7 @@ import java.util.concurrent.atomic.AtomicReference;
import static haveno.desktop.util.FormBuilder.getIconForLabel;
import static haveno.desktop.util.FormBuilder.getRegularIconButton;
public abstract class DisputeView extends ActivatableView<VBox, Void> {
public abstract class DisputeView extends ActivatableView<VBox, Void> implements DisputeChatPopup.ChatCallback {
public enum FilterResult {
NO_MATCH("No Match"),
NO_FILTER("No filter text"),
@ -181,6 +185,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
private Map<String, Button> chatButtonByDispute = new HashMap<>();
private Map<String, JFXBadge> chatBadgeByDispute = new HashMap<>();
private Map<String, JFXBadge> newBadgeByDispute = new HashMap<>();
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
private final PeerInfoIconMap avatarMap = new PeerInfoIconMap();
protected DisputeChatPopup chatPopup;
@ -212,8 +218,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
this.accountAgeWitnessService = accountAgeWitnessService;
this.arbitratorManager = arbitratorManager;
this.useDevPrivilegeKeys = useDevPrivilegeKeys;
DisputeChatPopup.ChatCallback chatCallback = this::handleOnProcessDispute;
chatPopup = new DisputeChatPopup(disputeManager, formatter, preferences, chatCallback);
chatPopup = new DisputeChatPopup(disputeManager, formatter, preferences, this);
}
@Override
@ -223,6 +228,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
HBox.setHgrow(label, Priority.NEVER);
filterTextField = new InputTextField();
filterTextField.setPromptText(Res.get("support.filter.prompt"));
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(100));
tooltip.setShowDuration(Duration.seconds(10));
@ -382,7 +388,9 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
ObservableList<ChatMessage> chatMessages = dispute.getChatMessages();
// If last message is not a result message we re-open as we might have received a new message from the
// trader/mediator/arbitrator who has reopened the case
if (!chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) {
if (!chatMessages.isEmpty() &&
!chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute) &&
dispute.unreadMessageCount(senderFlag()) > 0) {
onSelectDispute(dispute);
reOpenDispute();
}
@ -428,7 +436,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
// For open filter we do not want to continue further as json data would cause a match
if (filter.equalsIgnoreCase("open")) {
return !dispute.isClosed() ? FilterResult.OPEN_DISPUTES : FilterResult.NO_MATCH;
return !dispute.isClosed() || dispute.unreadMessageCount(senderFlag()) > 0 ?
FilterResult.OPEN_DISPUTES : FilterResult.NO_MATCH;
}
if (dispute.getTradeId().toLowerCase().contains(filter)) {
@ -1083,7 +1092,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
private TableColumn<Dispute, Dispute> getDateColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.date")) {
{
setMinWidth(180);
setMinWidth(100);
setPrefWidth(150);
}
};
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
@ -1109,7 +1119,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
private TableColumn<Dispute, Dispute> getTradeIdColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.tradeId")) {
{
setMinWidth(110);
setMinWidth(50);
setPrefWidth(100);
}
};
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
@ -1167,10 +1178,14 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
@Override
public void updateItem(final Dispute item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty)
if (item != null && !empty) {
setText(getBuyerOnionAddressColumnLabel(item));
else
PeerInfoIconDispute peerInfoIconDispute = createAvatar(tableRowProperty().get().getIndex(), item, true);
setGraphic(peerInfoIconDispute);
} else {
setText("");
setText(null);
}
}
};
}
@ -1193,10 +1208,14 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
@Override
public void updateItem(final Dispute item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty)
if (item != null && !empty) {
setText(getSellerOnionAddressColumnLabel(item));
else
PeerInfoIconDispute peerInfoIconDispute = createAvatar(tableRowProperty().get().getIndex(), item, false);
setGraphic(peerInfoIconDispute);
} else {
setText("");
setGraphic(null);
}
}
};
}
@ -1314,8 +1333,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
return;
}
String keyBaseUserName = DisputeAgentLookupMap.getMatrixUserName(agentNodeAddress.getFullAddress());
setText(keyBaseUserName);
String MatrixUserName = DisputeAgentLookupMap.getMatrixUserName(agentNodeAddress.getFullAddress());
setText(MatrixUserName);
} else {
setText("");
}
@ -1448,4 +1467,36 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
return (disputeManager instanceof MediationManager) ? Res.get("shared.mediator") : Res.get("shared.refundAgent");
}
}
private PeerInfoIconDispute createAvatar(Integer tableRowId, Dispute dispute, boolean isBuyer) {
NodeAddress nodeAddress = isBuyer ? dispute.getContract().getBuyerNodeAddress() : dispute.getContract().getSellerNodeAddress();
String key = tableRowId + nodeAddress.getHostNameWithoutPostFix() + (isBuyer ? "BUYER" : "SELLER");
Long accountAge = isBuyer ?
accountAgeWitnessService.getAccountAge(dispute.getBuyerPaymentAccountPayload(), dispute.getContract().getBuyerPubKeyRing()) :
accountAgeWitnessService.getAccountAge(dispute.getSellerPaymentAccountPayload(), dispute.getContract().getSellerPubKeyRing());
PeerInfoIconDispute peerInfoIcon = new PeerInfoIconDispute(
nodeAddress,
disputeManager.getNrOfDisputes(isBuyer, dispute.getContract()),
accountAge,
preferences);
avatarMap.put(key, peerInfoIcon); // TODO
return peerInfoIcon;
}
@Override
public void onCloseDisputeFromChatWindow(Dispute dispute) {
if (dispute.getDisputeState() == Dispute.State.NEW || dispute.getDisputeState() == Dispute.State.OPEN) {
handleOnProcessDispute(dispute);
} else {
closeDisputeFromButton();
}
}
@Override
public void onSendLogsFromChatWindow(Dispute dispute) {
if (!(disputeManager instanceof ArbitrationManager))
return;
ArbitrationManager arbitrationManager = (ArbitrationManager) disputeManager;
new SendLogFilesWindow(dispute.getTradeId(), dispute.getTraderId(), arbitrationManager).show();
}
}

View File

@ -24,5 +24,6 @@ public enum AckMessageSourceType {
ARBITRATION_MESSAGE,
MEDIATION_MESSAGE,
TRADE_CHAT_MESSAGE,
REFUND_MESSAGE
REFUND_MESSAGE,
LOG_TRANSFER
}

View File

@ -0,0 +1,106 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.network.p2p;
import haveno.common.app.Version;
import haveno.common.proto.network.NetworkEnvelope;
import com.google.protobuf.ByteString;
import lombok.EqualsAndHashCode;
import lombok.Value;
@EqualsAndHashCode(callSuper = true)
@Value
public class FileTransferPart extends NetworkEnvelope implements ExtendedDataSizePermission, SendersNodeAddressMessage {
NodeAddress senderNodeAddress;
public String uid;
public String tradeId;
public int traderId;
public long seqNumOrFileLength;
public ByteString messageData; // if message_data is empty it is the first message, requesting file upload permission
public FileTransferPart(NodeAddress senderNodeAddress,
String tradeId,
int traderId,
String uid,
long seqNumOrFileLength,
ByteString messageData) {
this(senderNodeAddress, tradeId, traderId, uid, seqNumOrFileLength, messageData, Version.getP2PMessageVersion());
}
public boolean isInitialRequest() {
return messageData.size() == 0;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private FileTransferPart(NodeAddress senderNodeAddress,
String tradeId,
int traderId,
String uid,
long seqNumOrFileLength,
ByteString messageData,
String messageVersion) {
super(messageVersion);
this.senderNodeAddress = senderNodeAddress;
this.tradeId = tradeId;
this.traderId = traderId;
this.uid = uid;
this.seqNumOrFileLength = seqNumOrFileLength;
this.messageData = messageData;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setFileTransferPart(protobuf.FileTransferPart.newBuilder()
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setTradeId(tradeId)
.setTraderId(traderId)
.setUid(uid)
.setSeqNumOrFileLength(seqNumOrFileLength)
.setMessageData(messageData)
.build())
.build();
}
public static FileTransferPart fromProto(protobuf.FileTransferPart proto, String messageVersion) {
return new FileTransferPart(
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getTradeId(),
proto.getTraderId(),
proto.getUid(),
proto.getSeqNumOrFileLength(),
proto.getMessageData(),
messageVersion);
}
@Override
public String toString() {
return "FileTransferPart{" +
"\n senderNodeAddress='" + senderNodeAddress.getHostNameForDisplay() + '\'' +
",\n uid='" + uid + '\'' +
",\n tradeId='" + tradeId + '\'' +
",\n traderId='" + traderId + '\'' +
",\n seqNumOrFileLength=" + seqNumOrFileLength +
"\n} " + super.toString();
}
}

View File

@ -64,6 +64,8 @@ message NetworkEnvelope {
MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 37;
MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 38;
FileTransferPart file_transfer_part = 39;
}
}
@ -100,6 +102,15 @@ message GetUpdatedDataRequest {
string version = 4;
}
message FileTransferPart {
NodeAddress sender_node_address = 1;
string uid = 2;
string trade_id = 3;
int32 trader_id = 4;
int64 seq_num_or_file_length = 5;
bytes message_data = 6;
}
message GetPeersRequest {
NodeAddress sender_node_address = 1;
int32 nonce = 2;