2424import org .slf4j .LoggerFactory ;
2525
2626import org .togetherjava .tjbot .config .Config ;
27+ import org .togetherjava .tjbot .config .MessageInfo ;
2728import org .togetherjava .tjbot .config .ScamBlockerConfig ;
28- import org .togetherjava .tjbot .features .MessageReceiverAdapter ;
29- import org .togetherjava .tjbot .features .UserInteractionType ;
30- import org .togetherjava .tjbot .features .UserInteractor ;
29+ import org .togetherjava .tjbot .features .*;
3130import org .togetherjava .tjbot .features .componentids .ComponentIdGenerator ;
3231import org .togetherjava .tjbot .features .componentids .ComponentIdInteractor ;
3332import org .togetherjava .tjbot .features .moderation .ModerationAction ;
3837import org .togetherjava .tjbot .logging .LogMarkers ;
3938
4039import java .awt .Color ;
41- import java .util .Collection ;
42- import java .util .EnumSet ;
43- import java .util .List ;
44- import java .util .Optional ;
45- import java .util .Set ;
40+ import java .time .Instant ;
41+ import java .time .temporal .ChronoUnit ;
42+ import java .util .*;
43+ import java .util .concurrent .TimeUnit ;
4644import java .util .function .Consumer ;
4745import java .util .function .Predicate ;
4846import java .util .function .UnaryOperator ;
5553 * If scam is detected, depending on the configuration, the blockers actions range from deleting the
5654 * message and banning the author to just logging the message for auditing.
5755 */
58- public final class ScamBlocker extends MessageReceiverAdapter implements UserInteractor {
56+ public final class ScamBlocker extends MessageReceiverAdapter implements UserInteractor , Routine {
5957 private static final Logger logger = LoggerFactory .getLogger (ScamBlocker .class );
6058 private static final Color AMBIENT_COLOR = Color .decode ("#CFBFF5" );
6159 private static final Set <ScamBlockerConfig .Mode > MODES_WITH_IMMEDIATE_DELETION =
@@ -72,6 +70,7 @@ public final class ScamBlocker extends MessageReceiverAdapter implements UserInt
7270 private final Predicate <String > hasRequiredRole ;
7371
7472 private final ComponentIdInteractor componentIdInteractor ;
73+ private final Set <MessageInfo > messageCache ;
7574
7675 /**
7776 * Creates a new listener to receive all message sent in any channel.
@@ -95,6 +94,7 @@ public ScamBlocker(ModerationActionsStore actionsStore, ScamHistoryStore scamHis
9594 hasRequiredRole = Pattern .compile (config .getSoftModerationRolePattern ()).asMatchPredicate ();
9695
9796 componentIdInteractor = new ComponentIdInteractor (getInteractionType (), getName ());
97+ messageCache = new HashSet <>();
9898 }
9999
100100 @ Override
@@ -124,7 +124,7 @@ public void onMessageReceived(MessageReceivedEvent event) {
124124
125125 Message message = event .getMessage ();
126126 String content = message .getContentDisplay ();
127- if (!scamDetector .isScam (content )) {
127+ if (!scamDetector .isScam (content ) && ! doSimilarMessageCheck ( event ) ) {
128128 return ;
129129 }
130130
@@ -136,6 +136,42 @@ public void onMessageReceived(MessageReceivedEvent event) {
136136 takeAction (event );
137137 }
138138
139+ @ Override
140+ public Schedule createSchedule () {
141+ return new Schedule (ScheduleMode .FIXED_RATE , 1 , 1 , TimeUnit .HOURS );
142+ }
143+
144+ @ Override
145+ public void runRoutine (JDA jda ) {
146+ Instant now = Instant .now ();
147+ messageCache .removeIf (m -> m .timestamp ().plus (1 , ChronoUnit .HOURS ).isBefore (now ));
148+ }
149+
150+ /**
151+ * Stores message data and if many messages of same author, different channel and same content
152+ * is posted several times, returns true.
153+ *
154+ * @param event the message event
155+ * @return true if the user spammed the message in several channels, false otherwise
156+ */
157+ private boolean doSimilarMessageCheck (MessageReceivedEvent event ) {
158+ long userId = event .getAuthor ().getIdLong ();
159+ long channelId = event .getChannel ().getIdLong ();
160+ int messageHash = getHash (event .getMessage ());
161+ Instant timestamp = event .getMessage ().getTimeCreated ().toInstant ();
162+ messageCache .add (new MessageInfo (userId , channelId , messageHash , timestamp ));
163+ return config .getScamBlocker ().getMaxSimilarMessages () < messageCache .stream ()
164+ .filter (m -> m .userId () == userId && m .messageHash () == messageHash )
165+ .count ();
166+ }
167+
168+ private int getHash (Message message ) {
169+ return message .getContentRaw ().hashCode () + message .getAttachments ()
170+ .stream ()
171+ .mapToInt (a -> a .getFileName ().hashCode ())
172+ .reduce (1 , (a , b ) -> a * b );
173+ }
174+
139175 private void takeActionWasAlreadyReported (MessageReceivedEvent event ) {
140176 // The user recently send the same scam already, and that was already reported and handled
141177 addScamToHistory (event );
0 commit comments