From 88711ae018a384eacca737d15af29e19e7feb1d2 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 7 Aug 2021 15:56:08 +0100 Subject: [PATCH] Delete messages from cache after 1 hour to reduce memory usage. This would allow a duplicate message to be send again after an hour, but this is okay as it is only really designed to prevent frequent spamming of the same content. --- .../chat/ChatDuplicateMessageFilter.java | 140 ++++++++++++++++-- .../qortal/transaction/ChatTransaction.java | 2 +- 2 files changed, 130 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/qortal/chat/ChatDuplicateMessageFilter.java b/src/main/java/org/qortal/chat/ChatDuplicateMessageFilter.java index 33a8784a..ae63f4c2 100644 --- a/src/main/java/org/qortal/chat/ChatDuplicateMessageFilter.java +++ b/src/main/java/org/qortal/chat/ChatDuplicateMessageFilter.java @@ -1,19 +1,53 @@ package org.qortal.chat; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import org.qortal.settings.Settings; +import org.qortal.utils.NTP; + +import java.util.*; import java.util.concurrent.ConcurrentHashMap; -public class ChatDuplicateMessageFilter { +public class ChatDuplicateMessageFilter extends Thread { + + public static class SimpleChatMessage { + private long timestamp; + private String message; + + public SimpleChatMessage(long timestamp, String message) { + this.timestamp = timestamp; + this.message = message; + } + + public long getTimestamp() { + return this.timestamp; + } + + public String getMessage() { + return this.message; + } + + @Override + public boolean equals(Object other) { + if (other == this) + return true; + + if (!(other instanceof SimpleChatMessage)) + return false; + + SimpleChatMessage otherMessage = (SimpleChatMessage) other; + + return Objects.equals(this.getMessage(), otherMessage.getMessage()); + } + } + private static ChatDuplicateMessageFilter instance; private volatile boolean isStopping = false; - private static final int numberOfUniqueMessagesToMonitor = 3; + private static final int numberOfUniqueMessagesToMonitor = 3; // Only hold the last 3 messages in memory + private static final long maxMessageAge = 60 * 60 * 1000L; // Forget messages after 1 hour // Maintain a short list of recent chat messages for each address, to save having to query the database every time - private Map> recentMessages = new ConcurrentHashMap<>(); + private Map> recentMessages = new ConcurrentHashMap<>(); public ChatDuplicateMessageFilter() { @@ -22,23 +56,48 @@ public class ChatDuplicateMessageFilter { public static synchronized ChatDuplicateMessageFilter getInstance() { if (instance == null) { instance = new ChatDuplicateMessageFilter(); + instance.start(); } return instance; } - public boolean isDuplicateMessage(String address, String message) { + @Override + public void run() { + Thread.currentThread().setName("Duplicate Chat Message Filter"); + + try { + while (!isStopping) { + Thread.sleep(60000); + + this.cleanup(); + } + } catch (InterruptedException e) { + // Fall-through to exit thread... + } + } + + public void shutdown() { + isStopping = true; + this.interrupt(); + } + + + public boolean isDuplicateMessage(String address, long timestamp, String message) { boolean isDuplicateMessage; boolean messagesUpdated = false; - // Add timestamp to array for address - List messages = new ArrayList<>(); + SimpleChatMessage thisMessage = new SimpleChatMessage(timestamp, message); + + // Add message to array for address + List messages = new ArrayList<>(); if (this.recentMessages.containsKey(address)) { messages = this.recentMessages.get(address); } - if (!messages.contains(message)) { - messages.add(message); + // Check for duplicate, and add if unique + if (!messages.contains(thisMessage)) { + messages.add(thisMessage); this.recentMessages.put(address, messages); messagesUpdated = true; isDuplicateMessage = false; @@ -54,6 +113,18 @@ public class ChatDuplicateMessageFilter { messagesUpdated = true; } + // Ensure we're not holding on to messages for longer than a defined time period + Iterator iterator = messages.iterator(); + long now = NTP.getTime(); + while (iterator.hasNext()) { + SimpleChatMessage simpleChatMessage = (SimpleChatMessage) iterator.next(); + if (simpleChatMessage.getTimestamp() < now - maxMessageAge) { + // Older than tracked interval + iterator.remove(); + messagesUpdated = true; + } + } + if (messagesUpdated) { if (messages.size() > 0) { this.recentMessages.put(address, messages); @@ -66,4 +137,51 @@ public class ChatDuplicateMessageFilter { return isDuplicateMessage; } + + private void cleanup() { + + // Cleanup map of addresses and messages + this.deleteOldMessagesForAllAddresses(); + } + + private void deleteOldMessagesForAddress(String address, long now) { + if (address == null) { + return; + } + + if (this.recentMessages.containsKey(address)) { + boolean messagesUpdated = false; + + List messages = recentMessages.get(address); + + // Ensure we're not holding on to messages for longer than a defined time period + Iterator iterator = messages.iterator(); + while (iterator.hasNext()) { + SimpleChatMessage simpleChatMessage = (SimpleChatMessage) iterator.next(); + if (simpleChatMessage.getTimestamp() < now - maxMessageAge) { + // Older than tracked interval + iterator.remove(); + messagesUpdated = true; + } + } + + // Update messages for address + if (messagesUpdated) { + if (messages.size() > 0) { + this.recentMessages.put(address, messages); + } + else { + this.recentMessages.remove(address); + } + } + } + } + + private void deleteOldMessagesForAllAddresses() { + long now = NTP.getTime(); + for (Map.Entry> entry : this.recentMessages.entrySet()) { + this.deleteOldMessagesForAddress(entry.getKey(), now); + } + } + } diff --git a/src/main/java/org/qortal/transaction/ChatTransaction.java b/src/main/java/org/qortal/transaction/ChatTransaction.java index 529a1ccb..0b5414be 100644 --- a/src/main/java/org/qortal/transaction/ChatTransaction.java +++ b/src/main/java/org/qortal/transaction/ChatTransaction.java @@ -172,7 +172,7 @@ public class ChatTransaction extends Transaction { if (!chatTransactionData.getIsEncrypted() && chatTransactionData.getIsText()) { ChatDuplicateMessageFilter duplicateFilter = ChatDuplicateMessageFilter.getInstance(); String message58 = Base58.encode(chatTransactionData.getData()); - if (duplicateFilter.isDuplicateMessage(chatTransactionData.getSender(), message58)) + if (duplicateFilter.isDuplicateMessage(chatTransactionData.getSender(), chatTransactionData.getTimestamp(), message58)) return ValidationResult.DUPLICATE_MESSAGE; }