DuplicationFilter.java

package dev.aura.bungeechat.filter;

import com.google.common.annotations.VisibleForTesting;
import dev.aura.bungeechat.api.account.BungeeChatAccount;
import dev.aura.bungeechat.api.filter.BlockMessageException;
import dev.aura.bungeechat.api.filter.BungeeChatFilter;
import dev.aura.bungeechat.api.filter.FilterManager;
import dev.aura.bungeechat.message.Messages;
import dev.aura.bungeechat.permission.Permission;
import dev.aura.bungeechat.permission.PermissionManager;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import lombok.Value;

public class DuplicationFilter implements BungeeChatFilter {
  private final ConcurrentMap<UUID, Queue<TimePointMessage>> playerMessagesStorage;
  private final int checkPastMessages;
  private final long expiryTimer;
  private final boolean noPermissions;

  public DuplicationFilter(int checkPastMessages, int expireAfter) {
    this(checkPastMessages, expireAfter, false);
  }

  @VisibleForTesting
  DuplicationFilter(int checkPastMessages, int expireAfter, boolean noPermissions) {
    playerMessagesStorage = new ConcurrentHashMap<>();
    this.checkPastMessages = checkPastMessages;
    expiryTimer = TimeUnit.SECONDS.toNanos(expireAfter);
    this.noPermissions = noPermissions;
  }

  @Override
  public String applyFilter(BungeeChatAccount sender, String message) throws BlockMessageException {
    if (!noPermissions && PermissionManager.hasPermission(sender, Permission.BYPASS_ANTI_DUPLICATE))
      return message;

    final UUID uuid = sender.getUniqueId();

    if (!playerMessagesStorage.containsKey(uuid)) {
      playerMessagesStorage.put(uuid, new ArrayDeque<>(checkPastMessages));
    }

    final Queue<TimePointMessage> playerMessages = playerMessagesStorage.get(uuid);
    final long now = System.nanoTime();
    final long expiry = now - expiryTimer;

    while (!playerMessages.isEmpty() && (playerMessages.peek().getTimePoint() < expiry)) {
      playerMessages.poll();
    }

    if (playerMessages.stream().map(TimePointMessage::getMessage).anyMatch(message::equals))
      throw new ExtendedBlockMessageException(Messages.ANTI_DUPLICATION, sender, message);

    if (playerMessages.size() == checkPastMessages) {
      playerMessages.remove();
    }

    playerMessages.add(new TimePointMessage(now, message));

    return message;
  }

  @Override
  public int getPriority() {
    return FilterManager.DUPLICATION_FILTER_PRIORITY;
  }

  @VisibleForTesting
  void clear() {
    playerMessagesStorage.clear();
  }

  @Value
  private static class TimePointMessage {
    private final long timePoint;
    private final String message;
  }
}