Part 1 / 2

Only thing left is to fix all the code issues from moving out the discord and shop implementations.
This commit is contained in:
Paul Reilly
2023-03-09 01:42:18 -06:00
parent f53696aa9e
commit 2265783afb
319 changed files with 1531 additions and 1440 deletions

41
discord/pom.xml Normal file
View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>me.totalfreedom</groupId>
<artifactId>TotalFreedomMod</artifactId>
<version>2023.02</version>
</parent>
<artifactId>discord</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.discord4j</groupId>
<artifactId>discord4j-core</artifactId>
<version>3.2.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>me.totalfreedom</groupId>
<artifactId>commons</artifactId>
<version>2023.02</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.5.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,138 @@
package me.totalfreedom.discord;
import discord4j.common.util.Snowflake;
import discord4j.core.DiscordClientBuilder;
import discord4j.core.GatewayDiscordClient;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.core.object.entity.Guild;
import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.channel.TextChannel;
import me.totalfreedom.discord.command.HelpCommand;
import me.totalfreedom.discord.command.ListCommand;
import me.totalfreedom.discord.command.TPSCommand;
import me.totalfreedom.discord.handling.CommandHandler;
import me.totalfreedom.discord.util.SnowflakeEntry;
import me.totalfreedom.discord.util.TFM_Bridge;
import me.totalfreedom.totalfreedommod.config.ConfigEntry;
import me.totalfreedom.totalfreedommod.player.PlayerData;
import me.totalfreedom.totalfreedommod.util.FLog;
import net.dv8tion.jda.internal.utils.concurrent.CountingThreadFactory;
import org.bukkit.Bukkit;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
public class Bot
{
private final GatewayDiscordClient client;
private final TFM_Bridge tfm;
private final HashMap<String, PlayerData> LINK_CODES = new HashMap<>();
private ScheduledThreadPoolExecutor RATELIMIT_EXECUTOR;
private Boolean enabled = false;
public Bot()
{
//Creates the gateway client and connects to the gateway
this.client = DiscordClientBuilder.create(ConfigEntry.DISCORD_TOKEN.getString())
.build()
.login()
.block();
if (client == null) throw new IllegalStateException();
final CommandHandler handler = new CommandHandler(client.getRestClient());
/* Call our code to handle creating/deleting/editing our global slash commands.
We have to hard code our list of command files since iterating over a list of files in a resource directory
is overly complicated for such a simple demo and requires handling for both IDE and .jar packaging.
Using SpringBoot we can avoid all of this and use their resource pattern matcher to do this for us.
*/
List<String> commands = List.of("greet.json", "ping.json");
try
{
handler.registerCommands(commands);
handler.registerCommand(new HelpCommand());
handler.registerCommand(new ListCommand());
handler.registerCommand(new TPSCommand());
} catch (Exception e)
{
Bukkit.getLogger().severe("Error trying to register global slash commands.\n" + e.getMessage());
}
//Register our slash command listener
client.on(ChatInputInteractionEvent.class, handler::handle)
.then(client.onDisconnect())
.block(); // We use .block() as there is not another non-daemon thread and the jvm would close otherwise.
this.tfm = new TFM_Bridge(this);
RATELIMIT_EXECUTOR = new ScheduledThreadPoolExecutor(5, new CountingThreadFactory(this::poolIdentifier, "RateLimit"));
RATELIMIT_EXECUTOR.setRemoveOnCancelPolicy(true);
}
private String poolIdentifier()
{
return "TFD4J";
}
public TFM_Bridge getTFM()
{
return tfm;
}
public Mono<Guild> getGuildById()
{
return client.getGuildById(SnowflakeEntry.serverID);
}
public GatewayDiscordClient getClient()
{
return client;
}
public Map<String, PlayerData> getLinkCodes()
{
return LINK_CODES;
}
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 = client.getGuildById(SnowflakeEntry.serverID).block();
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.getChannelById(SnowflakeEntry.reportChannelID)
.ofType(TextChannel.class)
.block();
if (channel == null)
{
FLog.severe("The report channel ID specified in the config is invalid.");
return false;
}
return true;
}
}

View File

@ -0,0 +1,54 @@
package me.totalfreedom.discord;
import me.totalfreedom.discord.listener.AdminChatListener;
import me.totalfreedom.discord.listener.BukkitNative;
import me.totalfreedom.discord.listener.MinecraftListener;
import me.totalfreedom.discord.util.Utilities;
import org.bukkit.plugin.java.JavaPlugin;
public class TFD4J extends JavaPlugin
{
private Bot bot;
private Utilities utils;
private MinecraftListener mc;
private AdminChatListener ac;
@Override
public void onEnable()
{
this.bot = new Bot();
this.utils = new Utilities(this);
new BukkitNative(this);
this.mc = new MinecraftListener(this);
this.ac = new AdminChatListener(this);
mc.minecraftChatBound();
ac.adminChatBound();
}
@Override
public void onDisable()
{
bot.getClient().onDisconnect().subscribe();
}
public Bot getBot()
{
return bot;
}
public Utilities getUtils()
{
return utils;
}
public MinecraftListener getMinecraftListener()
{
return mc;
}
public AdminChatListener getAdminChatListener()
{
return ac;
}
}

View File

