package com.supaham.commons.bukkit;
import com.google.common.base.Preconditions;
import com.supaham.commons.bukkit.modules.CommonModule;
import com.supaham.commons.bukkit.modules.ModuleContainer;
import com.supaham.commons.utils.StringUtils;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Chat silencing module. Supports global and per player chatting mute. This module allows for the
* ability to assign a message response as to why the player was muted whenever they try to chat.
* <p/>
* Please note that while this module does support global chat silence, it does NOT override
* per-player silencing. Meaning if {@link #unsilenceAll(boolean)} is called with the boolean as
* true, meaning only lift the global silence, and a player is specifically muted per
* {@link #silence(Player)} methods, they will still be silenced until their silence duration is
* over or they are unsilenced using {@link #unsilence(Player)}, or {@link #unsilenceAll()}
*
* @see #ChatSilencer(ModuleContainer)
* @see #silence(Player)
* @see #silenceAll()
* @since 0.2.3
*/
public class ChatSilencer extends CommonModule {
private SilenceData globalMuteData;
private Map<Player, SilenceData> silenced = new HashMap<>();
private Listener listener = new ChatListener();
public ChatSilencer(@Nonnull ModuleContainer container) {
super(container);
registerListener(this.listener);
}
/**
* Silences a {@link Player}'s chat events indefinitely with no response-reason. This is
* equivalent to calling {@link #silence(Player, int)} with the {@code int} as -1.
*
* @param player player to silence
*
* @return whether the silence was successful
*/
public boolean silence(@Nonnull Player player) {
return silence(player, -1);
}
/**
* Silences a {@link Player}'s chat events, indefinitely, with no response-reason. This is
* equivalent to calling {@link #silence(Player, int, String)} with the {@code String} as null.
*
* @param player player to silence
* @param duration duration of the silence to set, -1 for infinite
*
* @return whether the silence was successful
*/
public boolean silence(@Nonnull Player player, int duration) {
return silence(player, duration, null);
}
/**
* Silences a {@link Player}'s chat events, indefinitely, and gives a reason as to why when the
* player attempts to chat.
*
* @param player player to silence
* @param duration duration of the silence to set, -1 for infinite
*
* @return whether the silence was successful
*/
public boolean silence(@Nonnull Player player, int duration, @Nullable String muteMessage) {
Preconditions.checkNotNull(player, "player cannot be null.");
SilenceData data = new SilenceData(duration, muteMessage);
if (!data.isDone()) {
this.silenced.put(player, data);
return true;
}
return false;
}
/**
* Silences all chat events, indefinitely, with no response-reason. This is equivalent to calling
* {@link #silenceAll(int)} with the {@code int} as -1.
*
* @return whether the silence was successful
*/
public boolean silenceAll() {
return silenceAll(-1);
}
/**
* Silences all chat events, for a certain duration, with no response-reason. This is
* equivalent to calling {@link #silenceAll(int, String)} with the {@code String} as null.
*
* @param duration duration of the silence to set, -1 for infinite
*
* @return whether the silence was successful
*/
public boolean silenceAll(int duration) {
return silenceAll(duration, null);
}
/**
* Silences all chat events, for a certain duration, and gives a reason as to why when each
* player attempts to chat.
*
* @param duration duration (in ticks) of the silence to set, -1 for infinite
* @param muteMessage the message response for the global silence
*
* @return whether the silence was successful
*/
public boolean silenceAll(int duration, @Nullable String muteMessage) {
SilenceData data = new SilenceData(duration, muteMessage);
if (!data.isDone()) {
this.globalMuteData = data;
return true;
}
return false;
}
/**
* Unsilences a {@link Player}.
*
* @param player player to unsilence
*
* @return whether the player was previously silenced
*/
public boolean unsilence(@Nonnull Player player) {
Preconditions.checkNotNull(player, "player cannot be null.");
return this.silenced.remove(player) != null;
}
/**
* Unsilences all future chat events, globally and per player. This is equivalent to calling
* {@link #unsilenceAll(boolean)} with the boolean as false.
*/
public void unsilenceAll() {
unsilenceAll(false);
}
/**
* Removes the global silence. It is important to note that, if {@code onlyGlobal} is true, and a
* player is specifically silenced, they will still be silenced as global chat silence is
* separate from per-player.
*
* @param onlyGlobal whether to only unsilence global mute
*/
public void unsilenceAll(boolean onlyGlobal) {
this.globalMuteData = null;
if (!onlyGlobal) {
this.silenced.clear();
}
}
protected void handleEvent(AsyncPlayerChatEvent event) {
// The global mute takes power over the player mute, so the order of these events matter.
SilenceData data = ChatSilencer.this.globalMuteData;
SilenceData playerData = null;
if (data == null) {
data = playerData = ChatSilencer.this.silenced.get(event.getPlayer());
}
if (data != null) {
if (check(data)) {
event.setCancelled(true);
if (data.muteMessage != null) {
event.getPlayer().sendMessage(data.muteMessage);
}
} else if (playerData != null) { // The player is no longer muted.
unsilence(event.getPlayer());
}
}
}
protected void handleEvent(PlayerQuitEvent event) {
unsilence(event.getPlayer());
}
private boolean check(SilenceData data) {
if (getState().isIdle()) {
return false;
}
boolean muted = false;
if (data != null) {
if (!data.isDone()) {
muted = true;
} else {
if (data == globalMuteData) {
this.globalMuteData = null;
}
}
}
return muted;
}
private class SilenceData {
private long expires;
private String muteMessage;
public SilenceData(int duration, String muteMessage) {
this.muteMessage = StringUtils.stripToNull(muteMessage);
setExpires(duration);
}
public boolean isDone() {
return this.expires > -1 && System.currentTimeMillis() - this.expires >= 0;
}
public void setExpires(int duration) {
if (duration >= 0 && duration != Integer.MAX_VALUE) {
this.expires = System.currentTimeMillis() + ((long) duration * 50);
} else {
this.expires = -1;
}
}
}
private final class ChatListener implements Listener {
@EventHandler
public void onAsyncPlayerChat(AsyncPlayerChatEvent event) {
handleEvent(event);
}
@EventHandler
public void onPlayerQuit(@Nonnull PlayerQuitEvent event) {
handleEvent(event);
}
}
}