529 lines
18 KiB
Java
529 lines
18 KiB
Java
package me.totalfreedom.totalfreedommod.discord;
|
|
|
|
import com.google.common.base.Strings;
|
|
import java.time.Instant;
|
|
import java.time.ZonedDateTime;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
|
import javax.security.auth.login.LoginException;
|
|
|
|
import com.google.common.collect.ImmutableList;
|
|
import me.totalfreedom.totalfreedommod.FreedomService;
|
|
import me.totalfreedom.totalfreedommod.admin.Admin;
|
|
import me.totalfreedom.totalfreedommod.config.ConfigEntry;
|
|
import me.totalfreedom.totalfreedommod.discord.command.DiscordCommandManager;
|
|
import me.totalfreedom.totalfreedommod.player.PlayerData;
|
|
import me.totalfreedom.totalfreedommod.rank.Rank;
|
|
import me.totalfreedom.totalfreedommod.util.FLog;
|
|
import net.dv8tion.jda.api.EmbedBuilder;
|
|
import net.dv8tion.jda.api.JDA;
|
|
import net.dv8tion.jda.api.JDABuilder;
|
|
import net.dv8tion.jda.api.entities.Guild;
|
|
import net.dv8tion.jda.api.entities.Member;
|
|
import net.dv8tion.jda.api.entities.Message;
|
|
import net.dv8tion.jda.api.entities.MessageEmbed;
|
|
import net.dv8tion.jda.api.entities.PrivateChannel;
|
|
import net.dv8tion.jda.api.entities.Role;
|
|
import net.dv8tion.jda.api.entities.SelfUser;
|
|
import net.dv8tion.jda.api.entities.TextChannel;
|
|
import net.dv8tion.jda.api.entities.User;
|
|
import net.dv8tion.jda.api.events.ReadyEvent;
|
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
|
import net.dv8tion.jda.api.requests.GatewayIntent;
|
|
import net.dv8tion.jda.api.utils.ChunkingFilter;
|
|
import net.dv8tion.jda.api.utils.MemberCachePolicy;
|
|
import net.dv8tion.jda.internal.utils.concurrent.CountingThreadFactory;
|
|
import net.kyori.adventure.text.Component;
|
|
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
|
import net.md_5.bungee.api.ChatColor;
|
|
import org.apache.commons.lang.WordUtils;
|
|
import org.apache.commons.lang3.RandomStringUtils;
|
|
import org.bukkit.GameRule;
|
|
import org.bukkit.OfflinePlayer;
|
|
import org.bukkit.entity.Player;
|
|
import org.bukkit.event.EventHandler;
|
|
import org.bukkit.event.EventPriority;
|
|
import org.bukkit.event.entity.PlayerDeathEvent;
|
|
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
|
import org.bukkit.event.player.PlayerJoinEvent;
|
|
import org.bukkit.event.player.PlayerQuitEvent;
|
|
import org.jetbrains.annotations.NotNull;
|
|
|
|
public class Discord extends FreedomService
|
|
{
|
|
|
|
public static HashMap<String, PlayerData> LINK_CODES = new HashMap<>();
|
|
public static JDA bot = null;
|
|
public static DiscordCommandManager DISCORD_COMMAND_MANAGER;
|
|
public ScheduledThreadPoolExecutor RATELIMIT_EXECUTOR;
|
|
public List<CompletableFuture<Message>> sentMessages = new ArrayList<>();
|
|
public Boolean enabled = false;
|
|
private static final ImmutableList<String> DISCORD_SUBDOMAINS = ImmutableList.of("discordapp.com", "discord.com", "discord.gg");
|
|
|
|
public static String getCode(PlayerData playerData)
|
|
{
|
|
for (String code : LINK_CODES.keySet())
|
|
{
|
|
if (LINK_CODES.get(code).equals(playerData))
|
|
{
|
|
return code;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static boolean syncRoles(Admin admin, String discordID)
|
|
{
|
|
if (discordID == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Guild server = bot.getGuildById(ConfigEntry.DISCORD_SERVER_ID.getString());
|
|
if (server == null)
|
|
{
|
|
FLog.severe("The Discord server ID specified is invalid, or the bot is not on the server.");
|
|
return false;
|
|
}
|
|
|
|
Member member = server.getMemberById(discordID);
|
|
if (member == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Role adminRole = server.getRoleById(ConfigEntry.DISCORD_NEW_ADMIN_ROLE_ID.getString());
|
|
if (adminRole == null)
|
|
{
|
|
FLog.severe("The specified Admin role does not exist!");
|
|
return false;
|
|
}
|
|
|
|
Role senioradminRole = server.getRoleById(ConfigEntry.DISCORD_SENIOR_ADMIN_ROLE_ID.getString());
|
|
if (senioradminRole == null)
|
|
{
|
|
FLog.severe("The specified Senior Admin role does not exist!");
|
|
return false;
|
|
}
|
|
|
|
if (!admin.isActive())
|
|
{
|
|
if (member.getRoles().contains(adminRole))
|
|
{
|
|
server.removeRoleFromMember(member, adminRole).complete();
|
|
}
|
|
if (member.getRoles().contains(senioradminRole))
|
|
{
|
|
server.removeRoleFromMember(member, senioradminRole).complete();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (admin.getRank().equals(Rank.ADMIN))
|
|
{
|
|
if (!member.getRoles().contains(adminRole))
|
|
{
|
|
server.addRoleToMember(member, adminRole).complete();
|
|
}
|
|
if (member.getRoles().contains(senioradminRole))
|
|
{
|
|
server.removeRoleFromMember(member, senioradminRole).complete();
|
|
}
|
|
return true;
|
|
}
|
|
else if (admin.getRank().equals(Rank.SENIOR_ADMIN))
|
|
{
|
|
if (!member.getRoles().contains(senioradminRole))
|
|
{
|
|
server.addRoleToMember(member, senioradminRole).complete();
|
|
}
|
|
if (member.getRoles().contains(adminRole))
|
|
{
|
|
server.removeRoleFromMember(member, adminRole).complete();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void startBot()
|
|
{
|
|
DISCORD_COMMAND_MANAGER = new DiscordCommandManager();
|
|
DISCORD_COMMAND_MANAGER.init(this);
|
|
|
|
enabled = !Strings.isNullOrEmpty(ConfigEntry.DISCORD_TOKEN.getString());
|
|
if (!enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (bot != null)
|
|
{
|
|
RATELIMIT_EXECUTOR = new ScheduledThreadPoolExecutor(5, new CountingThreadFactory(this::poolIdentifier, "RateLimit"));
|
|
RATELIMIT_EXECUTOR.setRemoveOnCancelPolicy(true);
|
|
for (Object object : bot.getRegisteredListeners())
|
|
{
|
|
bot.removeEventListener(object);
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
bot = JDABuilder.createDefault(ConfigEntry.DISCORD_TOKEN.getString())
|
|
.addEventListeners(new PrivateMessageListener(),
|
|
new DiscordToMinecraftListener(),
|
|
new DiscordToAdminChatListener(),
|
|
new MessageReactionListener(),
|
|
new ListenerAdapter()
|
|
{
|
|
@Override
|
|
public void onReady(@NotNull ReadyEvent event)
|
|
{
|
|
new StartEvent().start();
|
|
}
|
|
})
|
|
.setAutoReconnect(true)
|
|
.setRateLimitPool(RATELIMIT_EXECUTOR)
|
|
.setChunkingFilter(ChunkingFilter.ALL)
|
|
.setMemberCachePolicy(MemberCachePolicy.ALL)
|
|
.enableIntents(GatewayIntent.GUILD_MEMBERS)
|
|
.build();
|
|
FLog.info("Discord integration has successfully enabled!");
|
|
}
|
|
catch (LoginException e)
|
|
{
|
|
FLog.warning("An invalid token for Discord integration was provided, the bot will not enable.");
|
|
enabled = false;
|
|
}
|
|
catch (IllegalArgumentException e)
|
|
{
|
|
FLog.warning("Discord integration failed to start.");
|
|
enabled = false;
|
|
}
|
|
catch (NoClassDefFoundError e)
|
|
{
|
|
FLog.warning("The JDA plugin is not installed, therefore the discord bot cannot start.");
|
|
FLog.warning("To resolve this error, please download the latest JDA from: https://github.com/AtlasMediaGroup/Minecraft-JDA/releases");
|
|
enabled = false;
|
|
}
|
|
|
|
}
|
|
|
|
public String poolIdentifier()
|
|
{
|
|
return "JDA";
|
|
}
|
|
|
|
public void clearQueue()
|
|
{
|
|
for (CompletableFuture<Message> messages : sentMessages)
|
|
{
|
|
if (!messages.isDone())
|
|
{
|
|
messages.cancel(true);
|
|
}
|
|
}
|
|
sentMessages.clear();
|
|
messageChatChannel("**Message queue cleared**", true);
|
|
}
|
|
|
|
public String generateCode(int size)
|
|
{
|
|
return RandomStringUtils.randomNumeric(size);
|
|
}
|
|
|
|
@EventHandler(priority = EventPriority.MONITOR)
|
|
public void onPlayerDeath(PlayerDeathEvent event)
|
|
{
|
|
//Avoiding NPE Unboxing Warnings
|
|
Boolean b = event.getEntity().getWorld().getGameRuleValue(GameRule.SHOW_DEATH_MESSAGES);
|
|
if (b == null || !b)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Component deathMessage = event.deathMessage();
|
|
|
|
if (deathMessage != null)
|
|
{
|
|
messageChatChannel("**" + deformat(PlainTextComponentSerializer.plainText().serialize(deathMessage)) + "**", true);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onStart()
|
|
{
|
|
startBot();
|
|
}
|
|
|
|
@EventHandler(priority = EventPriority.MONITOR)
|
|
public void onPlayerJoin(PlayerJoinEvent event)
|
|
{
|
|
if (!plugin.al.isVanished(event.getPlayer().getUniqueId()))
|
|
{
|
|
messageChatChannel("**" + deformat(event.getPlayer().getName()) + " joined the server" + "**", true);
|
|
}
|
|
}
|
|
|
|
@EventHandler(priority = EventPriority.MONITOR)
|
|
public void onPlayerLeave(PlayerQuitEvent event)
|
|
{
|
|
if (!plugin.al.isVanished(event.getPlayer().getUniqueId()))
|
|
{
|
|
messageChatChannel("**" + deformat(event.getPlayer().getName()) + " left the server" + "**", true);
|
|
}
|
|
}
|
|
|
|
public static String sanitizeChatMessage(String message)
|
|
{
|
|
String newMessage = message;
|
|
|
|
if (message.contains("@"))
|
|
{
|
|
// \u200B is Zero Width Space, invisible on Discord
|
|
newMessage = message.replaceAll("@", "@\u200B");
|
|
}
|
|
|
|
if (message.toLowerCase().contains("discord.gg")) // discord.gg/invite works as an invite
|
|
{
|
|
return "";
|
|
}
|
|
|
|
for (String subdomain : DISCORD_SUBDOMAINS)
|
|
{
|
|
if (message.toLowerCase().contains(subdomain + "/invite"))
|
|
{
|
|
return "";
|
|
}
|
|
}
|
|
|
|
if (message.contains("§"))
|
|
{
|
|
newMessage = message.replaceAll("§", "");
|
|
}
|
|
|
|
return deformat(newMessage);
|
|
}
|
|
|
|
public void messageChatChannel(String message)
|
|
{
|
|
messageChatChannel(message, false);
|
|
}
|
|
|
|
public void messageChatChannel(String message, boolean system)
|
|
{
|
|
String chat_channel_id = ConfigEntry.DISCORD_CHAT_CHANNEL_ID.getString();
|
|
|
|
String sanitizedMessage = (system) ? message : sanitizeChatMessage(message);
|
|
|
|
if (sanitizedMessage.isBlank()) return;
|
|
|
|
if (enabled && !chat_channel_id.isEmpty())
|
|
{
|
|
CompletableFuture<Message> sentMessage = Objects.requireNonNull(bot.getTextChannelById(chat_channel_id)).sendMessage(sanitizedMessage).submit(true);
|
|
sentMessages.add(sentMessage);
|
|
}
|
|
}
|
|
|
|
public void messageAdminChatChannel(String message)
|
|
{
|
|
messageAdminChatChannel(message, false);
|
|
}
|
|
|
|
public void messageAdminChatChannel(String message, boolean system)
|
|
{
|
|
String chat_channel_id = ConfigEntry.DISCORD_ADMINCHAT_CHANNEL_ID.getString();
|
|
|
|
String sanitizedMessage = sanitizeChatMessage(message);
|
|
|
|
if (sanitizedMessage.isBlank()) return;
|
|
|
|
if (enabled && !chat_channel_id.isEmpty())
|
|
{
|
|
CompletableFuture<Message> sentMessage = Objects.requireNonNull(bot.getTextChannelById(chat_channel_id)).sendMessage(sanitizedMessage).submit(true);
|
|
sentMessages.add(sentMessage);
|
|
}
|
|
}
|
|
|
|
public String formatBotTag()
|
|
{
|
|
SelfUser user = bot.getSelfUser();
|
|
return user.getAsTag();
|
|
}
|
|
|
|
@Override
|
|
public void onStop()
|
|
{
|
|
if (bot != null)
|
|
{
|
|
messageChatChannel("**Server has stopped**", true);
|
|
}
|
|
|
|
FLog.info("Discord integration has successfully shutdown.");
|
|
}
|
|
|
|
public static String deformat(String input)
|
|
{
|
|
return input.replaceAll("([_\\\\`*>|])", "\\\\$1");
|
|
}
|
|
|
|
public boolean shouldISendReport()
|
|
{
|
|
if (ConfigEntry.DISCORD_REPORT_CHANNEL_ID.getString().isEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ConfigEntry.DISCORD_SERVER_ID.getString().isEmpty())
|
|
{
|
|
FLog.severe("No Discord server ID was specified in the config, but there is a report channel ID.");
|
|
return false;
|
|
}
|
|
|
|
Guild server = bot.getGuildById(ConfigEntry.DISCORD_SERVER_ID.getString());
|
|
if (server == null)
|
|
|
|
{
|
|
FLog.severe("The Discord server ID specified is invalid, or the bot is not on the server.");
|
|
return false;
|
|
}
|
|
|
|
TextChannel channel = server.getTextChannelById(ConfigEntry.DISCORD_REPORT_CHANNEL_ID.getString());
|
|
if (channel == null)
|
|
{
|
|
FLog.severe("The report channel ID specified in the config is invalid.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public boolean sendReportOffline(Player reporter, OfflinePlayer reported, String reason)
|
|
{
|
|
if (!shouldISendReport())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
final Guild server = bot.getGuildById(ConfigEntry.DISCORD_SERVER_ID.getString());
|
|
|
|
if (server == null)
|
|
{
|
|
FLog.severe("The guild ID specified in the config is invalid.");
|
|
return false;
|
|
}
|
|
|
|
final TextChannel channel = server.getTextChannelById(ConfigEntry.DISCORD_REPORT_CHANNEL_ID.getString());
|
|
|
|
if (channel == null)
|
|
{
|
|
FLog.severe("The report channel ID specified in the config is invalid.");
|
|
return false;
|
|
}
|
|
|
|
final EmbedBuilder embedBuilder = new EmbedBuilder();
|
|
embedBuilder.setTitle("Report for " + reported.getName() + " (offline)");
|
|
embedBuilder.setDescription(reason);
|
|
embedBuilder.setFooter("Reported by " + reporter.getName(), "https://minotar.net/helm/" + reporter.getName() + ".png");
|
|
embedBuilder.setTimestamp(Instant.from(ZonedDateTime.now()));
|
|
if (plugin.esb.isEnabled())
|
|
{
|
|
com.earth2me.essentials.User user = plugin.esb.getEssentialsUser(reported.getName());
|
|
String location = "World: " + Objects.requireNonNull(user.getLastLocation().getWorld()).getName() + ", X: " + user.getLastLocation().getBlockX() + ", Y: " + user.getLastLocation().getBlockY() + ", Z: " + user.getLastLocation().getBlockZ();
|
|
embedBuilder.addField("Location", location, true);
|
|
embedBuilder.addField("God Mode", WordUtils.capitalizeFully(String.valueOf(user.isGodModeEnabled())), true);
|
|
if (user.getNickname() != null)
|
|
{
|
|
embedBuilder.addField("Nickname", user.getNickname(), true);
|
|
}
|
|
}
|
|
MessageEmbed embed = embedBuilder.build();
|
|
Message message = channel.sendMessageEmbeds(embed).complete();
|
|
|
|
if (!ConfigEntry.DISCORD_REPORT_ARCHIVE_CHANNEL_ID.getString().isEmpty())
|
|
{
|
|
message.addReaction("\uD83D\uDCCB").complete();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public boolean sendReport(Player reporter, Player reported, String reason)
|
|
{
|
|
if (!shouldISendReport())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
final Guild server = bot.getGuildById(ConfigEntry.DISCORD_SERVER_ID.getString());
|
|
|
|
if (server == null)
|
|
{
|
|
FLog.severe("The guild ID specified in the config is invalid.");
|
|
return false;
|
|
}
|
|
|
|
final TextChannel channel = server.getTextChannelById(ConfigEntry.DISCORD_REPORT_CHANNEL_ID.getString());
|
|
|
|
if (channel == null)
|
|
{
|
|
FLog.severe("The report channel ID specified in the config is invalid.");
|
|
return false;
|
|
}
|
|
|
|
final EmbedBuilder embedBuilder = new EmbedBuilder();
|
|
embedBuilder.setTitle("Report for " + reported.getName());
|
|
embedBuilder.setDescription(reason);
|
|
embedBuilder.setFooter("Reported by " + reporter.getName(), "https://minotar.net/helm/" + reporter.getName() + ".png");
|
|
embedBuilder.setTimestamp(Instant.from(ZonedDateTime.now()));
|
|
String location = "World: " + Objects.requireNonNull(reported.getLocation().getWorld()).getName() + ", X: " + reported.getLocation().getBlockX() + ", Y: " + reported.getLocation().getBlockY() + ", Z: " + reported.getLocation().getBlockZ();
|
|
embedBuilder.addField("Location", location, true);
|
|
embedBuilder.addField("Game Mode", WordUtils.capitalizeFully(reported.getGameMode().name()), true);
|
|
|
|
if (plugin.esb.isEnabled())
|
|
{
|
|
com.earth2me.essentials.User user = plugin.esb.getEssentialsUser(reported.getName());
|
|
embedBuilder.addField("God Mode", WordUtils.capitalizeFully(String.valueOf(user.isGodModeEnabled())), true);
|
|
if (user.getNickname() != null)
|
|
{
|
|
embedBuilder.addField("Nickname", user.getNickname(), true);
|
|
}
|
|
}
|
|
|
|
MessageEmbed embed = embedBuilder.build();
|
|
Message message = channel.sendMessageEmbeds(embed).complete();
|
|
|
|
if (!ConfigEntry.DISCORD_REPORT_ARCHIVE_CHANNEL_ID.getString().isEmpty())
|
|
{
|
|
message.addReaction("\uD83D\uDCCB").complete();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Do no ask why this is here. I spent two hours trying to make a simple thing work
|
|
public class StartEvent
|
|
{
|
|
public void start()
|
|
{
|
|
messageChatChannel("**Server has started**", true);
|
|
}
|
|
}
|
|
|
|
@EventHandler(ignoreCancelled = true)
|
|
public void onAsyncPlayerChat(AsyncPlayerChatEvent event)
|
|
{
|
|
Player player = event.getPlayer();
|
|
String message = event.getMessage();
|
|
|
|
if (!ConfigEntry.ADMIN_ONLY_MODE.getBoolean() && !server.hasWhitelist()
|
|
&& !plugin.pl.getPlayer(player).isMuted() && bot != null)
|
|
{
|
|
messageChatChannel(player.getName() + " \u00BB " + ChatColor.stripColor(message));
|
|
}
|
|
}
|
|
} |