@ -0,0 +1,18 @@
package me.totalfreedom.discord;
import org.bukkit.Bukkit;
public class TFM_Accessor
{
private final TFD4J accessor;
public TFM_Accessor()
{
this.accessor = (TFD4J) Bukkit.getPluginManager().getPlugin("TFD4J");
}
public TFD4J botAccessor()
{
return accessor;
}
}

View File

@ -0,0 +1,36 @@
package me.totalfreedom.discord.command;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.core.spec.EmbedCreateSpec;
import discord4j.rest.util.Color;
import me.totalfreedom.discord.handling.SlashCommand;
import reactor.core.publisher.Mono;
import java.time.Instant;
public class HelpCommand implements SlashCommand
{
@Override
public String getName()
{
return "help";
}
@Override
public Mono<Void> handle(ChatInputInteractionEvent event)
{
EmbedCreateSpec spec = EmbedCreateSpec.builder()
.color(Color.GREEN)
.title("Help Command")
.addField("Commands", "This is a list of all commands", false)
.addField("\u200B", "\u200B", false)
.addField("help", "Displays the help command. (This command.)", true)
.addField("list", "Displays a list of all online players.", true)
.addField("tps", "Displays the server's TPS.", true)
.timestamp(Instant.now())
.build();
return event.reply()
.withEmbeds(spec);
}
}

View File

@ -0,0 +1,57 @@
package me.totalfreedom.discord.command;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.core.spec.EmbedCreateSpec;
import discord4j.rest.util.Color;
import me.totalfreedom.discord.handling.SlashCommand;
import me.totalfreedom.totalfreedommod.config.ConfigEntry;
import org.bukkit.Bukkit;
import org.bukkit.entity.HumanEntity;
import reactor.core.publisher.Mono;
import java.util.Iterator;
import java.util.List;
public class ListCommand implements SlashCommand
{
@Override
public String getName()
{
return "list";
}
@Override
public Mono<Void> handle(ChatInputInteractionEvent event)
{
List<String> playerNames = Bukkit.getOnlinePlayers()
.stream()
.map(HumanEntity::getName)
.toList();
StringBuilder sb = new StringBuilder();
Iterator<String> iterator = playerNames.iterator();
while (iterator.hasNext()) {
sb.append(iterator.next());
if (iterator.hasNext()) {
sb.append(", \n");
}
}
String empty = "\u200B";
EmbedCreateSpec spec = EmbedCreateSpec.builder()
.title("Player List - " + ConfigEntry.SERVER_NAME.getString())
.color(Color.BISMARK)
.addField("Online Players", String.join(", ", playerNames), false)
.addField(empty, "Currently Online: " + playerNames.size(), false)
.addField(empty, empty, false)
.addField("Players: ", sb.toString(), true)
.build();
return event.reply()
.withEmbeds(spec)
.withEphemeral(true)
.withContent(sb.toString());
}
}

View File

@ -0,0 +1,39 @@
package me.totalfreedom.discord.command;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.core.spec.EmbedCreateSpec;
import discord4j.rest.util.Color;
import me.totalfreedom.discord.handling.SlashCommand;
import me.totalfreedom.totalfreedommod.util.FUtil;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Mono;
public class TPSCommand implements SlashCommand
{
@Override
public String getName()
{
return "tps";
}
@Override
public Mono<Void> handle(@NotNull ChatInputInteractionEvent event)
{
String tps = String.valueOf(FUtil.getMeanAverageDouble(Bukkit.getServer().getTPS()));
EmbedCreateSpec spec = EmbedCreateSpec.builder()
.title("Current Server Tick Information")
.addField("TPS", tps, false)
.addField("Uptime", FUtil.getUptime(), false)
.addField("Maximum Memory", Math.ceil(FUtil.getMaxMem()) + " MB", false)
.addField("Allocated Memory", Math.ceil(FUtil.getTotalMem()) + " MB", false)
.addField("Free Memory", Math.ceil(FUtil.getFreeMem()) + " MB", false)
.color(Color.BISMARK)
.build();
return event.reply()
.withEmbeds(spec)
.withEphemeral(true);
}
}

View File

