// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // Magicbane Emulator Project © 2013 - 2022 // www.magicbane.com package discord; import discord.handlers.*; import engine.gameManager.ConfigManager; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.utils.MemberCachePolicy; import net.dv8tion.jda.api.utils.cache.CacheFlag; import org.pmw.tinylog.Configurator; import org.pmw.tinylog.Level; import org.pmw.tinylog.Logger; import org.pmw.tinylog.labelers.TimestampLabeler; import org.pmw.tinylog.policies.StartupPolicy; import org.pmw.tinylog.writers.RollingFileWriter; import javax.security.auth.login.LoginException; import java.io.*; import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import static discord.ChatChannel.ADMINLOG; /* * MagicBot is many things to Magicbane... * * -Project Mascot * -Customer service and administration bot * -Benevolent dictator * -Investment manager. * * MagicBot will never beg you for money. He is a very * responsible robot. He was varnished but never garnished. * MagicBot does not for to overclock himself. His chips * will therefore never overcook. * MagicBot will never be a pitiful robot trying for to use * you as emotional support human. * * MagicBot is just not that sort of robot and Magicbane * just isn't that sort of project. * * MagicBot runs a Shaodowbane emulator not a Second Life emulator. * */ public class MagicBot extends ListenerAdapter { public static final Pattern accountNameRegex = Pattern.compile("^[\\p{Alnum}]{6,20}$"); public static final Pattern passwordRegex = Pattern.compile("^[\\p{Alnum}]{6,20}$"); public static JDA jda; public static Database database; public static long discordServerID; public static long discordRoleID; public static Guild magicbaneDiscord; public static Role memberRole; public static TextChannel septicChannel; public static void main(String[] args) throws LoginException, InterruptedException { // Configure tinylogger Configurator.defaultConfig() .addWriter(new RollingFileWriter("logs/discord/magicbot.txt", 30, new TimestampLabeler(), new StartupPolicy())) .level(Level.DEBUG) .formatPattern("{level} {date:yyyy-MM-dd HH:mm:ss.SSS} [{thread}] {class}.{method}({line}) : {message}") .activate(); // Configuration Manager to the front desk if (ConfigManager.init() == false) { Logger.error("ABORT! Missing config entry!"); return; } // Configure Discord essential identifiers discordServerID = Long.parseLong(ConfigManager.MB_MAGICBOT_SERVERID.getValue()); discordRoleID = Long.parseLong(ConfigManager.MB_MAGICBOT_ROLEID.getValue()); // Configure and instance the database interface database = new Database(); database.configureDatabase(); // Authentication token issued to MagicBot application to connect // to Discord is stored in config. Bot is pre-invited to the Magicbane server. // Configure and create JDA discord instance JDABuilder jdaBuilder = JDABuilder.create(GatewayIntent.GUILD_MEMBERS, GatewayIntent.DIRECT_MESSAGES) .setToken(ConfigManager.MB_MAGICBOT_BOTTOKEN.getValue()) .addEventListeners(new MagicBot()) .disableCache(EnumSet.of(CacheFlag.VOICE_STATE, CacheFlag.EMOTE, CacheFlag.ACTIVITY, CacheFlag.CLIENT_STATUS)) .setMemberCachePolicy(MemberCachePolicy.ALL); jda = jdaBuilder.build(); jda.awaitReady(); // Cache guild and role values for later usage in #register magicbaneDiscord = jda.getGuildById(discordServerID); memberRole = magicbaneDiscord.getRoleById(discordRoleID); // Initialize chat channel support ChatChannel.Init(); // Background thread to send Admin Events Runnable adminLogRunnable = () -> SendAdminLogUpdates(); ScheduledExecutorService exec = Executors.newScheduledThreadPool(1); exec.scheduleAtFixedRate(adminLogRunnable, 0, 1, TimeUnit.MINUTES); Logger.info("***MAGICBOT IS RUNNING***"); } public static void sendResponse(MessageReceivedEvent event, String responseContent) { // Send a formatted MagicBot response to a Discord user String discordUserName; MessageChannel channel; // Exit if discord is offline if (jda.getStatus().equals(JDA.Status.CONNECTED) == false) return; discordUserName = event.getAuthor().getName(); channel = event.getMessage().getChannel(); channel.sendMessage( "```\n" + "Hello Player " + discordUserName + "\n\n" + responseContent + "\n\n" + RobotSpeak.getRobotSpeak() + "\n```").queue(); } public static boolean isAdminEvent(MessageReceivedEvent event) { String discordAccountID = event.getAuthor().getId(); List discordAccounts; DiscordAccount discordAccount; // Note that database errors will cause this to return false. // After the database is offline Avail status must be set // to true before any subsequent admin commands will function. if (Database.online == false) return false; discordAccounts = database.getDiscordAccounts(discordAccountID); if (discordAccounts.isEmpty()) return false; discordAccount = discordAccounts.get(0); return (discordAccount.isDiscordAdmin == 1); } public static String generatePassword(int length) { String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; StringBuilder passwordBuilder = new StringBuilder(length); Random random = new Random(); // Generate alphanumeric password of a given length. // Could not find a good method of generating a password // based upon a given regex. for (int i = 0; i < length; i++) passwordBuilder.append(ALPHABET.charAt(random.nextInt(ALPHABET.length()))); return new String(passwordBuilder); } public static String readLogFile(String filePath, int lineCount) { ProcessBuilder builder = new ProcessBuilder("/bin/bash", "-c", "tail -n " + lineCount + " " + filePath); builder.redirectErrorStream(true); Process process = null; String line = null; String logOutput = ""; try { process = builder.start(); InputStream is = process.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); while ((line = reader.readLine()) != null) { logOutput += line + "\n"; } } catch (IOException e) { Logger.error(e.toString()); return "Error while reading logfile"; } return logOutput; } private static void junkbot(String command, String[] inString) { String outString; Writer fileWriter; if (inString == null) return; ; outString = command + String.join(" ", inString); outString += "\n"; try { fileWriter = new BufferedWriter(new FileWriter("junkbot.txt", true)); fileWriter.append(outString); fileWriter.close(); } catch (IOException e) { e.printStackTrace(); } } private static void SendAdminLogUpdates() { HashMap adminEvents = database.getAdminEvents(); for (int adminEvent : adminEvents.keySet()) { // Set event as read database.setAdminEventAsRead(adminEvent); String outString = "```\n" + "Hello Players \n\n" + adminEvents.get(adminEvent) + "\n\n" + RobotSpeak.getRobotSpeak() + "\n```"; if (ADMINLOG.textChannel.canTalk()) ADMINLOG.textChannel.sendMessage(outString).queue(); } } @Override public void onMessageReceived(MessageReceivedEvent event) { // Exit if discord is offline if (jda.getStatus().equals(JDA.Status.CONNECTED) == false) return; // Early exit if message sent to us by another bot or ourselves. if (event.getAuthor().isBot()) return; // Extract message and origin channel from event Message message = event.getMessage(); // Only private messages MessageChannel channel = event.getMessage().getChannel(); if (channel.getType().equals(ChannelType.PRIVATE) == false) return; // Only real users if (event.getAuthor().isBot()) return; // Only users who have actually joined Magicbane discord. if (magicbaneDiscord.isMember(event.getAuthor()) == false) return; // getContentRaw() is an atomic getter // getContentDisplay() is a lazy getter which modifies the content // for e.g. console view or logging (strip discord formatting) String content = message.getContentRaw(); String[] args = content.split(" "); String command = args[0].toLowerCase(); if (args.length > 1) args = Arrays.copyOfRange(args, 1, args.length); else args = new String[0]; switch (command) { case "#register": RegisterAccountHandler.handleRequest(event, args); break; case "#help": handleHelpRequest(event); break; case "#account": AccountInfoRequest.handleRequest(event); break; case "#password": PasswordChangeHandler.handleRequest(event, args); break; case "#changelog": ChatChannelHandler.handleRequest(ChatChannel.CHANGELOG, event, args); break; case "#general": ChatChannelHandler.handleRequest(ChatChannel.GENERAL, event, args); break; case "#politics": ChatChannelHandler.handleRequest(ChatChannel.POLITICAL, event, args); break; case "#announce": ChatChannelHandler.handleRequest(ChatChannel.ANNOUNCE, event, args); break; case "#bug": ChatChannelHandler.handleRequest(ChatChannel.FORTOFIX, event, args); break; case "#recruit": ChatChannelHandler.handleRequest(ChatChannel.RECRUIT, event, args); break; case "#magicbox": ChatChannelHandler.handleRequest(ChatChannel.MAGICBOX, event, args); break; case "#lookup": LookupRequestHandler.handleRequest(event, args); break; case "#rules": RulesRequestHandler.handleRequest(event); break; case "#status": StatusRequestHandler.handleRequest(event); break; case "#setavail": SetAvailHandler.handleRequest(event, args); break; case "#ban": BanToggleHandler.handleRequest(event, args); break; case "#server": ServerRequestHandler.handleRequest(event, args); break; case "#dev": DevRequestHandler.handleRequest(event, args); break; case "#logs": LogsRequestHandler.handleRequest(event, args); break; case "#flash": FlashHandler.handleRequest(event, args); break; case "#trash": TrashRequestHandler.handleRequest(event, args); break; default: junkbot(command, args); break; } } public void handleHelpRequest(MessageReceivedEvent event) { // Help is kept here in the main class instead of a handler as a // design decision for ease of maintenance. String helpString = "I wish for to do the following things for you, not to you!\n\n" + "#register Register account for to play Magicbane.\n" + "#password Change your current game password.\n" + "#account List your account detailings.\n" + "#rules List of MagicBane server rules.\n" + "#status Display MagicBane server status.\n" + "#help List of MagicBot featurings.\n\n" + "http://magicbane.com/tinyinstaller.zip"; if (isAdminEvent(event)) helpString += "\n" + "#lookup Return accounts starting with string.\n" + "#bug -r Post to the bug channel/\n" + "#announce -r Post to the announcement channel/\n" + "#changelog Post to the Changelog channel/\n" + "#general -r Post to the general channel/\n" + "#politics -r Post to the politics channel/\n" + "#recruit -r Post to the politics channel/\n" + "#ban ###### Toggle active status of account.\n" + "#setavail true/false Toggle status of database access.\n" + "#server reboot/shutdown are your options.\n" + "#logs magicbot/world/login n (tail)\n" + "#flash Send flash message\n" + "#dev help (list dev subcommands)\n" + "#trash /detail/flush"; sendResponse(event, helpString); } }