Skip to content

Commit

Permalink
Add moderationRequestService
Browse files Browse the repository at this point in the history
Implement rate limit for public messages
  • Loading branch information
HenrikJannsen committed Jan 26, 2025
1 parent fe4449b commit 6536f45
Show file tree
Hide file tree
Showing 19 changed files with 594 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
import bisq.desktop.components.controls.validator.SettableErrorValidator;
import bisq.desktop.components.overlay.Popup;
import bisq.i18n.Res;
import bisq.support.mediation.MediationRequestService;
import bisq.support.moderator.ModeratorService;
import bisq.support.moderator.ModerationRequestService;
import bisq.trade.bisq_easy.BisqEasyTrade;
import bisq.user.profile.UserProfile;
import javafx.beans.property.BooleanProperty;
Expand All @@ -55,17 +54,15 @@ public View getView() {

private static class Controller extends BaseState.Controller<Model, View> {
private final BisqEasyService bisqEasyService;
private final MediationRequestService mediationRequestService;
private final ModeratorService moderatorService;
private final ModerationRequestService moderationRequestService;

private Controller(ServiceProvider serviceProvider,
BisqEasyTrade bisqEasyTrade,
BisqEasyOpenTradeChannel channel) {
super(serviceProvider, bisqEasyTrade, channel);

bisqEasyService = serviceProvider.getBisqEasyService();
mediationRequestService = serviceProvider.getSupportService().getMediationRequestService();
moderatorService = serviceProvider.getSupportService().getModeratorService();
moderationRequestService = serviceProvider.getSupportService().getModerationRequestService();
}

@Override
Expand All @@ -92,7 +89,7 @@ public void onActivate() {

// Report to moderator
String message = "Account data of " + peerUserName + " is banned: " + sellersAccountData;
moderatorService.reportUserProfile(peer, message);
moderationRequestService.reportUserProfile(peer, message);

// We reject the trade to avoid the banned user can continue
bisqEasyTradeService.cancelTrade(bisqEasyTrade);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import bisq.desktop.overlay.OverlayController;
import bisq.desktop.overlay.OverlayModel;
import bisq.i18n.Res;
import bisq.support.moderator.ModeratorService;
import bisq.support.moderator.ModerationRequestService;
import bisq.support.moderator.ReportToModeratorMessage;
import bisq.user.profile.UserProfile;
import javafx.beans.property.BooleanProperty;
Expand Down Expand Up @@ -67,11 +67,11 @@ private static class Controller implements InitWithDataController<InitData> {
@Getter
private final View view;
private final Model model;
private final ModeratorService moderatorService;
private final ModerationRequestService moderationRequestService;
private final ServiceProvider serviceProvider;

private Controller(ServiceProvider serviceProvider) {
moderatorService = serviceProvider.getSupportService().getModeratorService();
moderationRequestService = serviceProvider.getSupportService().getModerationRequestService();
this.serviceProvider = serviceProvider;
model = new Model();
view = new View(model, this);
Expand Down Expand Up @@ -101,7 +101,7 @@ void onReport() {
return;
}

moderatorService.reportUserProfile(model.getAccusedUserProfile(), message);
moderationRequestService.reportUserProfile(model.getAccusedUserProfile(), message);
onCancel();
}

Expand Down
37 changes: 30 additions & 7 deletions chat/src/main/java/bisq/chat/ChatChannelService.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,32 @@ public void setChatChannelNotificationType(ChatChannel<? extends ChatMessage> ch
}

public void addMessage(M message, C channel) {
if (bannedUserService.isUserProfileBanned(message.getAuthorUserProfileId())) {
String authorUserProfileId = message.getAuthorUserProfileId();
if (bannedUserService.isUserProfileBanned(authorUserProfileId)) {
log.warn("Message ignored as sender is banned");
return;
}
if (bannedUserService.isRateLimitExceeding(authorUserProfileId)) {
log.warn("Message ignored as sender exceeded rate limit");
return;
}
synchronized (getPersistableStore()) {
channel.addChatMessage(message);
boolean changed = channel.addChatMessage(message);
if (changed) {
checkRateLimit(authorUserProfileId, message.getDate());
}
}
persist();
}

protected boolean isValid(M message) {
if (bannedUserService.isUserProfileBanned(message.getAuthorUserProfileId())) {
log.warn("Message invalid as sender is banned. AuthorUserProfileId={}",message.getAuthorUserProfileId());
String authorUserProfileId = message.getAuthorUserProfileId();
if (bannedUserService.isUserProfileBanned(authorUserProfileId)) {
log.warn("Message invalid as sender is banned. AuthorUserProfileId={}", authorUserProfileId);
return false;
}
if (bannedUserService.isRateLimitExceeding(authorUserProfileId)) {
log.warn("Message ignored as sender exceeded rate limit");
return false;
}
return true;
Expand Down Expand Up @@ -123,12 +136,20 @@ public Optional<C> getDefaultChannel() {
protected abstract String getChannelTitlePostFix(ChatChannel<? extends ChatMessage> chatChannel);

protected void addMessageReaction(ChatMessageReaction chatMessageReaction, M message) {
if (bannedUserService.isUserProfileBanned(chatMessageReaction.getUserProfileId())) {
log.warn("Reaction ignored as sender is banned.");
String authorUserProfileId = chatMessageReaction.getUserProfileId();
if (bannedUserService.isUserProfileBanned(authorUserProfileId)) {
log.warn("ChatMessageReaction ignored as sender is banned.");
return;
}
if (bannedUserService.isRateLimitExceeding(authorUserProfileId)) {
log.warn("ChatMessageReaction ignored as sender exceeded rate limit");
return;
}
synchronized (getPersistableStore()) {
message.addChatMessageReaction(chatMessageReaction);
boolean changed = message.addChatMessageReaction(chatMessageReaction);
if (changed) {
checkRateLimit(authorUserProfileId, chatMessageReaction.getDate());
}
}
persist();
}
Expand All @@ -139,4 +160,6 @@ protected void removeMessageReaction(ChatMessageReaction chatMessageReaction, M
}
persist();
}

protected abstract void checkRateLimit(String authorUserProfileId, long messageDate);
}
2 changes: 1 addition & 1 deletion chat/src/main/java/bisq/chat/ChatMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -184,5 +184,5 @@ public boolean canShowReactions() {
return false;
}

public abstract void addChatMessageReaction(ChatMessageReaction reaction);
public abstract boolean addChatMessageReaction(ChatMessageReaction reaction);
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,20 @@ public BisqEasyOpenTradeMessage(String tradeId,
}

public BisqEasyOpenTradeMessage(String tradeId,
String messageId,
ChatChannelDomain chatChannelDomain,
String channelId,
UserProfile senderUserProfile,
String receiverUserProfileId,
NetworkId receiverNetworkId,
@Nullable String text,
Optional<Citation> citation,
long date,
boolean wasEdited,
Optional<UserProfile> mediator,
ChatMessageType chatMessageType,
Optional<BisqEasyOffer> bisqEasyOffer,
Set<BisqEasyOpenTradeMessageReaction> reactions) {
String messageId,
ChatChannelDomain chatChannelDomain,
String channelId,
UserProfile senderUserProfile,
String receiverUserProfileId,
NetworkId receiverNetworkId,
@Nullable String text,
Optional<Citation> citation,
long date,
boolean wasEdited,
Optional<UserProfile> mediator,
ChatMessageType chatMessageType,
Optional<BisqEasyOffer> bisqEasyOffer,
Set<BisqEasyOpenTradeMessageReaction> reactions) {
super(messageId, chatChannelDomain, channelId, senderUserProfile, receiverUserProfileId,
receiverNetworkId, text, citation, date, wasEdited, chatMessageType, reactions);
this.tradeId = tradeId;
Expand Down Expand Up @@ -222,8 +222,7 @@ public boolean canShowReactions() {
}

@Override
public void addChatMessageReaction(ChatMessageReaction newReaction) {
BisqEasyOpenTradeMessageReaction newBisqEasyOpenTradeReaction = (BisqEasyOpenTradeMessageReaction) newReaction;
addPrivateChatMessageReaction(newBisqEasyOpenTradeReaction);
public boolean addChatMessageReaction(ChatMessageReaction chatMessageReaction) {
return addPrivateChatMessageReaction((BisqEasyOpenTradeMessageReaction) chatMessageReaction);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -261,4 +261,9 @@ protected abstract R createAndGetNewPrivateChatMessageReaction(M message,
Reaction reaction,
String messageReactionId,
boolean isRemoved);

@Override
protected void checkRateLimit(String authorUserProfileId, long messageDate) {
// For private messages we don't check, as user can ignore anyway the peer.
}
}
29 changes: 16 additions & 13 deletions chat/src/main/java/bisq/chat/priv/PrivateChatMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,19 +130,22 @@ public NetworkId getReceiver() {
return receiverNetworkId;
}

public void addPrivateChatMessageReaction(R newReaction) {
getChatMessageReactions().stream()
public boolean addPrivateChatMessageReaction(R newReaction) {
Optional<R> existingReaction = getChatMessageReactions().stream()
.filter(privateChatReaction -> privateChatReaction.matches(newReaction))
.findFirst()
.ifPresentOrElse(
existingPrivateChatReaction -> {
if (newReaction.getDate() > existingPrivateChatReaction.getDate()) {
// only update if more recent
getChatMessageReactions().remove(existingPrivateChatReaction);
getChatMessageReactions().add(newReaction);
}
},
() -> getChatMessageReactions().add(newReaction)
);
.findFirst();
if (existingReaction.isPresent()) {
R existingPrivateChatReaction = existingReaction.get();
if (newReaction.getDate() > existingPrivateChatReaction.getDate()) {
// only update if more recent
getChatMessageReactions().remove(existingPrivateChatReaction);
return getChatMessageReactions().add(newReaction);
} else {
// Ignore older reaction
return false;
}
} else {
return getChatMessageReactions().add(newReaction);
}
}
}
54 changes: 49 additions & 5 deletions chat/src/main/java/bisq/chat/pub/PublicChatChannelService.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@
import bisq.chat.*;
import bisq.chat.reactions.ChatMessageReaction;
import bisq.chat.reactions.Reaction;
import bisq.common.observable.Pin;
import bisq.network.NetworkService;
import bisq.network.identity.NetworkIdWithKeyPair;
import bisq.network.p2p.ServiceNode;
import bisq.network.p2p.services.data.BroadcastResult;
import bisq.network.p2p.services.data.DataService;
import bisq.network.p2p.services.data.inventory.InventoryService;
import bisq.network.p2p.services.data.storage.DistributedData;
import bisq.network.p2p.services.data.storage.auth.AuthenticatedData;
import bisq.persistence.PersistableStore;
Expand All @@ -33,7 +36,9 @@
import lombok.extern.slf4j.Slf4j;

import java.security.KeyPair;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import static com.google.common.base.Preconditions.checkArgument;
Expand All @@ -42,6 +47,10 @@
public abstract class PublicChatChannelService<M extends PublicChatMessage, C extends PublicChatChannel<M>,
S extends PersistableStore<S>, R extends ChatMessageReaction> extends ChatChannelService<M, C, S> implements DataService.Listener {

private boolean initialized = false;
private boolean allInventoryDataReceived = false;
private final Set<Pin> allInventoryDataReceivedPins = new HashSet<>();

public PublicChatChannelService(NetworkService networkService,
UserService userService,
ChatChannelDomain chatChannelDomain) {
Expand All @@ -61,13 +70,26 @@ public CompletableFuture<Boolean> initialize() {
networkService.getDataService().ifPresent(dataService ->
dataService.getAuthenticatedData().forEach(this::handleAuthenticatedDataAdded));

networkService.getSupportedTransportTypes().forEach(type ->
networkService.getServiceNodesByTransport().findServiceNode(type)
.flatMap(ServiceNode::getInventoryService).stream()
.map(InventoryService::getInventoryRequestService)
.forEach(inventoryRequestService -> {
Pin pin = inventoryRequestService.getAllDataReceived().addObserver(allDataReceived -> {
if (allDataReceived) {
allInventoryDataReceived = true;
}
});
allInventoryDataReceivedPins.add(pin);
}));
initialized= true;
return CompletableFuture.completedFuture(true);
}

protected abstract void handleAuthenticatedDataAdded(AuthenticatedData authenticatedData);

@Override
public CompletableFuture<Boolean> shutdown() {
allInventoryDataReceivedPins.forEach(Pin::unbind);
allInventoryDataReceivedPins.clear();
networkService.removeDataServiceListener(this);
return CompletableFuture.completedFuture(true);
}
Expand All @@ -87,12 +109,21 @@ public CompletableFuture<BroadcastResult> publishChatMessage(String text,

public CompletableFuture<BroadcastResult> publishChatMessage(M message,
UserIdentity userIdentity) {
if (bannedUserService.isUserProfileBanned(message.getAuthorUserProfileId())) {
String authorUserProfileId = message.getAuthorUserProfileId();

// For rate limit violation we let the user know that his message was not sent, by not inserting the message.
if (bannedUserService.isRateLimitExceeding(authorUserProfileId)) {
return CompletableFuture.failedFuture(new RuntimeException());
}

// Sender adds the message at sending to avoid the delayed display if using the received message from the network.
findChannel(message.getChannelId()).ifPresent(channel -> addMessage(message, channel));

// For banned users we hide that their message is not published by inserting it to their local message list.
if (bannedUserService.isUserProfileBanned(authorUserProfileId)) {
return CompletableFuture.failedFuture(new RuntimeException());
}

KeyPair keyPair = userIdentity.getNetworkIdWithKeyPair().getKeyPair();
return networkService.publishAuthenticatedData(message, keyPair);
}
Expand All @@ -108,7 +139,8 @@ public CompletableFuture<BroadcastResult> publishEditedChatMessage(M originalCha
});
}

public CompletableFuture<BroadcastResult> deleteChatMessage(M chatMessage, NetworkIdWithKeyPair networkIdWithKeyPair) {
public CompletableFuture<BroadcastResult> deleteChatMessage(M chatMessage,
NetworkIdWithKeyPair networkIdWithKeyPair) {
return networkService.removeAuthenticatedData(chatMessage, networkIdWithKeyPair.getKeyPair());
}

Expand All @@ -129,7 +161,8 @@ public CompletableFuture<BroadcastResult> publishChatMessageReaction(M message,
return networkService.publishAuthenticatedData((DistributedData) chatMessageReaction, keyPair);
}

public CompletableFuture<BroadcastResult> deleteChatMessageReaction(R chatMessageReaction, NetworkIdWithKeyPair networkIdWithKeyPair) {
public CompletableFuture<BroadcastResult> deleteChatMessageReaction(R chatMessageReaction,
NetworkIdWithKeyPair networkIdWithKeyPair) {
checkArgument(chatMessageReaction instanceof DistributedData, "A public chat message reaction needs to implement DistributedData.");
return networkService.removeAuthenticatedData((DistributedData) chatMessageReaction, networkIdWithKeyPair.getKeyPair());
}
Expand Down Expand Up @@ -163,6 +196,8 @@ protected void removeMessage(M message, C channel) {
persist();
}

protected abstract void handleAuthenticatedDataAdded(AuthenticatedData authenticatedData);

protected abstract M createChatMessage(String text,
Optional<Citation> citation,
C publicChannel,
Expand All @@ -189,4 +224,13 @@ protected void processRemovedReaction(R chatMessageReaction) {
}

protected abstract R createChatMessageReaction(M message, Reaction reaction, UserIdentity userIdentity);


protected void checkRateLimit(String authorUserProfileId, long messageDate) {
// If we receive the message from the network after inventory requests are completed, we use our local receive time.
// Otherwise, at batch processing inventory data we use the senders date.
// Using the senders date for all cases would add risk for abuse by manipulating the date.
long timestamp = allInventoryDataReceived && initialized ? System.currentTimeMillis() : messageDate;
bannedUserService.checkRateLimit(authorUserProfileId, timestamp);
}
}
4 changes: 2 additions & 2 deletions chat/src/main/java/bisq/chat/pub/PublicChatMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public boolean isDataInvalid(byte[] pubKeyHash) {
}

@Override
public void addChatMessageReaction(ChatMessageReaction reaction) {
getChatMessageReactions().add(reaction);
public boolean addChatMessageReaction(ChatMessageReaction reaction) {
return getChatMessageReactions().add(reaction);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,7 @@ public boolean canShowReactions() {
}

@Override
public void addChatMessageReaction(ChatMessageReaction newReaction) {
TwoPartyPrivateChatMessageReaction newTwoPartyReaction = (TwoPartyPrivateChatMessageReaction) newReaction;
addPrivateChatMessageReaction(newTwoPartyReaction);
public boolean addChatMessageReaction(ChatMessageReaction chatMessageReaction) {
return addPrivateChatMessageReaction((TwoPartyPrivateChatMessageReaction) chatMessageReaction);
}
}
Loading

0 comments on commit 6536f45

Please sign in to comment.