@ -0,0 +1,113 @@
package me.totalfreedom.discord.handling;
import discord4j.common.JacksonResources;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.discordjson.json.ApplicationCommandRequest;
import discord4j.rest.RestClient;
import discord4j.rest.service.ApplicationService;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class CommandHandler
{
private final List<SlashCommand> commands = new ArrayList<>();
private final RestClient restClient;
public CommandHandler(RestClient restClient)
{
this.restClient = restClient;
}
public void registerCommands(List<String> fileNames) throws IOException
{
//Create an ObjectMapper that supports Discord4J classes
final JacksonResources d4jMapper = JacksonResources.create();
// Convenience variables for the sake of easier to read code below
final ApplicationService applicationService = restClient.getApplicationService();
final long applicationId = Objects.requireNonNull(restClient.getApplicationId().block());
//Get our commands json from resources as command data
List<ApplicationCommandRequest> commands = new ArrayList<>();
for (String json : getCommandsJson(fileNames))
{
ApplicationCommandRequest request = d4jMapper.getObjectMapper()
.readValue(json, ApplicationCommandRequest.class);
commands.add(request); //Add to our array list
}
/* Bulk overwrite commands. This is now idempotent, so it is safe to use this even when only 1 command
is changed/added/removed
*/
applicationService.bulkOverwriteGlobalApplicationCommand(applicationId, commands)
.doOnNext(cmd -> Bukkit.getLogger().info("Successfully registered Global Command "
+ cmd.name()))
.doOnError(e -> Bukkit.getLogger().severe("Failed to register global commands.\n"
+ e.getMessage()))
.subscribe();
}
private @NotNull List<String> getCommandsJson(List<String> fileNames) throws IOException
{
// Confirm that the commands folder exists
String commandsFolderName = "commands/";
URL url = this.getClass().getClassLoader().getResource(commandsFolderName);
Objects.requireNonNull(url, commandsFolderName + " could not be found");
//Get all the files inside this folder and return the contents of the files as a list of strings
List<String> list = new ArrayList<>();
for (String file : fileNames)
{
String resourceFileAsString = getResourceFileAsString(commandsFolderName + file);
list.add(Objects.requireNonNull(resourceFileAsString, "Command file not found: " + file));
}
return list;
}
private @Nullable String getResourceFileAsString(String fileName) throws IOException
{
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
try (InputStream resourceAsStream = classLoader.getResourceAsStream(fileName))
{
if (resourceAsStream == null) return null;
try (InputStreamReader inputStreamReader = new InputStreamReader(resourceAsStream);
BufferedReader reader = new BufferedReader(inputStreamReader))
{
return reader.lines().collect(Collectors.joining(System.lineSeparator()));
}
}
}
public void registerCommand(SlashCommand command)
{
commands.add(command);
}
public Mono<Void> handle(ChatInputInteractionEvent event)
{
// Convert our array list to a flux that we can iterate through
return Flux.fromIterable(commands)
//Filter out all commands that don't match the name of the command this event is for
.filter(command -> command.getName().equals(event.getCommandName()))
// Get the first (and only) item in the flux that matches our filter
.next()
//have our command class handle all the logic related to its specific command.
.flatMap(command -> command.handle(event));
}
}

View File

@ -0,0 +1,10 @@
package me.totalfreedom.discord.handling;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import reactor.core.publisher.Mono;
public interface SlashCommand {
String getName();
Mono<Void> handle(ChatInputInteractionEvent event);
}

View File

@ -0,0 +1,159 @@
package me.totalfreedom.discord.listener;
import com.google.common.base.Strings;
import discord4j.core.event.domain.message.MessageCreateEvent;
import discord4j.core.object.entity.Attachment;
import discord4j.core.object.entity.Guild;
import discord4j.core.object.entity.Member;
import discord4j.core.object.entity.Message;
import me.totalfreedom.discord.Bot;
import me.totalfreedom.discord.TFD4J;
import me.totalfreedom.discord.util.SnowflakeEntry;
import me.totalfreedom.totalfreedommod.TotalFreedomMod;
import me.totalfreedom.totalfreedommod.admin.Admin;
import me.totalfreedom.totalfreedommod.rank.Displayable;
import me.totalfreedom.totalfreedommod.rank.Rank;
import me.totalfreedom.totalfreedommod.rank.Title;
import me.totalfreedom.totalfreedommod.util.FLog;
import me.totalfreedom.totalfreedommod.util.FUtil;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
public class AdminChatListener
{
private final Bot bot;
private final TFD4J tfd4j;
public AdminChatListener(TFD4J tfd4j)
{
this.tfd4j = tfd4j;
this.bot = tfd4j.getBot();
}
public static net.md_5.bungee.api.ChatColor getColor(Displayable display)
{
return display.getColor();
}
public void adminChatBound()
{
tfd4j.getBot()
.getClient()
.getEventDispatcher()
.on(MessageCreateEvent.class)
.filter(m -> m.getMessage()
.getChannel()
.blockOptional()
.orElseThrow()
.getId()
.equals(SnowflakeEntry.adminChatChannelID))
.filter(m -> !m.getMessage()
.getAuthor()
.orElseThrow()
.getId()
.equals(tfd4j.getBot().getClient().getSelfId()))
.subscribe(this::createMessageSpec);
}
public void createMessageSpec(MessageCreateEvent m)
{
Member member = m.getMember().orElseThrow(IllegalAccessError::new);
String tag = tfd4j.getMinecraftListener().getDisplay(member);
Message msg = m.getMessage();
String mediamessage = ChatColor.YELLOW + "[Media]";
StringBuilder logmessage = new StringBuilder(ChatColor.DARK_GRAY + "[" + ChatColor.DARK_AQUA + "Discord" + ChatColor.DARK_GRAY + "] " + ChatColor.RESET);
String lm = ChatColor.DARK_RED + member.getDisplayName() + " "
+ ChatColor.DARK_GRAY + tag + ChatColor.DARK_GRAY
+ ChatColor.WHITE + ": " + ChatColor.GOLD + FUtil.colorize(msg.getContent());
logmessage.append(lm);
if (!msg.getAttachments().isEmpty())
{
logmessage.append(mediamessage); // Actually for logging...
}
FLog.info(logmessage.toString());
Bukkit.getOnlinePlayers().stream().filter(player -> TotalFreedomMod.getPlugin().al.isAdmin(player)).forEach(player ->
{
StringBuilder message = new StringBuilder(ChatColor.DARK_GRAY + "[" + ChatColor.DARK_AQUA + "Discord" + ChatColor.DARK_GRAY + "] " + ChatColor.RESET);
ComponentBuilder builder = new ComponentBuilder(message.toString());
Admin admin = TotalFreedomMod.getPlugin().al.getAdmin(player);
String format = admin.getAcFormat();
if (!Strings.isNullOrEmpty(format))
{
Displayable display = getDisplay(member);
net.md_5.bungee.api.ChatColor color = getColor(display);
String m1 = format.replace("%name%", member.getDisplayName())
.replace("%rank%", display.getAbbr())
.replace("%rankcolor%", color.toString())
.replace("%msg%", FUtil.colorize(msg.getContent()));
builder.append(FUtil.colorize(m1));
} else
{
String m1 = ChatColor.DARK_RED + member.getDisplayName() + " "
+ ChatColor.DARK_GRAY + tag + ChatColor.DARK_GRAY
+ ChatColor.WHITE + ": " + ChatColor.GOLD + FUtil.colorize(msg.getContent());
builder.append(m1);
}
if (!msg.getAttachments().isEmpty())
{
for (Attachment attachment : msg.getAttachments())
{
TextComponent text = new TextComponent(mediamessage);
text.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, attachment.getUrl()));
if (!msg.getContent().isEmpty())
{
builder.append(" ");
}
builder.append(text);
}
}
player.spigot().sendMessage(builder.create());
});
}
public Displayable getDisplay(Member member)
{
Guild server = tfd4j.getBot().getGuildById().block();
// Server Owner
assert server != null;
return member.getRoles().map(role ->
{
if (role.getId().equals(SnowflakeEntry.ownerRoleID))
{
return Title.OWNER;
} else if (role.getId().equals(SnowflakeEntry.developerRoleID))
{
return Title.DEVELOPER;
} else if (role.getId().equals(SnowflakeEntry.executiveRoleID))
{
return Title.EXECUTIVE;
} else if (role.getId().equals(SnowflakeEntry.assistantRoleID))
{
return Title.ASSTEXEC;
} else if (role.getId().equals(SnowflakeEntry.seniorRoleID))
{
return Rank.SENIOR_ADMIN;
} else if (role.getId().equals(SnowflakeEntry.adminRoleID))
{
return Rank.ADMIN;
} else if (role.getId().equals(SnowflakeEntry.builderRoleID))
{
return Title.MASTER_BUILDER;
} else
{
return null;
}
}).blockFirst();
}
}

