MessagesService.java

package dev.aura.bungeechat.message;

import com.typesafe.config.Config;
import dev.aura.bungeechat.account.BungeecordAccountManager;
import dev.aura.bungeechat.api.account.AccountManager;
import dev.aura.bungeechat.api.account.BungeeChatAccount;
import dev.aura.bungeechat.api.enums.ChannelType;
import dev.aura.bungeechat.api.filter.BlockMessageException;
import dev.aura.bungeechat.api.filter.FilterManager;
import dev.aura.bungeechat.api.module.ModuleManager;
import dev.aura.bungeechat.api.placeholder.BungeeChatContext;
import dev.aura.bungeechat.api.placeholder.InvalidContextError;
import dev.aura.bungeechat.chatlog.ChatLoggingManager;
import dev.aura.bungeechat.module.BungeecordModuleManager;
import dev.aura.bungeechat.module.IgnoringModule;
import dev.aura.bungeechat.permission.Permission;
import dev.aura.bungeechat.permission.PermissionManager;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;
import lombok.Setter;
import lombok.experimental.UtilityClass;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.config.ServerInfo;

@UtilityClass
public class MessagesService {
  @Setter private static List<List<String>> multiCastServerGroups = null;

  public static void unsetMultiCastServerGroups() {
    setMultiCastServerGroups(null);
  }

  public static void sendPrivateMessage(CommandSender sender, CommandSender target, String message)
      throws InvalidContextError {
    sendPrivateMessage(new Context(sender, target, message));
  }

  public static void sendPrivateMessage(BungeeChatContext context) throws InvalidContextError {
    context.require(
        BungeeChatContext.HAS_SENDER, BungeeChatContext.HAS_TARGET, BungeeChatContext.HAS_MESSAGE);

    Optional<BungeeChatAccount> account = context.getSender();
    BungeeChatAccount senderAccount = account.get();
    BungeeChatAccount targetAccount = context.getTarget().get();
    CommandSender sender = BungeecordAccountManager.getCommandSender(senderAccount).get();
    CommandSender target = BungeecordAccountManager.getCommandSender(targetAccount).get();
    boolean filterPrivateMessages =
        BungeecordModuleManager.MESSENGER_MODULE
            .getModuleSection()
            .getBoolean("filterPrivateMessages");

    if (targetAccount.hasIgnored(senderAccount)
        && !PermissionManager.hasPermission(sender, Permission.BYPASS_IGNORE)) {
      MessagesService.sendMessage(sender, Messages.HAS_IGNORED.get(context));

      return;
    }

    Optional<String> messageSender =
        preProcessMessage(context, account, Format.MESSAGE_SENDER, filterPrivateMessages);

    if (messageSender.isPresent()) {
      MessagesService.sendMessage(sender, messageSender.get());

      String messageTarget =
          preProcessMessage(context, account, Format.MESSAGE_TARGET, filterPrivateMessages, true)
              .get();
      MessagesService.sendMessage(target, messageTarget);

      if (ModuleManager.isModuleActive(BungeecordModuleManager.SPY_MODULE)
          && !PermissionManager.hasPermission(account.get(), Permission.COMMAND_SOCIALSPY_EXEMPT)) {
        String socialSpyMessage =
            preProcessMessage(context, account, Format.SOCIAL_SPY, false).get();

        sendToMatchingPlayers(
            socialSpyMessage,
            acc ->
                (!acc.getUniqueId().equals(senderAccount.getUniqueId()))
                    && (!acc.getUniqueId().equals(targetAccount.getUniqueId()))
                    && acc.hasSocialSpyEnabled());
      }
    }

    if (BungeecordModuleManager.CHAT_LOGGING_MODULE
        .getModuleSection()
        .getBoolean("privateMessages")) {
      ChatLoggingManager.logMessage("PM to " + targetAccount.getName(), context);
    }
  }

  public static void sendChannelMessage(CommandSender sender, ChannelType channel, String message)
      throws InvalidContextError {
    sendChannelMessage(new Context(sender, message), channel);
  }

  public static void sendChannelMessage(BungeeChatContext context, ChannelType channel)
      throws InvalidContextError {
    context.require(BungeeChatContext.HAS_SENDER, BungeeChatContext.HAS_MESSAGE);

    switch (channel) {
      case GLOBAL:
        sendGlobalMessage(context);
        break;
      case LOCAL:
        sendLocalMessage(context);
        break;
      case STAFF:
        sendStaffMessage(context);
        break;
      case HELP:
        sendHelpMessage(context);
        break;
      default:
        // Ignore
        break;
    }
  }

