Add alert icon to list entries

Support agent can mark a suspicious dispute as resolved so it does not
show the alert icon anymore. In the full report a [ACK] got added to
that dispute.
This commit is contained in:
chimp1984 2020-09-05 17:39:22 -05:00
parent 5a65b150fe
commit 4a4bd7cd12
No known key found for this signature in database
GPG key ID: 9801B4EC591F90E3
5 changed files with 440 additions and 254 deletions

View file

@ -1,214 +0,0 @@
/*
* 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 bisq.core.support.dispute.agent;
import bisq.core.locale.Res;
import bisq.core.payment.payload.PayloadWithHolderName;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeList;
import bisq.core.support.dispute.DisputeManager;
import bisq.core.trade.Contract;
import bisq.common.crypto.Hash;
import bisq.common.crypto.PubKeyRing;
import bisq.common.util.Utilities;
import javafx.collections.ListChangeListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FraudDetection {
public interface Listener {
void onSuspiciousDisputeDetected();
}
private final DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager;
private Map<String, List<RealNameAccountInfo>> buyerRealNameAccountByAddressMap = new HashMap<>();
private Map<String, List<RealNameAccountInfo>> sellerRealNameAccountByAddressMap = new HashMap<>();
@Getter
private Map<String, List<RealNameAccountInfo>> accountsUsingMultipleNames = new HashMap<>();
private List<Listener> listeners = new CopyOnWriteArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public FraudDetection(DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager) {
this.disputeManager = disputeManager;
disputeManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) c -> {
c.next();
if (c.wasAdded()) {
checkForMultipleHolderNames();
}
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void checkForMultipleHolderNames() {
log.error("checkForMultipleHolderNames");
buildRealNameAccountMaps();
detectUsageOfDifferentUserNames();
log.error("hasSuspiciousDisputeDetected() " + hasSuspiciousDisputeDetected());
}
public boolean hasSuspiciousDisputeDetected() {
return !accountsUsingMultipleNames.isEmpty();
}
public String getAccountsUsingMultipleNamesAsString() {
return accountsUsingMultipleNames.entrySet().stream()
.map(entry -> {
String accountInfo = entry.getValue().stream()
.map(info -> {
String tradeId = info.getDispute().getShortTradeId();
String holderName = info.getPayloadWithHolderName().getHolderName();
return " Account owner name: '" + holderName +
"'; Trade ID: '" + tradeId +
"'; Address: '" + info.getAddress() +
"'; Payment method: '" + Res.get(info.getPaymentAccountPayload().getPaymentMethodId()) +
"'; Role: " + (info.isBuyer() ? "'Buyer'" : "'Seller'");
})
.collect(Collectors.joining("\n"));
return "Trader with multiple identities:\n" +
accountInfo;
})
.collect(Collectors.joining("\n\n"));
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void buildRealNameAccountMaps() {
buyerRealNameAccountByAddressMap.clear();
sellerRealNameAccountByAddressMap.clear();
disputeManager.getDisputesAsObservableList()
.forEach(dispute -> {
Contract contract = dispute.getContract();
PubKeyRing traderPubKeyRing = dispute.getTraderPubKeyRing();
String traderPubKeyHash = getTraderPuKeyHash(traderPubKeyRing);
String buyerPubKeyHash = getTraderPuKeyHash(contract.getBuyerPubKeyRing());
boolean isBuyer = contract.isMyRoleBuyer(traderPubKeyRing);
if (buyerPubKeyHash.equals(traderPubKeyHash)) {
PaymentAccountPayload buyerPaymentAccountPayload = contract.getBuyerPaymentAccountPayload();
String buyersAddress = contract.getBuyerNodeAddress().getFullAddress();
addToMap(traderPubKeyHash, buyerRealNameAccountByAddressMap, buyerPaymentAccountPayload, buyersAddress, dispute, isBuyer);
} else {
PaymentAccountPayload sellerPaymentAccountPayload = contract.getSellerPaymentAccountPayload();
String sellerAddress = contract.getSellerNodeAddress().getFullAddress();
addToMap(traderPubKeyHash, sellerRealNameAccountByAddressMap, sellerPaymentAccountPayload, sellerAddress, dispute, isBuyer);
}
});
}
private String getTraderPuKeyHash(PubKeyRing pubKeyRing) {
return Utilities.encodeToHex(Hash.getRipemd160hash(pubKeyRing.toProtoMessage().toByteArray()));
}
private void addToMap(String pubKeyHash, Map<String, List<RealNameAccountInfo>> map,
PaymentAccountPayload paymentAccountPayload,
String address,
Dispute dispute,
boolean isBuyer) {
if (paymentAccountPayload instanceof PayloadWithHolderName) {
map.putIfAbsent(pubKeyHash, new ArrayList<>());
RealNameAccountInfo info = new RealNameAccountInfo(address,
(PayloadWithHolderName) paymentAccountPayload,
paymentAccountPayload,
dispute,
isBuyer);
map.get(pubKeyHash).add(info);
}
}
private void detectUsageOfDifferentUserNames() {
detectUsageOfDifferentUserNames(buyerRealNameAccountByAddressMap);
detectUsageOfDifferentUserNames(sellerRealNameAccountByAddressMap);
}
private void detectUsageOfDifferentUserNames(Map<String, List<RealNameAccountInfo>> map) {
String previous = accountsUsingMultipleNames.toString();
map.forEach((key, value) -> {
Set<String> userNames = value.stream()
.map(info -> info.getPayloadWithHolderName().getHolderName())
.collect(Collectors.toSet());
if (userNames.size() > 1) {
accountsUsingMultipleNames.put(key, value);
}
});
String updated = accountsUsingMultipleNames.toString();
if (!previous.equals(updated)) {
listeners.forEach(Listener::onSuspiciousDisputeDetected);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Static class
///////////////////////////////////////////////////////////////////////////////////////////
@Value
private static class RealNameAccountInfo {
private final String address;
private final PayloadWithHolderName payloadWithHolderName;
private final Dispute dispute;
private final boolean isBuyer;
private final PaymentAccountPayload paymentAccountPayload;
RealNameAccountInfo(String address,
PayloadWithHolderName payloadWithHolderName,
PaymentAccountPayload paymentAccountPayload,
Dispute dispute,
boolean isBuyer) {
this.address = address;
this.payloadWithHolderName = payloadWithHolderName;
this.paymentAccountPayload = paymentAccountPayload;
this.dispute = dispute;
this.isBuyer = isBuyer;
}
}
}

View file

@ -0,0 +1,270 @@
/*
* 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 bisq.core.support.dispute.agent;
import bisq.core.locale.Res;
import bisq.core.payment.payload.PayloadWithHolderName;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeList;
import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeResult;
import bisq.core.trade.Contract;
import bisq.core.user.DontShowAgainLookup;
import bisq.common.crypto.Hash;
import bisq.common.crypto.PubKeyRing;
import bisq.common.util.Tuple2;
import bisq.common.util.Utilities;
import javafx.collections.ListChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* Detects traders who had disputes where they used different account holder names. Only payment methods where a
* real name is required are used for the check.
* Strings are not translated here as it is only visible to dispute agents
*/
@Slf4j
public class MultipleHolderNameDetection {
///////////////////////////////////////////////////////////////////////////////////////////
// Listener
///////////////////////////////////////////////////////////////////////////////////////////
public interface Listener {
void onSuspiciousDisputeDetected();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Static
///////////////////////////////////////////////////////////////////////////////////////////
private static final String ACK_KEY = "Ack-";
private static String getSigPuKeyHashAsHex(PubKeyRing pubKeyRing) {
return Utilities.encodeToHex(Hash.getRipemd160hash(pubKeyRing.getSignaturePubKeyBytes()));
}
private static String getSigPubKeyHashAsHex(Dispute dispute) {
return getSigPuKeyHashAsHex(dispute.getTraderPubKeyRing());
}
private static boolean isBuyer(Dispute dispute) {
String traderSigPubKeyHashAsHex = getSigPubKeyHashAsHex(dispute);
String buyerSigPubKeyHashAsHex = getSigPuKeyHashAsHex(dispute.getContract().getBuyerPubKeyRing());
return buyerSigPubKeyHashAsHex.equals(traderSigPubKeyHashAsHex);
}
private static PayloadWithHolderName getPayloadWithHolderName(Dispute dispute) {
return (PayloadWithHolderName) getPaymentAccountPayload(dispute);
}
public static PaymentAccountPayload getPaymentAccountPayload(Dispute dispute) {
return isBuyer(dispute) ?
dispute.getContract().getBuyerPaymentAccountPayload() :
dispute.getContract().getSellerPaymentAccountPayload();
}
public static String getAddress(Dispute dispute) {
return isBuyer(dispute) ?
dispute.getContract().getBuyerNodeAddress().getHostName() :
dispute.getContract().getSellerNodeAddress().getHostName();
}
public static String getAckKey(Dispute dispute) {
return ACK_KEY + getSigPubKeyHashAsHex(dispute).substring(0, 4) + "/" + dispute.getShortTradeId();
}
private static String getIsBuyerSubString(boolean isBuyer) {
return "'\n Role: " + (isBuyer ? "'Buyer'" : "'Seller'");
}
///////////////////////////////////////////////////////////////////////////////////////////
// Class fields
///////////////////////////////////////////////////////////////////////////////////////////
private final DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager;
// Key is hex of hash of sig pubKey which we consider a trader identity. We could use onion address as well but
// once we support multiple onion addresses that would not work anymore.
@Getter
private Map<String, List<Dispute>> suspiciousDisputesByTraderMap = new HashMap<>();
private List<Listener> listeners = new CopyOnWriteArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public MultipleHolderNameDetection(DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager) {
this.disputeManager = disputeManager;
disputeManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) c -> {
c.next();
if (c.wasAdded()) {
detectMultipleHolderNames();
}
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void detectMultipleHolderNames() {
String previous = suspiciousDisputesByTraderMap.toString();
getAllDisputesByTraderMap().forEach((key, value) -> {
Set<String> userNames = value.stream()
.map(dispute -> getPayloadWithHolderName(dispute).getHolderName())
.collect(Collectors.toSet());
if (userNames.size() > 1) {
// As we compare previous results we need to make sorting deterministic
value.sort(Comparator.comparing(Dispute::getId));
suspiciousDisputesByTraderMap.put(key, value);
}
});
String updated = suspiciousDisputesByTraderMap.toString();
if (!previous.equals(updated)) {
listeners.forEach(Listener::onSuspiciousDisputeDetected);
}
}
public boolean hasSuspiciousDisputesDetected() {
return !suspiciousDisputesByTraderMap.isEmpty();
}
// Returns all disputes of a trader who used multiple names
public List<Dispute> getDisputesForTrader(Dispute dispute) {
String traderPubKeyHash = getSigPubKeyHashAsHex(dispute);
if (suspiciousDisputesByTraderMap.containsKey(traderPubKeyHash)) {
return suspiciousDisputesByTraderMap.get(traderPubKeyHash);
}
return new ArrayList<>();
}
// Get a report of traders who used multiple names with all their disputes listed
public String getReportForAllDisputes() {
return getReport(suspiciousDisputesByTraderMap.values());
}
// Get a report for a trader who used multiple names with all their disputes listed
public String getReportForDisputeOfTrader(List<Dispute> disputes) {
Collection<List<Dispute>> values = new ArrayList<>();
values.add(disputes);
return getReport(values);
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private Map<String, List<Dispute>> getAllDisputesByTraderMap() {
Map<String, List<Dispute>> allDisputesByTraderMap = new HashMap<>();
disputeManager.getDisputesAsObservableList()
.forEach(dispute -> {
Contract contract = dispute.getContract();
PaymentAccountPayload paymentAccountPayload = isBuyer(dispute) ?
contract.getBuyerPaymentAccountPayload() :
contract.getSellerPaymentAccountPayload();
if (paymentAccountPayload instanceof PayloadWithHolderName) {
String traderPubKeyHash = getSigPubKeyHashAsHex(dispute);
allDisputesByTraderMap.putIfAbsent(traderPubKeyHash, new ArrayList<>());
List<Dispute> disputes = allDisputesByTraderMap.get(traderPubKeyHash);
disputes.add(dispute);
}
});
return allDisputesByTraderMap;
}
// Get a text report for a trader who used multiple names and list all the his disputes
private String getReport(Collection<List<Dispute>> collectionOfDisputesOfTrader) {
return collectionOfDisputesOfTrader.stream()
.map(disputes -> {
Set<String> addresses = new HashSet<>();
Set<Boolean> isBuyerHashSet = new HashSet<>();
Set<String> names = new HashSet<>();
String disputesReport = disputes.stream()
.map(dispute -> {
addresses.add(getAddress(dispute));
String ackKey = getAckKey(dispute);
String ackSubString = " ";
if (!DontShowAgainLookup.showAgain(ackKey)) {
ackSubString = "[ACK] ";
}
String holderName = getPayloadWithHolderName(dispute).getHolderName();
names.add(holderName);
boolean isBuyer = isBuyer(dispute);
isBuyerHashSet.add(isBuyer);
String isBuyerSubString = getIsBuyerSubString(isBuyer);
DisputeResult disputeResult = dispute.disputeResultProperty().get();
String summaryNotes = disputeResult != null ? disputeResult.getSummaryNotesProperty().get().trim() : "Not closed yet";
return ackSubString +
"Trade ID: '" + dispute.getShortTradeId() +
"'\n Account holder name: '" + holderName +
"'\n Payment method: '" + Res.get(getPaymentAccountPayload(dispute).getPaymentMethodId()) +
isBuyerSubString +
"'\n Summary: '" + summaryNotes;
})
.collect(Collectors.joining("\n"));
String addressSubString = addresses.size() > 1 ?
"used multiple addresses " + addresses + " with" :
"with address " + new ArrayList<>(addresses).get(0) + " used";
String roleSubString = "Trader ";
if (isBuyerHashSet.size() == 1) {
boolean isBuyer = new ArrayList<>(isBuyerHashSet).get(0);
String isBuyerSubString = getIsBuyerSubString(isBuyer);
disputesReport = disputesReport.replace(isBuyerSubString, "");
roleSubString = isBuyer ? "Buyer " : "Seller ";
}
String traderReport = roleSubString + addressSubString + " multiple names: " + names.toString() + "\n" + disputesReport;
return new Tuple2<>(roleSubString, traderReport);
})
.sorted(Comparator.comparing(o -> o.first)) // Buyers first, then seller, then mixed (trader was in seller and buyer role)
.map(e -> e.second)
.collect(Collectors.joining("\n\n"));
}
}

View file

@ -51,7 +51,7 @@ 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. Aribtrator is
* 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

View file

@ -127,7 +127,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
private final AccountAgeWitnessService accountAgeWitnessService;
private final boolean useDevPrivilegeKeys;
private TableView<Dispute> tableView;
protected TableView<Dispute> tableView;
private SortedList<Dispute> sortedList;
@Getter
@ -142,12 +142,12 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
protected FilteredList<Dispute> filteredList;
protected InputTextField filterTextField;
private ChangeListener<String> filterTextFieldListener;
private HBox filterBox;
protected AutoTooltipButton reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton;
private Map<String, ListChangeListener<ChatMessage>> disputeChatMessagesListeners = new HashMap<>();
@Nullable
private ListChangeListener<Dispute> disputesListener; // Only set in mediation cases
protected Label alertIconLabel;
protected TableColumn<Dispute, Dispute> stateColumn;
///////////////////////////////////////////////////////////////////////////////////////////
@ -231,7 +231,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
Pane spacer = new Pane();
HBox.setHgrow(spacer, Priority.ALWAYS);
filterBox = new HBox();
HBox filterBox = new HBox();
filterBox.setSpacing(5);
filterBox.getChildren().addAll(label,
filterTextField,
@ -419,7 +419,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
}
}
protected void handleKeyPressed(KeyEvent event) {
private void handleKeyPressed(KeyEvent event) {
}
protected void reOpenDispute() {
@ -733,7 +733,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
// Table
///////////////////////////////////////////////////////////////////////////////////////////
private void setupTable() {
protected void setupTable() {
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
Label placeholder = new AutoTooltipLabel(Res.get("support.noTickets"));
placeholder.setWrapText(true);
@ -764,7 +764,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
TableColumn<Dispute, Dispute> roleColumn = getRoleColumn();
tableView.getColumns().add(roleColumn);
TableColumn<Dispute, Dispute> stateColumn = getStateColumn();
stateColumn = getStateColumn();
tableView.getColumns().add(stateColumn);
tradeIdColumn.setComparator(Comparator.comparing(Dispute::getTradeId));
@ -1120,7 +1120,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
});
return column;
}
}

View file

@ -18,6 +18,7 @@
package bisq.desktop.main.support.dispute.agent;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.ContractWindow;
import bisq.desktop.main.overlays.windows.DisputeSummaryWindow;
@ -31,19 +32,35 @@ import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeList;
import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeSession;
import bisq.core.support.dispute.agent.FraudDetection;
import bisq.core.support.dispute.agent.MultipleHolderNameDetection;
import bisq.core.trade.TradeManager;
import bisq.core.user.DontShowAgainLookup;
import bisq.core.util.coin.CoinFormatter;
import bisq.common.crypto.KeyRing;
import bisq.common.util.Utilities;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
public abstract class DisputeAgentView extends DisputeView implements FraudDetection.Listener {
import javafx.geometry.Insets;
private final FraudDetection fraudDetection;
import javafx.beans.property.ReadOnlyObjectWrapper;
import java.util.List;
import static bisq.desktop.util.FormBuilder.getIconForLabel;
public abstract class DisputeAgentView extends DisputeView implements MultipleHolderNameDetection.Listener {
private final MultipleHolderNameDetection multipleHolderNameDetection;
public DisputeAgentView(DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager,
KeyRing keyRing,
@ -66,9 +83,14 @@ public abstract class DisputeAgentView extends DisputeView implements FraudDetec
accountAgeWitnessService,
useDevPrivilegeKeys);
fraudDetection = new FraudDetection(disputeManager);
multipleHolderNameDetection = new MultipleHolderNameDetection(disputeManager);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Life cycle
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void initialize() {
super.initialize();
@ -84,16 +106,16 @@ public abstract class DisputeAgentView extends DisputeView implements FraudDetec
fullReportButton.setVisible(true);
fullReportButton.setManaged(true);
fraudDetection.checkForMultipleHolderNames();
multipleHolderNameDetection.detectMultipleHolderNames();
}
@Override
protected void activate() {
super.activate();
fraudDetection.addListener(this);
if (fraudDetection.hasSuspiciousDisputeDetected()) {
showAlertIcon();
multipleHolderNameDetection.addListener(this);
if (multipleHolderNameDetection.hasSuspiciousDisputesDetected()) {
suspiciousDisputeDetected();
}
}
@ -101,9 +123,24 @@ public abstract class DisputeAgentView extends DisputeView implements FraudDetec
protected void deactivate() {
super.deactivate();
fraudDetection.removeListener(this);
multipleHolderNameDetection.removeListener(this);
}
///////////////////////////////////////////////////////////////////////////////////////////
// MultipleHolderNamesDetection.Listener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onSuspiciousDisputeDetected() {
suspiciousDisputeDetected();
}
///////////////////////////////////////////////////////////////////////////////////////////
// DisputeView
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void applyFilteredListPredicate(String filterString) {
filteredList.setPredicate(dispute -> {
@ -130,34 +167,128 @@ public abstract class DisputeAgentView extends DisputeView implements FraudDetec
}
@Override
public void onSuspiciousDisputeDetected() {
showAlertIcon();
protected void setupTable() {
super.setupTable();
stateColumn.getStyleClass().remove("last-column");
tableView.getColumns().add(getAlertColumn());
}
private void showAlertIcon() {
String accountsUsingMultipleNamesList = fraudDetection.getAccountsUsingMultipleNamesAsString();
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void suspiciousDisputeDetected() {
alertIconLabel.setVisible(true);
alertIconLabel.setManaged(true);
alertIconLabel.setTooltip(new Tooltip("You have disputes where user used different " +
"real life names from the same application. Click for more details."));
alertIconLabel.setTooltip(new Tooltip("You have suspicious disputes where the same trader used different " +
"account holder names.\nClick for more information."));
// Text below is for arbitrators only so no need to translate it
alertIconLabel.setOnMouseClicked(e -> new Popup()
.width(1100)
.warning("You have dispute cases where traders used different account holder names.\n\n" +
"This might be not critical in case of small variations of the same name " +
"(e.g. first name and last name are swapped), " +
"but if the name is different you should request information from the trader why they " +
"used a different name and request proof that the person with the real name is aware " +
"of the trade. " +
"It can be that the trader uses the account of their wife/husband, but it also could " +
"be a case of a stolen bank account or money laundering.\n\n" +
"Please check below the list of the names which got detected. Search with the trade ID for " +
"the dispute case for evaluating if it might be a fraudulent account. If so, please notify the " +
"developers and provide the contract json data to them so they can ban those traders.\n\n" +
accountsUsingMultipleNamesList)
.actionButtonText(Res.get("shared.copyToClipboard"))
.onAction(() -> Utilities.copyToClipboard(accountsUsingMultipleNamesList))
.show());
alertIconLabel.setOnMouseClicked(e -> {
String reportForAllDisputes = multipleHolderNameDetection.getReportForAllDisputes();
new Popup()
.width(1100)
.warning(getReportMessage(reportForAllDisputes, "traders"))
.actionButtonText(Res.get("shared.copyToClipboard"))
.onAction(() -> Utilities.copyToClipboard(reportForAllDisputes))
.show();
});
}
private TableColumn<Dispute, Dispute> getAlertColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>("Alert") {
{
setMinWidth(50);
}
};
column.getStyleClass().add("last-column");
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
column.setCellFactory(
c -> new TableCell<>() {
Label alertIconLabel;
@Override
public void updateItem(Dispute dispute, boolean empty) {
if (dispute != null && !empty) {
if (!showAlertAtDispute(dispute)) {
setGraphic(null);
if (alertIconLabel != null) {
alertIconLabel.setOnMouseClicked(null);
}
return;
}
if (alertIconLabel != null) {
alertIconLabel.setOnMouseClicked(null);
}
alertIconLabel = new Label();
Text icon = getIconForLabel(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE, "1.5em", alertIconLabel);
icon.getStyleClass().add("alert-icon");
HBox.setMargin(alertIconLabel, new Insets(4, 0, 0, 10));
alertIconLabel.setMouseTransparent(false);
setGraphic(alertIconLabel);
alertIconLabel.setOnMouseClicked(e -> {
List<Dispute> realNameAccountInfoList = multipleHolderNameDetection.getDisputesForTrader(dispute);
String reportForDisputeOfTrader = multipleHolderNameDetection.getReportForDisputeOfTrader(realNameAccountInfoList);
String key = MultipleHolderNameDetection.getAckKey(dispute);
new Popup()
.width(1100)
.warning(getReportMessage(reportForDisputeOfTrader, "this trader"))
.actionButtonText(Res.get("shared.copyToClipboard"))
.onAction(() -> {
Utilities.copyToClipboard(reportForDisputeOfTrader);
if (!DontShowAgainLookup.showAgain(key)) {
setGraphic(null);
}
})
.dontShowAgainId(key)
.dontShowAgainText("Is not suspicious")
.onClose(() -> {
if (!DontShowAgainLookup.showAgain(key)) {
setGraphic(null);
}
})
.show();
});
} else {
setGraphic(null);
if (alertIconLabel != null) {
alertIconLabel.setOnMouseClicked(null);
}
}
}
});
column.setComparator((o1, o2) -> Boolean.compare(showAlertAtDispute(o1), showAlertAtDispute(o2)));
column.setSortable(true);
return column;
}
private boolean showAlertAtDispute(Dispute dispute) {
return DontShowAgainLookup.showAgain(MultipleHolderNameDetection.getAckKey(dispute)) &&
!multipleHolderNameDetection.getDisputesForTrader(dispute).isEmpty();
}
private String getReportMessage(String report, String subString) {
return "You have dispute cases where " + subString + " used different account holder names.\n\n" +
"This might be not critical in case of small variations of the same name " +
"(e.g. first name and last name are swapped), " +
"but if the name is completely different you should request information from the trader why they " +
"used a different name and request proof that the person with the real name is aware " +
"of the trade. " +
"It can be that the trader uses the account of their wife/husband, but it also could " +
"be a case of a stolen bank account or money laundering.\n\n" +
"Please check below the list of the names which have been detected. " +
"Search with the trade ID for the dispute case or check out the alert icon at each dispute in " +
"the list (you might need to remove the 'open' filter) and evaluate " +
"if it might be a fraudulent account (buyer role is more likely to be fraudulent). " +
"If you find suspicious disputes, please notify the developers and provide the contract json data " +
"to them so they can ban those traders.\n\n" +
Utilities.toTruncatedString(report, 700, false);
}
}