View File

@ -0,0 +1,91 @@
package me.totalfreedom.discord.listener;
import me.totalfreedom.discord.Bot;
import me.totalfreedom.discord.TFD4J;
import me.totalfreedom.totalfreedommod.TotalFreedomMod;
import me.totalfreedom.totalfreedommod.config.ConfigEntry;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.GameRule;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
public class BukkitNative implements Listener
{
private final TotalFreedomMod commons;
private final Bot bot;
private final TFD4J tfd4j;
public BukkitNative(TFD4J tfd4j)
{
this.tfd4j = tfd4j;
this.bot = tfd4j.getBot();
this.commons = bot.getTFM().getCommons();
tfd4j.getServer().getPluginManager().registerEvents(this, tfd4j);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event)
{
if (!commons.al.isVanished(event.getPlayer().getUniqueId()))
{
tfd4j.getUtils().messageChatChannel("**"
+ tfd4j.getUtils().deformat(event.getPlayer().getName())
+ " joined the server" + "**", true);
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerLeave(PlayerQuitEvent event)
{
if (!commons.al.isVanished(event.getPlayer().getUniqueId()))
{
tfd4j.getUtils().messageChatChannel("**"
+ tfd4j.getUtils().deformat(event.getPlayer().getName())
+ " left the server" + "**", true);
}
}
@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)
{
tfd4j.getUtils().messageChatChannel("**"
+ tfd4j.getUtils().deformat(PlainTextComponentSerializer.plainText().serialize(deathMessage))
+ "**", true);
}
}
@EventHandler(ignoreCancelled = true)
public void onAsyncPlayerChat(AsyncPlayerChatEvent event)
{
Player player = event.getPlayer();
String message = event.getMessage();
if (!ConfigEntry.ADMIN_ONLY_MODE.getBoolean() && !tfd4j.getServer().hasWhitelist()
&& !commons.pl.getPlayer(player).isMuted() && bot != null)
{
tfd4j.getUtils().messageChatChannel(player.getName()
+ " \u00BB "
+ ChatColor.stripColor(message), true);
}
}
}

View File