  public static void sendGlobalMessage(CommandSender sender, String message)
      throws InvalidContextError {
    sendGlobalMessage(new Context(sender, message));
  }

  public static void sendGlobalMessage(BungeeChatContext context) throws InvalidContextError {
    context.require(BungeeChatContext.HAS_SENDER, BungeeChatContext.HAS_MESSAGE);

    Optional<BungeeChatAccount> account = context.getSender();
    Optional<String> finalMessage = preProcessMessage(context, Format.GLOBAL_CHAT);

    sendToMatchingPlayers(finalMessage, getGlobalPredicate(), getNotIgnoredPredicate(account));

    ChatLoggingManager.logMessage(ChannelType.GLOBAL, context);
  }

  public static void sendLocalMessage(CommandSender sender, String message)
      throws InvalidContextError {
    sendLocalMessage(new Context(sender, message));
  }

  public static void sendLocalMessage(BungeeChatContext context) throws InvalidContextError {
    context.require(BungeeChatContext.HAS_SENDER, BungeeChatContext.HAS_MESSAGE);

    Optional<BungeeChatAccount> account = context.getSender();
    Optional<String> finalMessage = preProcessMessage(context, Format.LOCAL_CHAT);
    String localServerName =
        context.hasServer() ? context.getServer().get() : context.getSender().get().getServerName();
    Predicate<BungeeChatAccount> isLocal = getLocalPredicate(localServerName);
    Predicate<BungeeChatAccount> notIgnored = getNotIgnoredPredicate(account);

    sendToMatchingPlayers(finalMessage, isLocal, notIgnored);

    ChatLoggingManager.logMessage(ChannelType.LOCAL, context);

    if (ModuleManager.isModuleActive(BungeecordModuleManager.SPY_MODULE)) {
      String localSpyMessage = preProcessMessage(context, account, Format.LOCAL_SPY, false).get();
      Predicate<BungeeChatAccount> isNotLocal = isLocal.negate();

      sendToMatchingPlayers(
          localSpyMessage, BungeeChatAccount::hasLocalSpyEnabled, isNotLocal, notIgnored);
    }
  }

  public static void sendTransparentMessage(BungeeChatContext context) throws InvalidContextError {
    context.require(BungeeChatContext.HAS_SENDER, BungeeChatContext.HAS_MESSAGE);

    Optional<BungeeChatAccount> account = context.getSender();
    String localServerName =
        context.hasServer() ? context.getServer().get() : context.getSender().get().getServerName();
    Predicate<BungeeChatAccount> isLocal = getLocalPredicate(localServerName);

    ChatLoggingManager.logMessage(ChannelType.LOCAL, context);

    if (ModuleManager.isModuleActive(BungeecordModuleManager.SPY_MODULE)
        && !PermissionManager.hasPermission(account.get(), Permission.COMMAND_LOCALSPY_EXEMPT)) {
      String localSpyMessage = preProcessMessage(context, account, Format.LOCAL_SPY, false).get();
      Predicate<BungeeChatAccount> isNotLocal = isLocal.negate();

      sendToMatchingPlayers(localSpyMessage, BungeeChatAccount::hasLocalSpyEnabled, isNotLocal);
    }
  }

  public static void sendStaffMessage(CommandSender sender, String message)
      throws InvalidContextError {
    sendStaffMessage(new Context(sender, message));
  }

  public static void sendStaffMessage(BungeeChatContext context) throws InvalidContextError {
    context.require(BungeeChatContext.HAS_SENDER, BungeeChatContext.HAS_MESSAGE);

    Optional<String> finalMessage = preProcessMessage(context, Format.STAFF_CHAT);

    sendToMatchingPlayers(
        finalMessage, pp -> PermissionManager.hasPermission(pp, Permission.COMMAND_STAFFCHAT_VIEW));

    ChatLoggingManager.logMessage(ChannelType.STAFF, context);
  }

  public static void sendHelpMessage(CommandSender sender, String message)
      throws InvalidContextError {
    sendHelpMessage(new Context(sender, message));
  }