@ -0,0 +1,164 @@
package me.totalfreedom.discord.listener;
import discord4j.core.event.domain.message.MessageCreateEvent;
import discord4j.core.object.entity.Attachment;
import discord4j.core.object.entity.Guild;
import discord4j.core.object.entity.Member;
import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.channel.TextChannel;
import me.totalfreedom.discord.Bot;
import me.totalfreedom.discord.TFD4J;
import me.totalfreedom.discord.util.SnowflakeEntry;
import me.totalfreedom.totalfreedommod.TotalFreedomMod;
import me.totalfreedom.totalfreedommod.config.ConfigEntry;
import me.totalfreedom.totalfreedommod.rank.Rank;
import me.totalfreedom.totalfreedommod.rank.Title;
import me.totalfreedom.totalfreedommod.util.FLog;
import me.totalfreedom.totalfreedommod.util.FUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
public class MinecraftListener
{
private final TotalFreedomMod commons;
private final Bot bot;
private final TFD4J tfd4j;
public MinecraftListener(TFD4J tfd4j)
{
this.tfd4j = tfd4j;
this.bot = tfd4j.getBot();
this.commons = bot.getTFM().getCommons();
}
public void minecraftChatBound()
{
tfd4j.getBot().getClient()
.getEventDispatcher()
.on(MessageCreateEvent.class)
.filter(m -> m.getMember().orElse(null) != null)
.filter(m -> !m.getMessage()
.getAuthor()
.orElseThrow(IllegalAccessError::new)
.getId()
.equals(tfd4j.getBot().getClient().getSelfId()))
.filter(m -> m.getMessage()
.getChannel()
.blockOptional()
.orElseThrow(IllegalAccessError::new)
.getId()
.equals(SnowflakeEntry.chatChannelID))
.filter(m ->
{
Boolean b = m.getMessage()
.getChannel()
.map(c -> c instanceof TextChannel)
.block();
return b != null && b;
}).subscribe(this::doMessageBodyDetails);
}
private void doMessageBodyDetails(MessageCreateEvent m)
{
Member member = m.getMember().orElseThrow();
Message msg = m.getMessage();
String tag = getDisplay(member);
TextComponent[] emsg = {Component.empty()}; // We are using a single value arrays here to silence SonarLint's "return value never used" bullshit.
emsg[0] = emsg[0].append(Component.text("[", NamedTextColor.DARK_GRAY));
TextComponent[] inviteLink = {Component.text("Discord")};
inviteLink[0] = inviteLink[0].color(NamedTextColor.DARK_AQUA);
HoverEvent<Component> hoverEvent = HoverEvent.showText(Component.text("Click to join our Discord server!"));
ClickEvent clickEvent = ClickEvent.openUrl(ConfigEntry.DISCORD_INVITE_LINK.getString());
inviteLink[0] = inviteLink[0].hoverEvent(hoverEvent);
inviteLink[0] = inviteLink[0].clickEvent(clickEvent);
emsg[0] = emsg[0].append(inviteLink[0]);
emsg[0] = emsg[0].append(Component.text("] ", NamedTextColor.DARK_GRAY));
// Tag (if they have one)
if (tag != null)
{
emsg[0] = emsg[0].append(Component.text(tag));
}
emsg[0] = emsg[0].append(Component.space());
// User
TextComponent[] user = {Component.text(FUtil.stripColors(member.getDisplayName()))};
user[0] = user[0].color(NamedTextColor.RED);
emsg[0] = emsg[0].append(user[0]);
// Message
emsg[0] = emsg[0].append(Component.text(": ", NamedTextColor.DARK_GRAY));
emsg[0] = emsg[0].append(Component.text(FUtil.stripColors(msg.getContent()), NamedTextColor.WHITE));
// Attachments
if (!msg.getAttachments().isEmpty())
{
if (!msg.getContent().isEmpty())
emsg[0] = emsg[0].append(Component.space());
for (Attachment attachment : msg.getAttachments())
{
TextComponent[] media = {Component.text("[Media] ")};
media[0] = media[0].color(NamedTextColor.YELLOW);
HoverEvent<Component> hover = HoverEvent.showText(Component.text(attachment.getUrl()));
ClickEvent click = ClickEvent.openUrl(attachment.getUrl());
media[0] = media[0].clickEvent(click);
media[0] = media[0].hoverEvent(hover);
emsg[0] = emsg[0].append(media[0]);
}
}
for (Player player : Bukkit.getOnlinePlayers())
{
if (TotalFreedomMod.getPlugin().pl.getData(player).doesDisplayDiscord())
{
player.sendMessage(emsg[0]);
}
}
FLog.info(emsg[0].content(), true);
}
public String getDisplay(Member member)
{
Guild server = tfd4j.getBot().getGuildById().block();
// Server Owner
assert server != null;
return member.getRoles().map(role ->
{
if (role.getId().equals(SnowflakeEntry.ownerRoleID))
{
return Title.OWNER.getColoredTag();
} else if (role.getId().equals(SnowflakeEntry.developerRoleID))
{
return Title.DEVELOPER.getColoredTag();
} else if (role.getId().equals(SnowflakeEntry.executiveRoleID))
{
return Title.EXECUTIVE.getColoredTag();
} else if (role.getId().equals(SnowflakeEntry.assistantRoleID))
{
return Title.ASSTEXEC.getColoredTag();
} else if (role.getId().equals(SnowflakeEntry.seniorRoleID))
{
return Rank.SENIOR_ADMIN.getColoredTag();
} else if (role.getId().equals(SnowflakeEntry.adminRoleID))
{
return Rank.ADMIN.getColoredTag();
} else if (role.getId().equals(SnowflakeEntry.builderRoleID))
{
return Title.MASTER_BUILDER.getColoredTag();
} else
{
return null;
}
}).blockFirst();
}
}

View File

@ -0,0 +1,57 @@
package me.totalfreedom.discord.listener;
import discord4j.core.event.domain.message.MessageCreateEvent;
import me.totalfreedom.discord.TFD4J;
import me.totalfreedom.discord.discord.Discord;
import me.totalfreedom.totalfreedommod.TotalFreedomMod;
import me.totalfreedom.totalfreedommod.admin.Admin;
import me.totalfreedom.totalfreedommod.player.PlayerData;
public class PrivateMessageListener
{
private final TFD4J tfd4j;
public PrivateMessageListener(TFD4J tfd4j)
{
this.tfd4j = tfd4j;
}
public void privateMessageReceived()
{
tfd4j.getBot()
.getClient()
.getEventDispatcher()
.on(MessageCreateEvent.class)
.filter(event -> event.getMessage().getAuthor().orElse(null) != null)
.filter(event -> !event.getMessage().getAuthor().orElseThrow().getId().equals(tfd4j.getBot().getClient().getSelfId()))
.filter(event -> event.getMessage().getContent().strip().matches("[0-9][0-9][0-9][0-9][0-9]"))
.subscribe(event ->
{
String code = event.getMessage().getContent().strip();
String name;
if (Discord.LINK_CODES.get(code) != null)
{
PlayerData player = Discord.LINK_CODES.get(code);
name = player.getName();
player.setDiscordID(event.getMessage().getAuthor().orElseThrow().getId().asString());
Admin admin = TotalFreedomMod.getPlugin().al.getEntryByUuid(player.getUuid());
if (admin != null)
{
Discord.syncRoles(admin, player.getDiscordID());
}
TotalFreedomMod.getPlugin().pl.save(player);
Discord.LINK_CODES.remove(code);
} else
{
return;
}
event.getMessage()
.getChannel()
.blockOptional()
.orElseThrow(UnsupportedOperationException::new)
.createMessage("Link successful. Now this Discord account is linked with your Minecraft account **" + name + "**.").block();
});
}
}

View File

@ -0,0 +1,77 @@
package me.totalfreedom.discord.listener;
import discord4j.core.event.domain.message.ReactionAddEvent;
import discord4j.core.object.Embed;
import discord4j.core.object.entity.Member;
import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.channel.Channel;
import discord4j.discordjson.json.MessageCreateRequest;
import me.totalfreedom.discord.TFD4J;
import me.totalfreedom.discord.util.SnowflakeEntry;
import me.totalfreedom.totalfreedommod.util.FLog;
public class ReactionListener
{
private final TFD4J tfd4j;
public ReactionListener(TFD4J tfd4J)
{
this.tfd4j = tfd4J;
}
public void onReactionAdd()
{
tfd4j.getBot()
.getClient()
.getEventDispatcher()
.on(ReactionAddEvent.class)
.filter(r -> r.getGuild().block() != null)
.filter(r -> r.getMember().orElse(null) != null)
.filter(r -> !r.getMember()
.orElseThrow()
.getId()
.equals(tfd4j.getBot().getClient().getSelfId()))
.filter(r -> !r.getChannel()
.blockOptional()
.orElseThrow().getId().equals(SnowflakeEntry.reportChannelID))
.filter(r -> r.getEmoji()
.asUnicodeEmoji()
.orElseThrow(UnsupportedOperationException::new)
.getRaw()
.equals("\uD83D\uDCCB"))
.subscribe(this::reactionWork);
}
public void reactionWork(ReactionAddEvent event)
{
final Channel archiveChannel = tfd4j.getBot().getClient().getChannelById(SnowflakeEntry.archiveChannelID).block();
if (archiveChannel == null)
{
FLog.warning("Report archive channel is defined in the config, yet doesn't actually exist!");
return;
}
final Message message = event.getMessage().blockOptional().orElseThrow();
final Member completer = event.getMember().orElseThrow();
if (!message.getAuthor().orElseThrow().getId().equals(tfd4j.getBot().getClient().getSelfId()))
{
return;
}
// We don't need other embeds... yet?
final Embed embed = message.getEmbeds().get(0);
final MessageCreateRequest request = MessageCreateRequest.builder()
.content("Report completed by " + completer.getUsername()
+ " (" + completer.getDiscriminator()
+ ")")
.addEmbed(embed.getData())
.build();
archiveChannel.getRestChannel().createMessage(request);
message.delete().block();
}
}

View File

@ -0,0 +1,20 @@
package me.totalfreedom.discord.util;
import discord4j.common.util.Snowflake;
import me.totalfreedom.totalfreedommod.config.ConfigEntry;
public class SnowflakeEntry
{
public static final Snowflake adminChatChannelID = Snowflake.of(ConfigEntry.DISCORD_ADMINCHAT_CHANNEL_ID.getString());
public static final Snowflake chatChannelID = Snowflake.of(ConfigEntry.DISCORD_CHAT_CHANNEL_ID.getString());
public static final Snowflake serverID = Snowflake.of(ConfigEntry.DISCORD_SERVER_ID.getString());
public static final Snowflake ownerRoleID = Snowflake.of(ConfigEntry.DISCORD_SERVER_OWNER_ROLE_ID.getString());
public static final Snowflake developerRoleID = Snowflake.of(ConfigEntry.DISCORD_DEVELOPER_ROLE_ID.getString());
public static final Snowflake executiveRoleID = Snowflake.of(ConfigEntry.DISCORD_EXECUTIVE_ROLE_ID.getString());
public static final Snowflake assistantRoleID = Snowflake.of(ConfigEntry.DISCORD_ASSISTANT_EXECUTIVE_ROLE_ID.getString());
public static final Snowflake seniorRoleID = Snowflake.of(ConfigEntry.DISCORD_SENIOR_ADMIN_ROLE_ID.getString());
public static final Snowflake adminRoleID = Snowflake.of(ConfigEntry.DISCORD_NEW_ADMIN_ROLE_ID.getString());
public static final Snowflake builderRoleID = Snowflake.of(ConfigEntry.DISCORD_MASTER_BUILDER_ROLE_ID.getString());
public static final Snowflake reportChannelID = Snowflake.of(ConfigEntry.DISCORD_REPORT_CHANNEL_ID.getString());
public static final Snowflake archiveChannelID = Snowflake.of(ConfigEntry.DISCORD_REPORT_ARCHIVE_CHANNEL_ID.getString());
}