  public static void sendHelpMessage(BungeeChatContext context) throws InvalidContextError {
    context.require(BungeeChatContext.HAS_SENDER, BungeeChatContext.HAS_MESSAGE);

    Optional<String> finalMessage = preProcessMessage(context, Format.HELP_OP);
    BungeeChatAccount sender = context.getSender().get();

    sendToMatchingPlayers(
        finalMessage,
        pp ->
            PermissionManager.hasPermission(pp, Permission.COMMAND_HELPOP_VIEW)
                || sender.equals(pp));

    ChatLoggingManager.logMessage(ChannelType.HELP, context);
  }

  public static void sendJoinMessage(CommandSender sender) throws InvalidContextError {
    sendJoinMessage(new Context(sender));
  }

  public static void sendJoinMessage(BungeeChatContext context) throws InvalidContextError {
    context.require(BungeeChatContext.HAS_SENDER);

    String finalMessage = Format.JOIN_MESSAGE.get(context);
    Predicate<BungeeChatAccount> predicate = getPermissionPredicate(Permission.MESSAGE_JOIN_VIEW);

    // This condition checks if the player is present and vanished
    if (context.getSender().filter(BungeeChatAccount::isVanished).isPresent()) {
      predicate = predicate.and(getPermissionPredicate(Permission.COMMAND_VANISH_VIEW));
    }

    sendToMatchingPlayers(finalMessage, predicate);

    context.setMessage(finalMessage);
    ChatLoggingManager.logMessage("JOIN", context);
  }

  public static void sendLeaveMessage(CommandSender sender) throws InvalidContextError {
    sendLeaveMessage(new Context(sender));
  }

  public static void sendLeaveMessage(BungeeChatContext context) throws InvalidContextError {
    context.require(BungeeChatContext.HAS_SENDER);

    String finalMessage = Format.LEAVE_MESSAGE.get(context);
    Predicate<BungeeChatAccount> predicate = getPermissionPredicate(Permission.MESSAGE_LEAVE_VIEW);

    // This condition checks if the player is present and vanished
    if (context.getSender().filter(BungeeChatAccount::isVanished).isPresent()) {
      predicate = predicate.and(getPermissionPredicate(Permission.COMMAND_VANISH_VIEW));
    }

    sendToMatchingPlayers(finalMessage, predicate);

    context.setMessage(finalMessage);
    ChatLoggingManager.logMessage("LEAVE", context);
  }

  public static void sendSwitchMessage(CommandSender sender, ServerInfo server)
      throws InvalidContextError {
    sendSwitchMessage(sender, (server == null) ? null : server.getName());
  }

  public static void sendSwitchMessage(CommandSender sender, String server)
      throws InvalidContextError {
    final Context context = new Context(sender);
    if (server != null) context.setServer(server);

    sendSwitchMessage(context);
  }

  public static void sendSwitchMessage(BungeeChatContext context) throws InvalidContextError {
    context.require(BungeeChatContext.HAS_SENDER);

    String finalMessage = Format.SERVER_SWITCH.get(context);
    Predicate<BungeeChatAccount> predicate = getPermissionPredicate(Permission.MESSAGE_SWITCH_VIEW);

    // This condition checks if the player is present and vanished
    if (context.getSender().filter(BungeeChatAccount::isVanished).isPresent()) {
      predicate = predicate.and(getPermissionPredicate(Permission.COMMAND_VANISH_VIEW));
    }

    sendToMatchingPlayers(finalMessage, predicate);

    context.setMessage(finalMessage);
    ChatLoggingManager.logMessage("SWITCH", context);
  }

  public static Optional<String> preProcessMessage(BungeeChatContext context, Format format)
      throws InvalidContextError {
    return preProcessMessage(context, context.getSender(), format, true);
  }

  public static Optional<String> preProcessMessage(
      BungeeChatContext context, Optional<BungeeChatAccount> account, Format format)
      throws InvalidContextError {
    return preProcessMessage(context, account, format, true);
  }

  public static Optional<String> preProcessMessage(
      BungeeChatContext context,
      Optional<BungeeChatAccount> account,
      Format format,
      boolean runFilters) {
    return preProcessMessage(context, account, format, runFilters, false);
  }