View File

@ -0,0 +1,145 @@
package me.totalfreedom.discord.util;
import discord4j.common.util.Snowflake;
import discord4j.core.object.entity.Guild;
import discord4j.core.object.entity.Member;
import discord4j.core.object.entity.Role;
import me.totalfreedom.discord.Bot;
import me.totalfreedom.totalfreedommod.TotalFreedomMod;
import me.totalfreedom.totalfreedommod.admin.Admin;
import me.totalfreedom.totalfreedommod.config.ConfigEntry;
import me.totalfreedom.totalfreedommod.player.PlayerData;
import me.totalfreedom.totalfreedommod.rank.Rank;
import me.totalfreedom.totalfreedommod.rank.Title;
import me.totalfreedom.totalfreedommod.util.FLog;
import org.bukkit.Bukkit;
public class TFM_Bridge
{
private final Bot bot;
private final TotalFreedomMod commons;
public TFM_Bridge(Bot bot)
{
this.bot = bot;
this.commons = (TotalFreedomMod) Bukkit.getPluginManager().getPlugin("TotalFreedomMod");
if (commons == null) throw new IllegalStateException();
}
public String getDisplay(Member member)
{
Guild server = bot.getGuildById().block();
// Server Owner
if (server == null)
{
throw new IllegalStateException();
}
Snowflake ownerID = Snowflake.of(ConfigEntry.DISCORD_SERVER_OWNER_ROLE_ID.getString());
Snowflake developerID = Snowflake.of(ConfigEntry.DISCORD_DEVELOPER_ROLE_ID.getString());
Snowflake executiveID = Snowflake.of(ConfigEntry.DISCORD_EXECUTIVE_ROLE_ID.getString());
Snowflake assistantExecutiveID = Snowflake.of(ConfigEntry.DISCORD_ASSISTANT_EXECUTIVE_ROLE_ID.getString());
Snowflake seniorAdminID = Snowflake.of(ConfigEntry.DISCORD_SENIOR_ADMIN_ROLE_ID.getString());
Snowflake adminID = Snowflake.of(ConfigEntry.DISCORD_NEW_ADMIN_ROLE_ID.getString());
Snowflake masterBuilderID = Snowflake.of(ConfigEntry.DISCORD_MASTER_BUILDER_ROLE_ID.getString());
Snowflake[] ids = {ownerID, developerID, executiveID, assistantExecutiveID, seniorAdminID, adminID, masterBuilderID};
String[] titles = {Title.OWNER.getColoredTag(), Title.DEVELOPER.getColoredTag(), Title.EXECUTIVE.getColoredTag(), Title.ASSTEXEC.getColoredTag(), Rank.SENIOR_ADMIN.getColoredTag(), Rank.ADMIN.getColoredTag(), Title.MASTER_BUILDER.getColoredTag()};
return member.getRoles().map(role ->
{
for (int i = 0; i < ids.length; i++)
{
if (role.getId().equals(ids[i]))
{
return titles[i];
}
}
return null;
}).blockFirst();
}
public String getCode(PlayerData playerData)
{
for (String code : bot.getLinkCodes().keySet())
{
if (bot.getLinkCodes().get(code).equals(playerData))
{
return code;
}
}
return null;
}
public boolean syncRoles(Admin admin, String discordID)
{
if (discordID == null)
{
return false;
}
Guild server = bot.getGuildById().block();
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(Snowflake.of(discordID)).block();
if (member == null)
{
return false;
}
Role adminRole = server.getRoleById(SnowflakeEntry.adminRoleID).block();
if (adminRole == null)
{
FLog.severe("The specified Admin role does not exist!");
return false;
}
Role senioradminRole = server.getRoleById(SnowflakeEntry.seniorRoleID).block();
if (senioradminRole == null)
{
FLog.severe("The specified Senior Admin role does not exist!");
return false;
}
if (!admin.isActive())
{
member.getRoles()
.filter(role -> role.equals(adminRole) || role.equals(senioradminRole))
.subscribe(r -> member.removeRole(r.getId()).block());
return true;
}
if (admin.getRank().equals(Rank.ADMIN))
{
member.getRoles()
.filter(role -> !role.equals(adminRole))
.subscribe(r -> member.addRole(r.getId()).block());
member.getRoles()
.filter(role -> role.equals(senioradminRole))
.subscribe(r -> member.removeRole(r.getId()).block());
return true;
}
else if (admin.getRank().equals(Rank.SENIOR_ADMIN))
{
member.getRoles()
.filter(role -> !role.equals(senioradminRole))
.subscribe(r -> member.addRole(r.getId()).block());
member.getRoles()
.filter(role -> role.equals(adminRole))
.subscribe(r -> member.removeRole(r.getId()).block());
return true;
}
return false;
}
public TotalFreedomMod getCommons() {
return commons;
}
}

View File

@ -0,0 +1,182 @@
package me.totalfreedom.discord.util;
import com.google.common.collect.ImmutableList;
import discord4j.core.object.entity.Guild;
import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.channel.TextChannel;
import discord4j.core.object.reaction.ReactionEmoji;
import discord4j.core.spec.EmbedCreateSpec;
import discord4j.core.spec.MessageCreateSpec;
import me.totalfreedom.discord.TFD4J;
import me.totalfreedom.totalfreedommod.config.ConfigEntry;
import me.totalfreedom.totalfreedommod.util.FLog;
import org.apache.commons.lang.WordUtils;
import org.bukkit.entity.Player;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class Utilities
{
private final Flux<Message> sentMessages = Flux.fromIterable(new ArrayList<>());
private final TFD4J tfd4J;
private final ImmutableList<String> DISCORD_SUBDOMAINS = (ImmutableList<String>)
List.of("discordapp.com",
"discord.com",
"discord.gg");
public Utilities(TFD4J tfd4J)
{
this.tfd4J = tfd4J;
}
public String sanitizeChatMessage(String message)
{
String newMessage = message;
if (message.contains("@"))
{
// \u200B is Zero Width Space, invisible on Discord
newMessage = message.replace("@", "@\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.replace("§", "");
}
return deformat(newMessage);
}
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 (!chat_channel_id.isEmpty())
{
MessageCreateSpec spec = MessageCreateSpec.builder()
.content(sanitizedMessage)
.build();
Mono<Message> sentMessage = tfd4J
.getBot()
.getClient()
.getChannelById(SnowflakeEntry.chatChannelID)
.ofType(TextChannel.class)
.flatMap(c -> c.createMessage(spec));
insert(sentMessage);
}
}
public void messageAdminChatChannel(String message)
{
String chat_channel_id = ConfigEntry.DISCORD_ADMINCHAT_CHANNEL_ID.getString();
String sanitizedMessage = sanitizeChatMessage(message);
if (sanitizedMessage.isBlank()) return;
if (!chat_channel_id.isEmpty())
{
MessageCreateSpec spec = MessageCreateSpec.builder()
.content(sanitizedMessage)
.build();
Mono<Message> sentMessage = tfd4J
.getBot()
.getClient()
.getChannelById(SnowflakeEntry.adminChatChannelID)
.ofType(TextChannel.class)
.flatMap(c -> c.createMessage(spec));
insert(sentMessage);
}
}
public boolean sendReport(Player reporter, Player reported, String reason)
{
if (!tfd4J.getBot().shouldISendReport())
{
return false;
}
final Guild server = tfd4J.getBot()
.getClient()
.getGuildById(SnowflakeEntry.serverID)
.block();
if (server == null)
{
FLog.severe("The guild ID specified in the config is invalid.");
return false;
}
final TextChannel channel = server.getChannelById(SnowflakeEntry.reportChannelID)
.ofType(TextChannel.class)
.block();
if (channel == null)
{
FLog.severe("The report channel ID specified in the config is invalid.");
return false;
}
String location = "World: " + Objects.requireNonNull(reported.getLocation().getWorld()).getName() + ", X: " + reported.getLocation().getBlockX() + ", Y: " + reported.getLocation().getBlockY() + ", Z: " + reported.getLocation().getBlockZ();
final EmbedCreateSpec spec = EmbedCreateSpec.builder()
.title("Report for " + reported.getName())
.description(reason)
.footer("Reported by " + reporter.getName(), "https://minotar.net/helm/" + reporter.getName() + ".png")
.timestamp(Instant.from(ZonedDateTime.now()))
.addField("Location", location, true)
.addField("Game Mode", WordUtils.capitalizeFully(reported.getGameMode().name()), true)
.build();
Message message = channel.createMessage(spec).block();
if (!ConfigEntry.DISCORD_REPORT_ARCHIVE_CHANNEL_ID.getString().isEmpty() && message != null)
{
ReactionEmoji emoji = ReactionEmoji.unicode("\uD83D\uDCCB");
message.addReaction(emoji);
}
return true;
}
public String deformat(String input)
{
return input.replaceAll("([_\\\\`*>|])", "\\\\$1");
}
public Flux<Message> getMessagesSent()
{
return sentMessages;
}
public void insert(Mono<Message> messageMono)
{
sentMessages.concatWith(messageMono);
}
}

View File

@ -0,0 +1,4 @@
{
"name": "help",
"description": "Bot help command."
}

View File

@ -0,0 +1,4 @@
{
"name": "list",
"description": "List all currently online players"
}

View File

@ -0,0 +1,4 @@
{
"name": "tps",
"description": "Gets the current TPS for the server."
}

View File

@ -0,0 +1,8 @@
name: TFD4J
main: me.totalfreedom.discord.TFD4J
version: 1.0
api-version: 1.19
depend: [TotalFreedomMod]
libraries:
- com.discord4j:discord4j-core:3.2.0
- io.projectreactor:reactor-core:3.4.9