  public static Optional<String> preProcessMessage(
      BungeeChatContext context,
      Optional<BungeeChatAccount> account,
      Format format,
      boolean runFilters,
      boolean ignoreBlockMessageExceptions)
      throws InvalidContextError {
    context.require(BungeeChatContext.HAS_MESSAGE);

    BungeeChatAccount playerAccount = account.get();
    CommandSender player = BungeecordAccountManager.getCommandSender(playerAccount).get();
    String message = PlaceHolderUtil.transformAltColorCodes(context.getMessage().get(), account);

    if (runFilters) {
      try {
        message = FilterManager.applyFilters(playerAccount, message);
      } catch (BlockMessageException e) {
        if (!ignoreBlockMessageExceptions) {
          MessagesService.sendMessage(player, e.getMessage());

          return Optional.empty();
        }
      }
    }

    context.setMessage(message);

    return Optional.of(PlaceHolderUtil.getFullFormatMessage(format, context));
  }

  @SafeVarargs
  public static void sendToMatchingPlayers(
      Optional<String> finalMessage, Predicate<BungeeChatAccount>... playerFilters) {
    if (finalMessage.isPresent()) {
      sendToMatchingPlayers(finalMessage.get(), playerFilters);
    }
  }

  @SafeVarargs
  public static void sendToMatchingPlayers(
      String finalMessage, Predicate<BungeeChatAccount>... playerFilters) {
    Predicate<BungeeChatAccount> playerFiler =
        Arrays.stream(playerFilters).reduce(Predicate::and).orElse(acc -> true);

    AccountManager.getPlayerAccounts().stream()
        .filter(playerFiler)
        .forEach(
            account ->
                MessagesService.sendMessage(
                    BungeecordAccountManager.getCommandSender(account).get(), finalMessage));
  }

  public static Predicate<BungeeChatAccount> getServerListPredicate(Config section) {
    if (!section.getBoolean("enabled")) return account -> true;
    else {
      // TODO: Use wildcard string
      List<String> allowedServers = section.getStringList("list");

      return account -> allowedServers.contains(account.getServerName());
    }
  }

  public static Predicate<BungeeChatAccount> getGlobalPredicate() {
    return getServerListPredicate(
        BungeecordModuleManager.GLOBAL_CHAT_MODULE.getModuleSection().getConfig("serverList"));
  }

  public static Predicate<BungeeChatAccount> getServerPredicate(List<String> servers) {
    return account -> servers.contains(account.getServerName());
  }

  public static Predicate<BungeeChatAccount> getLocalPredicate(String serverName) {
    if (multiCastServerGroups == null) {
      return account -> serverName.equals(account.getServerName());
    } else {
      return account -> {
        final String accountServerName = account.getServerName();

        for (List<String> group : multiCastServerGroups) {
          if (group.contains(accountServerName)) {
            return group.contains(serverName);
          }
        }

        return serverName.equals(accountServerName);
      };
    }
  }

  public static Predicate<BungeeChatAccount> getLocalPredicate() {
    final Config serverList =
        BungeecordModuleManager.LOCAL_CHAT_MODULE.getModuleSection().getConfig("serverList");
    final Config passThruServerList =
        BungeecordModuleManager.LOCAL_CHAT_MODULE
            .getModuleSection()
            .getConfig("passThruServerList");

    return Stream.of(serverList, passThruServerList)
        .flatMap(MessagesService::serverListToPredicate)
        .collect(() -> account -> true, Predicate::and, Predicate::and);
  }

  private static Stream<Predicate<BungeeChatAccount>> serverListToPredicate(Config section) {
    if (section.getBoolean("enabled")) {
      // TODO: Use wildcard string
      List<String> allowedServers = section.getStringList("list");

      return Stream.of(account -> allowedServers.contains(account.getServerName()));
    } else {
      return Stream.empty();
    }
  }

  public static Predicate<BungeeChatAccount> getPermissionPredicate(Permission permission) {
    return account -> PermissionManager.hasPermission(account, permission);
  }

  public static Predicate<BungeeChatAccount> getNotIgnoredPredicate(
      Optional<BungeeChatAccount> sender) {
    return getNotIgnoredPredicate(sender.get());
  }

  public static Predicate<BungeeChatAccount> getNotIgnoredPredicate(BungeeChatAccount sender) {
    final IgnoringModule ignoringModule = BungeecordModuleManager.IGNORING_MODULE;

    return (ignoringModule.isEnabled()
            && ignoringModule.getModuleSection().getBoolean("ignoreChatMessages")
            && !PermissionManager.hasPermission(sender, Permission.BYPASS_IGNORE))
        ? account -> !account.hasIgnored(sender)
        : account -> true;
  }

  public static void sendMessage(CommandSender recipient, String message) {
    if ((message == null) || message.isEmpty()) return;

    recipient.sendMessage(TextComponent.fromLegacyText(message));
  }
}