diff --git a/.gitignore b/.gitignore index 524f096..5ff6309 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,38 @@ -# Compiled class file -*.class +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ -# Log file -*.log +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr -# BlueJ files -*.ctxt +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache -# Mobile Tools for Java (J2ME) -.mtj.tmp/ +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar +### VS Code ### +.vscode/ -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..359bb53 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..82dbec8 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/MeowLogin_Run_.xml b/.idea/runConfigurations/MeowLogin_Run_.xml new file mode 100644 index 0000000..3b66f4c --- /dev/null +++ b/.idea/runConfigurations/MeowLogin_Run_.xml @@ -0,0 +1,67 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..08c011c --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5ef616a --- /dev/null +++ b/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + org.meowdream + MeowLogin + 1.0.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + UTF-8 + + + + + + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + + + + + org.spigotmc + spigot-api + 1.18.2-R0.1-SNAPSHOT + provided + + + \ No newline at end of file diff --git a/src/main/java/cn/meowdream/meowlogin/Main.java b/src/main/java/cn/meowdream/meowlogin/Main.java new file mode 100644 index 0000000..0880cfc --- /dev/null +++ b/src/main/java/cn/meowdream/meowlogin/Main.java @@ -0,0 +1,62 @@ +package cn.meowdream.meowlogin; + +import cn.meowdream.meowlogin.listeners.Captcha; +import cn.meowdream.meowlogin.listeners.Auth; +import cn.meowdream.meowlogin.listeners.PluginCommand; +import cn.meowdream.meowlogin.utils.Metrics; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.logging.Logger; + +public class Main extends JavaPlugin { + + private Logger logger; + private PluginDescriptionFile descriptionFile; + private static Main instance; + + @Override + public void onEnable() { + logger = getLogger(); + instance = this; + registerConfig(); + descriptionFile = getDescription(); + + Metrics metrics = new Metrics(this,21668); + + getServer().getPluginManager().registerEvents(new Auth(),this); + getServer().getPluginManager().registerEvents(new Captcha(),this); + + getCommand("mlreload").setExecutor(new PluginCommand()); + logger.info(""" + + + ===================================================================== + + __ ___ __ _ \s + / |/ / ___ ___ _ __ / / ___ ___ _ (_) ___\s + / /|_/ / / -_)/ _ \\| |/|/ / / /__/ _ \\ / _ `/ / / / _ \\ + /_/ /_/ \\__/ \\___/|__,__/ /____/\\___/ \\_, / /_/ /_//_/ + /___/ \s + + Author: Mewn_Lynsi + + ===================================================================== + """); + logger.info("Enabled " + descriptionFile.getName() + " " + descriptionFile.getVersion() + "..."); + } + + @Override + public void onDisable() { + logger.info("Disabled " + descriptionFile.getName() + "..."); + logger = null; + } + + private void registerConfig() { + saveDefaultConfig(); + } + + public static Main getInstance(){ + return instance; + } +} diff --git a/src/main/java/cn/meowdream/meowlogin/listeners/Auth.java b/src/main/java/cn/meowdream/meowlogin/listeners/Auth.java new file mode 100644 index 0000000..eba0cd8 --- /dev/null +++ b/src/main/java/cn/meowdream/meowlogin/listeners/Auth.java @@ -0,0 +1,125 @@ +package cn.meowdream.meowlogin.listeners; + +import cn.meowdream.meowlogin.Main; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.logging.Level; + +import static org.bukkit.Bukkit.getLogger; + +public class Auth implements Listener { + + private String apiUrl; + + public static String getCurrentDateTime() { + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return now.format(formatter); + } + + public static String encodeURI(String input) { + return URLEncoder.encode(input, StandardCharsets.UTF_8); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + String playerName = player.getName(), + playerIP = player.getAddress().getAddress().toString(); + + FileConfiguration config = Main.getInstance().getConfig(); + + try { + apiUrl = config.getString("api-server"); + String encodedName = URLEncoder.encode(playerName, StandardCharsets.UTF_8); + URL url = new URL(apiUrl + "/login/checkStatus?username=" + encodedName + "&&IP=" + playerIP + "&&app=" + encodeURI(config.getString("server-name")) + "&&desc=" + encodeURI(config.getString("server-desc"))); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + + while ((line = reader.readLine()) != null) { + response.append(line); + } + reader.close(); + + int apiResponse = Integer.parseInt(response.toString()); + if (apiResponse == 0) { + event.setJoinMessage("§a" + playerName + "§f 从沉睡中苏醒了"); + } else if (apiResponse == -1) { + player.kickPlayer(""" + 无法连接到服务器 + + §c原因:您尚未登录 + + §8================================== + + §7请前往本服务器QQ频道,使用频道机器人进行登录。 + 详细信息请私信§f“汐汐酱”§7发送“/帮助”指令获取或查阅本服务器Wiki + 完成以上操作后,请重新连接服务器 + + 频道号:""" + config.getString("channel-id") + """ + + §7更多帮助请参考:§f""" + config.getString("reference-page") + """ + §7 + + + """ + getCurrentDateTime()); + } else { + player.kickPlayer(""" + 无法连接到服务器 + + §c原因:无效的登录响应 + + §8================================== + + §7您的配置一切正常! + 该故障可能由服务器配置错误引起,请联系管理员解决 + + 频道号:""" + config.getString("channel-id") + """ + + §7更多帮助请参考:§f""" + config.getString("reference-page") + """ + §7 + + + """ + getCurrentDateTime()); + getLogger().info("Unexpected response from server: " + apiResponse); + } + } catch (IOException e) { + player.kickPlayer(""" + 无法连接到服务器 + + §c原因:验证服务器离线 + + §8================================== + + §7我们暂时无法连接到身份验证服务,请稍候再试 + 若长时间无法进入服务器,请联系管理员解决 + + 频道号:""" + config.getString("channel-id") + """ + + §7更多帮助请参考:§f""" + config.getString("reference-page") + """ + §7 + + + """ + getCurrentDateTime()); + getLogger().log(Level.SEVERE, "Error while sending player join event to API.", e); + } + } +} diff --git a/src/main/java/cn/meowdream/meowlogin/listeners/Captcha.java b/src/main/java/cn/meowdream/meowlogin/listeners/Captcha.java new file mode 100644 index 0000000..c60a4a7 --- /dev/null +++ b/src/main/java/cn/meowdream/meowlogin/listeners/Captcha.java @@ -0,0 +1,583 @@ +package cn.meowdream.meowlogin.listeners; + +import cn.meowdream.meowlogin.Main; +import cn.meowdream.meowlogin.utils.FancyCaptchaGenerator; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryOpenEvent; +import org.bukkit.event.player.*; +import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.event.server.PluginEnableEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.MapMeta; +import org.bukkit.map.*; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.Vector; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Objects; +import java.util.UUID; + +import static cn.meowdream.meowlogin.listeners.Auth.getCurrentDateTime; +import static org.bukkit.Bukkit.getLogger; + +public class Captcha implements Listener { + + private final HashMap playerItems = new HashMap<>(); + private final HashMap playerMaps = new HashMap<>(); + private final HashMap playerCaptcha = new HashMap<>(); + private final HashMap playerVerification = new HashMap<>(); + // 用于存储玩家的BossBar + private final HashMap countdownTasks = new HashMap<>(); // 用于存储倒计时任务 + private final HashMap playerBossBars = new HashMap<>(); + private final File dataFile = new File("plugins/MeowLogin/data.yml"); + private final FileConfiguration dataConfig = YamlConfiguration.loadConfiguration(dataFile); + FileConfiguration config = Main.getInstance().getConfig(); + + private final int COUNTDOWN_DURATION = config.getInt("captcha-countdown-duration"); + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + + if(!config.getBoolean("enable-captcha")){ + playerVerification.put(player.getUniqueId(), true); + player.sendMessage("§a亲爱的" + player.getDisplayName() + ",欢迎来到" + config.getString("server-name") + "!"); + return; + } + + player.setInvulnerable(true); + + if (playerVerification.containsKey(player.getUniqueId())) { + if (playerVerification.get(player.getUniqueId())) { + return; + } else { + playerVerification.put(player.getUniqueId(), false); + player.sendMessage(config.getString("captcha-needed")); + } + } else { + playerVerification.put(player.getUniqueId(), false); + player.sendMessage(config.getString("captcha-needed")); + } + + // 使用BukkitScheduler安排一个同步任务来应用效果 + Bukkit.getScheduler().runTask(Main.getInstance(), () -> { + // 赋予失明效果 + player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, Integer.MAX_VALUE, 1, false, false)); + + // 赋予夜视效果 + player.addPotionEffect(new PotionEffect(PotionEffectType.NIGHT_VISION, Integer.MAX_VALUE, 1, false, false)); + }); + + player.sendTitle(ChatColor.BLUE + "风控系统", ChatColor.WHITE + "请在聊天框中输入验证码", 10, 70, 20); + + ItemStack item = player.getInventory().getItemInMainHand(); + + // 检查是否手持物品 + if (!isCustomMap(item)) { + // 存储玩家手持的物品 + playerItems.put(player.getUniqueId(), item); + + // 删除玩家手持的物品 + player.getInventory().setItemInMainHand(new ItemStack(Material.AIR)); + + item = new ItemStack(Material.FILLED_MAP, 1); + MapMeta meta = (MapMeta) item.getItemMeta(); + + // 创建新地图并添加到玩家手中 + MapView mapView = null; + if (meta.hasMapView()) { + if (meta != null) { + mapView = meta.getMapView(); + } + } else { + mapView = Bukkit.createMap(player.getWorld()); + } + + if (mapView != null) { + mapView.getRenderers().clear(); // 清除默认渲染器 + mapView.addRenderer(new MapRenderer() { + @Override + public void render(MapView map, MapCanvas canvas, Player player) { + String base64Image; + String[] captcha; + if (playerCaptcha.containsKey(player.getUniqueId())) { + captcha = playerCaptcha.get(player.getUniqueId()); + } else { + FancyCaptchaGenerator fancyCaptchaGenerator = new FancyCaptchaGenerator(); + captcha = fancyCaptchaGenerator.generateCaptcha(); + playerCaptcha.put(player.getUniqueId(), captcha); + } + base64Image = captcha[0]; + // 将BASE64编码的图片渲染到地图上 + try { + byte[] imageBytes = Base64.getDecoder().decode(base64Image); + ByteArrayInputStream bis = new ByteArrayInputStream(imageBytes); + BufferedImage image = ImageIO.read(bis); + + // 确保图片大小符合要求 (128x128) + if (image.getWidth() != 128 || image.getHeight() != 128) { + getLogger().warning("Image dimensions are not 128x128 pixels."); + return; + } + + // 将图片像素绘制到地图画布上 + for (int y = 0; y < 128; y++) { + for (int x = 0; x < 128; x++) { + int colorValue = image.getRGB(x, y); + Color color = new Color(colorValue, true); // 使用 true 表示保留 alpha 通道 + canvas.setPixel(x, y, MapPalette.matchColor(color)); + } + } + } catch (IOException e) { + getLogger().warning("Error decoding BASE64 image: " + e.getMessage()); + } + // 在地图上添加文字 + canvas.drawText(10, 10, MinecraftFont.Font, "MeowCaptcha"); + } + }); + meta.setMapView(mapView); + } + + if (meta != null) { + meta.setCustomModelData(1); // 设置特殊的 CustomModelData,用于区分自定义地图 + } + item.setItemMeta(meta); + + player.getInventory().setItem(player.getInventory().getHeldItemSlot(), item); + + // 存储玩家地图 + playerMaps.put(player.getUniqueId(), mapView); + } + + // 创建一个新的BossBar,并设置样式和颜色 + BossBar bossBar = Bukkit.createBossBar("Verification Countdown", BarColor.BLUE, BarStyle.SOLID); + bossBar.setProgress(1.0); // 设置初始进度为满 + bossBar.addPlayer(player); // 将玩家添加到BossBar中 + + // 将BossBar存储到Map中 + playerBossBars.put(player.getUniqueId(), bossBar); + + // 启动一个倒计时任务 + BukkitTask countdownTask = new BukkitRunnable() { + int countdown = COUNTDOWN_DURATION; + + @Override + public void run() { + // 计算剩余时间的百分比 + double progress = (double) countdown / COUNTDOWN_DURATION; + + // 更新BossBar的进度 + if (progress < 0) { + progress = 0; + } else if (progress > 1) { + progress = 1; + } + bossBar.setProgress(progress); + + // 更新BossBar的进度 + bossBar.setProgress(progress); + + // 更新BossBar显示的时间 + bossBar.setTitle("验证倒计时: " + countdown + "s"); + + if (progress < 0.5) { + bossBar.setColor(BarColor.YELLOW); + } + if (progress < 0.2) { + bossBar.setColor(BarColor.RED); + } + + // 如果倒计时结束且玩家未验证通过,则踢出玩家 + if (countdown <= 0 && !playerVerification.getOrDefault(player.getUniqueId(), false)) { + // 使用BukkitScheduler安排一个同步任务来移除效果 + Bukkit.getScheduler().runTask(Main.getInstance(), () -> { + do { + player.removePotionEffect(PotionEffectType.BLINDNESS); + } while (player.hasPotionEffect(PotionEffectType.BLINDNESS)); + + do { + player.removePotionEffect(PotionEffectType.NIGHT_VISION); + } while (player.hasPotionEffect(PotionEffectType.NIGHT_VISION)); + + }); + + playerVerification.remove(player.getUniqueId()); + playerCaptcha.remove(player.getUniqueId()); + // 检查玩家是否存在存储的物品和地图 + if (playerItems.containsKey(player.getUniqueId()) && playerMaps.containsKey(player.getUniqueId())) { + // 还原玩家手中的物品 + player.getInventory().setItemInMainHand(playerItems.get(player.getUniqueId())); + + // 删除玩家手中的地图 + MapView mapView = playerMaps.get(player.getUniqueId()); + if (mapView != null) { + mapView.removeRenderer(mapView.getRenderers().get(0)); + player.getInventory().removeItem(new ItemStack(Material.FILLED_MAP, 1, (short) mapView.getId())); + } + + playerItems.remove(player.getUniqueId()); + playerMaps.remove(player.getUniqueId()); + } + + // 移除玩家对应的BossBar和倒计时任务 + BossBar bossBar = playerBossBars.remove(player.getUniqueId()); + if (bossBar != null) { + bossBar.removeAll(); // 移除所有玩家 + } + + player.kickPlayer(""" + 被服务器踢出 + + §c原因:验证超时 + + §8================================== + + §7进入服务器后,请在规定时间内通过人机验证。 + + 频道号:""" + config.getString("channel-id") + """ + + §7更多帮助请参考:§f""" + config.getString("reference-page") + """ + §7 + + + """ + getCurrentDateTime()); + + cancel(); // 停止倒计时任务 + } + + countdown--; + } + }.runTaskTimer(Main.getInstance(), 0L, 20L); // 每秒更新一次 + + // 存储倒计时任务,以便后续取消 + countdownTasks.put(player.getUniqueId(), countdownTask); + + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerDropItem(PlayerDropItemEvent event) { + Player player = event.getPlayer(); + ItemStack item = event.getItemDrop().getItemStack(); + + // 检查是否扔出的物品是地图,并且是自定义地图 + if (item.getType() == Material.FILLED_MAP && isCustomMap(item) && playerMaps.containsKey(player.getUniqueId())) { + event.setCancelled(true); // 阻止玩家扔出地图 + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + UUID playerId = player.getUniqueId(); + playerVerification.remove(playerId); + playerCaptcha.remove(playerId); + // 检查玩家是否存在存储的物品和地图 + if (playerItems.containsKey(player.getUniqueId()) && playerMaps.containsKey(player.getUniqueId())) { + // 还原玩家手中的物品 + player.getInventory().setItemInMainHand(playerItems.get(player.getUniqueId())); + + // 删除玩家手中的地图 + MapView mapView = playerMaps.get(player.getUniqueId()); + if (mapView != null) { + mapView.removeRenderer(mapView.getRenderers().get(0)); + player.getInventory().removeItem(new ItemStack(Material.FILLED_MAP, 1, (short) mapView.getId())); + } + + playerItems.remove(player.getUniqueId()); + playerMaps.remove(player.getUniqueId()); + } + + // 移除玩家对应的BossBar和倒计时任务 + BossBar bossBar = playerBossBars.remove(playerId); + if (bossBar != null) { + bossBar.removeAll(); // 移除所有玩家 + } + + BukkitTask countdownTask = countdownTasks.remove(playerId); + if (countdownTask != null) { + countdownTask.cancel(); // 取消倒计时任务 + } + + // 使用BukkitScheduler安排一个同步任务来移除效果 + Bukkit.getScheduler().runTask(Main.getInstance(), () -> { + // 取消失明效果 + player.removePotionEffect(PotionEffectType.BLINDNESS); + + // 取消夜视效果 + player.removePotionEffect(PotionEffectType.NIGHT_VISION); + }); + + playerVerification.remove(playerId); + playerCaptcha.remove(playerId); + playerItems.remove(playerId); + playerMaps.remove(playerId); + } + + @EventHandler + public void onPlayerKick(PlayerKickEvent event) { + Player player = event.getPlayer(); + UUID playerId = player.getUniqueId(); + playerVerification.remove(playerId); + playerCaptcha.remove(playerId); + // 检查玩家是否存在存储的物品和地图 + if (playerItems.containsKey(player.getUniqueId()) && playerMaps.containsKey(player.getUniqueId())) { + // 还原玩家手中的物品 + player.getInventory().setItemInMainHand(playerItems.get(player.getUniqueId())); + + // 删除玩家手中的地图 + MapView mapView = playerMaps.get(player.getUniqueId()); + if (mapView != null) { + mapView.removeRenderer(mapView.getRenderers().get(0)); + player.getInventory().removeItem(new ItemStack(Material.FILLED_MAP, 1, (short) mapView.getId())); + } + + playerItems.remove(player.getUniqueId()); + playerMaps.remove(player.getUniqueId()); + } + + // 移除玩家对应的BossBar和倒计时任务 + BossBar bossBar = playerBossBars.remove(playerId); + if (bossBar != null) { + bossBar.removeAll(); // 移除所有玩家 + } + + BukkitTask countdownTask = countdownTasks.remove(playerId); + if (countdownTask != null) { + countdownTask.cancel(); // 取消倒计时任务 + } + + // 使用BukkitScheduler安排一个同步任务来移除效果 + Bukkit.getScheduler().runTask(Main.getInstance(), () -> { + // 取消失明效果 + player.removePotionEffect(PotionEffectType.BLINDNESS); + + // 取消夜视效果 + player.removePotionEffect(PotionEffectType.NIGHT_VISION); + }); + + playerVerification.remove(playerId); + playerCaptcha.remove(playerId); + playerItems.remove(playerId); + playerMaps.remove(playerId); + } + + // 检查是否为自定义地图 + private boolean isCustomMap(ItemStack item) { + if (item.hasItemMeta() && item.getItemMeta().hasCustomModelData()) { + int customModelData = item.getItemMeta().getCustomModelData(); + return customModelData == 1; + } + return false; + } + + // Handle player commands + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerCommand(PlayerCommandPreprocessEvent event) { + UUID playerId = event.getPlayer().getUniqueId(); + if (!playerVerification.getOrDefault(playerId, false)) { + event.setCancelled(true); + event.getPlayer().sendMessage(config.getString("captcha-not-authed")); + } + } + + // Handle player chat + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + UUID playerId = player.getUniqueId(); + if (!playerVerification.getOrDefault(playerId, false)) { + if (playerCaptcha.containsKey(player.getUniqueId())) { + String[] captcha; + captcha = playerCaptcha.get(player.getUniqueId()); + if (Objects.equals(captcha[1], event.getMessage())) { + playerVerification.put(player.getUniqueId(), true); + player.sendTitle(ChatColor.GREEN + "验证成功", ChatColor.YELLOW + "您现在可正常游戏了", 10, 70, 20); + player.sendMessage("§a亲爱的" + player.getDisplayName() + ",欢迎来到" + config.getString("server-name") + "!"); + if (playerItems.containsKey(player.getUniqueId()) && playerMaps.containsKey(player.getUniqueId())) { + // 如果玩家验证通过,则取消倒计时任务和移除 BossBar + cancelCountdown(player); + + player.setInvulnerable(false); + + // 还原玩家手中的物品 + player.getInventory().setItemInMainHand(playerItems.get(player.getUniqueId())); + + // 删除玩家手中的地图 + MapView mapView = playerMaps.get(player.getUniqueId()); + mapView.removeRenderer(mapView.getRenderers().get(0)); + player.getInventory().removeItem(new ItemStack(Material.FILLED_MAP, 1, (short) mapView.getId())); + + // 使用BukkitScheduler安排一个同步任务来移除效果 + Bukkit.getScheduler().runTask(Main.getInstance(), () -> { + do { + player.removePotionEffect(PotionEffectType.BLINDNESS); + } while (player.hasPotionEffect(PotionEffectType.BLINDNESS)); + + do { + player.removePotionEffect(PotionEffectType.NIGHT_VISION); + } while (player.hasPotionEffect(PotionEffectType.NIGHT_VISION)); + + }); + + playerItems.remove(player.getUniqueId()); + playerMaps.remove(player.getUniqueId()); + } + } else { + event.getPlayer().sendMessage(config.getString("captcha-code-error")); + } + } else { + event.getPlayer().sendMessage(config.getString("captcha-not-authed")); + } + event.setCancelled(true); + } + } + + // Handle player interactions + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerInteract(PlayerInteractEvent event) { + UUID playerId = event.getPlayer().getUniqueId(); + if (!playerVerification.getOrDefault(playerId, false)) { + event.setCancelled(true); + } + } + + // 处理玩家移动 + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerMove(PlayerMoveEvent event) { + UUID playerId = event.getPlayer().getUniqueId(); + + // 检查玩家是否未验证 + if (!playerVerification.getOrDefault(playerId, false)) { + // 获取当前位置和目标位置 + Location from = event.getFrom(); + Location to = event.getTo(); + + // 将水平方向上的移动分量设置为零 + to.setX(from.getX()); + to.setY(from.getY()); + to.setZ(from.getZ()); + + // 将水平方向上的速度设置为零 + event.getPlayer().setVelocity(new Vector(0, 0, 0)); + + // 更新玩家的目标位置 + event.setTo(to); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onItemSwitch(PlayerItemHeldEvent event) { + UUID playerId = event.getPlayer().getUniqueId(); + if (!playerVerification.getOrDefault(playerId, false)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerSwapHandItems(PlayerSwapHandItemsEvent event) { + UUID playerId = event.getPlayer().getUniqueId(); + if (!playerVerification.getOrDefault(playerId, false)) { + event.setCancelled(true); + event.getPlayer().sendMessage(config.getString("captcha-not-authed")); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onInventoryOpenEvent(InventoryOpenEvent event) { + UUID playerId = event.getPlayer().getUniqueId(); + if (!playerVerification.getOrDefault(playerId, false)) { + event.setCancelled(true); + event.getPlayer().sendMessage(config.getString("captcha-not-authed")); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onDisable(PluginDisableEvent event) { + saveData(); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onEnable(PluginEnableEvent event) { + loadData(); + } + + private void saveData() { + dataConfig.set("playerItems", null); + dataConfig.set("playerMaps", null); + + for (UUID playerId : playerItems.keySet()) { + dataConfig.set("playerItems." + playerId, playerItems.get(playerId)); + } + + for (UUID playerId : playerMaps.keySet()) { + dataConfig.set("playerMaps." + playerId, playerMaps.get(playerId).getId()); + } + + try { + dataConfig.save(dataFile); + } catch (IOException e) { + getLogger().severe(e.getMessage()); + } + } + + private void loadData() { + if (dataFile.exists()) { + ConfigurationSection itemsSection = dataConfig.getConfigurationSection("playerItems"); + if (itemsSection != null) { + for (String playerIdStr : itemsSection.getKeys(false)) { + UUID playerId = UUID.fromString(playerIdStr); + ItemStack itemStack = (ItemStack) itemsSection.get(playerIdStr); + playerItems.put(playerId, itemStack); + } + } + + ConfigurationSection mapsSection = dataConfig.getConfigurationSection("playerMaps"); + if (mapsSection != null) { + for (String playerIdStr : mapsSection.getKeys(false)) { + UUID playerId = UUID.fromString(playerIdStr); + int mapViewId = mapsSection.getInt(playerIdStr); + MapView mapView = Bukkit.getMap(mapViewId); + if (mapView != null) { + playerMaps.put(playerId, mapView); + } + } + } + } + } + + private void cancelCountdown(Player player) { + UUID playerId = player.getUniqueId(); + BukkitTask countdownTask = countdownTasks.remove(playerId); + if (countdownTask != null) { + countdownTask.cancel(); // 取消倒计时任务 + } + + BossBar bossBar = playerBossBars.remove(playerId); + if (bossBar != null) { + bossBar.removeAll(); // 移除所有玩家 + } + } +} diff --git a/src/main/java/cn/meowdream/meowlogin/listeners/PluginCommand.java b/src/main/java/cn/meowdream/meowlogin/listeners/PluginCommand.java new file mode 100644 index 0000000..427d31b --- /dev/null +++ b/src/main/java/cn/meowdream/meowlogin/listeners/PluginCommand.java @@ -0,0 +1,21 @@ +package cn.meowdream.meowlogin.listeners; + +import cn.meowdream.meowlogin.Main; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; + +public class PluginCommand implements CommandExecutor { + + @Override + public boolean onCommand(CommandSender sender, Command pluginCommand, String label, String[] args) { + if (sender.hasPermission("meowlogin.reloadconfig")) { + Main.getInstance().reloadConfig(); + sender.sendMessage("配置文件已重新加载。"); + return true; + } else { + sender.sendMessage("你没有权限执行此命令。"); + return false; + } + } +} diff --git a/src/main/java/cn/meowdream/meowlogin/utils/FancyCaptchaGenerator.java b/src/main/java/cn/meowdream/meowlogin/utils/FancyCaptchaGenerator.java new file mode 100644 index 0000000..765d4ec --- /dev/null +++ b/src/main/java/cn/meowdream/meowlogin/utils/FancyCaptchaGenerator.java @@ -0,0 +1,130 @@ +package cn.meowdream.meowlogin.utils; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.geom.QuadCurve2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.Random; + +public class FancyCaptchaGenerator { + + public String[] generateCaptcha() { + // Generate random captcha text + String captchaText = generateRandomCaptchaText(); + + // Generate image + BufferedImage image = createCaptchaImage(captchaText); + + // Convert image to base64 + String base64Image = encodeImageToBase64(image); + + return new String[]{base64Image, captchaText}; + } + + private static String generateRandomCaptchaText() { + String characters = "0123456789@#%&"; + Random random = new Random(); + int length = random.nextInt(2) + 4; // Random length between 4 and 6 + StringBuilder captchaText = new StringBuilder(); + for (int i = 0; i < length; i++) { + captchaText.append(characters.charAt(random.nextInt(characters.length()))); + } + return captchaText.toString(); + } + + private static BufferedImage createCaptchaImage(String text) { + int width = 128; + int height = 128; + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = image.createGraphics(); + + // Set background gradient + Color startColor = getRandomLightColor(); + Color endColor = getRandomLightColor(); + int startX = new Random().nextInt(width); + int startY = new Random().nextInt(height); + int endX = new Random().nextInt(width); + int endY = new Random().nextInt(height); + GradientPaint gradient = new GradientPaint(startX, startY, startColor, endX, endY, endColor); + graphics.setPaint(gradient); + graphics.fillRect(0, 0, width, height); + + // Draw random curves + graphics.setColor(getRandomLightColor()); + for (int i = 0; i < 10; i++) { + int x1 = new Random().nextInt(width); + int y1 = new Random().nextInt(height); + int x2 = new Random().nextInt(width); + int y2 = new Random().nextInt(height); + int ctrlx = new Random().nextInt(width); + int ctrly = new Random().nextInt(height); + graphics.draw(new QuadCurve2D.Float(x1, y1, ctrlx, ctrly, x2, y2)); + } + + // Draw random circles (outline only) + graphics.setStroke(new BasicStroke(2)); // Set stroke width + for (int i = 0; i < 5; i++) { + int circleX = new Random().nextInt(width); + int circleY = new Random().nextInt(height); + int circleSize = new Random().nextInt(20) + 10; // Random size between 10 and 30 + graphics.setColor(getRandomLightColor()); + graphics.drawOval(circleX, circleY, circleSize, circleSize); + } + + // Draw random triangles (outline only) + for (int i = 0; i < 3; i++) { + int[] xPoints = {new Random().nextInt(width), new Random().nextInt(width), new Random().nextInt(width)}; + int[] yPoints = {new Random().nextInt(height), new Random().nextInt(height), new Random().nextInt(height)}; + graphics.setColor(getRandomLightColor()); + graphics.drawPolygon(xPoints, yPoints, 3); + } + + // Draw text with random rotation + graphics.setFont(new Font("Arial", Font.BOLD, 24)); + FontMetrics fm = graphics.getFontMetrics(); + int textWidth = fm.stringWidth(text); + int x = (width - textWidth) / 2; + int y = height / 2 + fm.getAscent() / 2; + for (int i = 0; i < text.length(); i++) { + graphics.setColor(getRandomLightColor()); + int rotation = new Random().nextInt(40) - 20; // Random rotation between -20 and 20 degrees + graphics.rotate(Math.toRadians(rotation), x, y); // Rotate around the center of the character + graphics.drawString(String.valueOf(text.charAt(i)), x, y); + x += fm.charWidth(text.charAt(i)); + graphics.rotate(Math.toRadians(-rotation), x, y); // Reset rotation + } + + graphics.dispose(); + return image; + } + + private static Color getRandomDarkColor() { + Random random = new Random(); + int red = 128 + random.nextInt(128); + int green = 128 + random.nextInt(128); + int blue = 128 + random.nextInt(128); + return new Color(red, green, blue); + } + + private static Color getRandomLightColor() { + Random random = new Random(); + int red = random.nextInt(128); + int green = random.nextInt(128); + int blue = random.nextInt(128); + return new Color(red, green, blue); + } + + private static String encodeImageToBase64(BufferedImage image) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + ImageIO.write(image, "png", bos); + } catch (IOException e) { + e.printStackTrace(); + } + byte[] imageBytes = bos.toByteArray(); + return Base64.getEncoder().encodeToString(imageBytes); + } +} diff --git a/src/main/java/cn/meowdream/meowlogin/utils/Metrics.java b/src/main/java/cn/meowdream/meowlogin/utils/Metrics.java new file mode 100644 index 0000000..a2a699f --- /dev/null +++ b/src/main/java/cn/meowdream/meowlogin/utils/Metrics.java @@ -0,0 +1,881 @@ +/* + * This Metrics class was auto-generated and can be copied into your project if you are + * not using a build tool like Gradle or Maven for dependency management. + * + * IMPORTANT: You are not allowed to modify this class, except changing the package. + * + * Disallowed modifications include but are not limited to: + * - Remove the option for users to opt-out + * - Change the frequency for data submission + * - Obfuscate the code (every obfuscator should allow you to make an exception for specific files) + * - Reformat the code (if you use a linter, add an exception) + * + * Violations will result in a ban of your plugin and account from bStats. + */ +package cn.meowdream.meowlogin.utils; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.zip.GZIPOutputStream; +import javax.net.ssl.HttpsURLConnection; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +public class Metrics { + + private final Plugin plugin; + + private final MetricsBase metricsBase; + + /** + * Creates a new Metrics instance. + * + * @param plugin Your plugin instance. + * @param serviceId The id of the service. It can be found at What is my plugin id? + */ + public Metrics(JavaPlugin plugin, int serviceId) { + this.plugin = plugin; + // Get the config file + File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); + File configFile = new File(bStatsFolder, "config.yml"); + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + if (!config.isSet("serverUuid")) { + config.addDefault("enabled", true); + config.addDefault("serverUuid", UUID.randomUUID().toString()); + config.addDefault("logFailedRequests", false); + config.addDefault("logSentData", false); + config.addDefault("logResponseStatusText", false); + // Inform the server owners about bStats + config + .options() + .header( + "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n" + + "many people use their plugin and their total player count. It's recommended to keep bStats\n" + + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n" + + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n" + + "anonymous.") + .copyDefaults(true); + try { + config.save(configFile); + } catch (IOException ignored) { + } + } + // Load the data + boolean enabled = config.getBoolean("enabled", true); + String serverUUID = config.getString("serverUuid"); + boolean logErrors = config.getBoolean("logFailedRequests", false); + boolean logSentData = config.getBoolean("logSentData", false); + boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false); + metricsBase = + new MetricsBase( + "bukkit", + serverUUID, + serviceId, + enabled, + this::appendPlatformData, + this::appendServiceData, + submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask), + plugin::isEnabled, + (message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error), + (message) -> this.plugin.getLogger().log(Level.INFO, message), + logErrors, + logSentData, + logResponseStatusText); + } + + /** Shuts down the underlying scheduler service. */ + public void shutdown() { + metricsBase.shutdown(); + } + + /** + * Adds a custom chart. + * + * @param chart The chart to add. + */ + public void addCustomChart(CustomChart chart) { + metricsBase.addCustomChart(chart); + } + + private void appendPlatformData(JsonObjectBuilder builder) { + builder.appendField("playerAmount", getPlayerAmount()); + builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0); + builder.appendField("bukkitVersion", Bukkit.getVersion()); + builder.appendField("bukkitName", Bukkit.getName()); + builder.appendField("javaVersion", System.getProperty("java.version")); + builder.appendField("osName", System.getProperty("os.name")); + builder.appendField("osArch", System.getProperty("os.arch")); + builder.appendField("osVersion", System.getProperty("os.version")); + builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); + } + + private void appendServiceData(JsonObjectBuilder builder) { + builder.appendField("pluginVersion", plugin.getDescription().getVersion()); + } + + private int getPlayerAmount() { + try { + // Around MC 1.8 the return type was changed from an array to a collection, + // This fixes java.lang.NoSuchMethodError: + // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; + Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); + return onlinePlayersMethod.getReturnType().equals(Collection.class) + ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() + : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; + } catch (Exception e) { + // Just use the new method if the reflection failed + return Bukkit.getOnlinePlayers().size(); + } + } + + public static class MetricsBase { + + /** The version of the Metrics class. */ + public static final String METRICS_VERSION = "3.0.2"; + + private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s"; + + private final ScheduledExecutorService scheduler; + + private final String platform; + + private final String serverUuid; + + private final int serviceId; + + private final Consumer appendPlatformDataConsumer; + + private final Consumer appendServiceDataConsumer; + + private final Consumer submitTaskConsumer; + + private final Supplier checkServiceEnabledSupplier; + + private final BiConsumer errorLogger; + + private final Consumer infoLogger; + + private final boolean logErrors; + + private final boolean logSentData; + + private final boolean logResponseStatusText; + + private final Set customCharts = new HashSet<>(); + + private final boolean enabled; + + /** + * Creates a new MetricsBase class instance. + * + * @param platform The platform of the service. + * @param serviceId The id of the service. + * @param serverUuid The server uuid. + * @param enabled Whether or not data sending is enabled. + * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and + * appends all platform-specific data. + * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and + * appends all service-specific data. + * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be + * used to delegate the data collection to a another thread to prevent errors caused by + * concurrency. Can be {@code null}. + * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. + * @param errorLogger A consumer that accepts log message and an error. + * @param infoLogger A consumer that accepts info log messages. + * @param logErrors Whether or not errors should be logged. + * @param logSentData Whether or not the sent data should be logged. + * @param logResponseStatusText Whether or not the response status text should be logged. + */ + public MetricsBase( + String platform, + String serverUuid, + int serviceId, + boolean enabled, + Consumer appendPlatformDataConsumer, + Consumer appendServiceDataConsumer, + Consumer submitTaskConsumer, + Supplier checkServiceEnabledSupplier, + BiConsumer errorLogger, + Consumer infoLogger, + boolean logErrors, + boolean logSentData, + boolean logResponseStatusText) { + ScheduledThreadPoolExecutor scheduler = + new ScheduledThreadPoolExecutor(1, task -> new Thread(task, "bStats-Metrics")); + // We want delayed tasks (non-periodic) that will execute in the future to be + // cancelled when the scheduler is shutdown. + // Otherwise, we risk preventing the server from shutting down even when + // MetricsBase#shutdown() is called + scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + this.scheduler = scheduler; + this.platform = platform; + this.serverUuid = serverUuid; + this.serviceId = serviceId; + this.enabled = enabled; + this.appendPlatformDataConsumer = appendPlatformDataConsumer; + this.appendServiceDataConsumer = appendServiceDataConsumer; + this.submitTaskConsumer = submitTaskConsumer; + this.checkServiceEnabledSupplier = checkServiceEnabledSupplier; + this.errorLogger = errorLogger; + this.infoLogger = infoLogger; + this.logErrors = logErrors; + this.logSentData = logSentData; + this.logResponseStatusText = logResponseStatusText; + checkRelocation(); + if (enabled) { + // WARNING: Removing the option to opt-out will get your plugin banned from + // bStats + startSubmitting(); + } + } + + public void addCustomChart(CustomChart chart) { + this.customCharts.add(chart); + } + + public void shutdown() { + scheduler.shutdown(); + } + + private void startSubmitting() { + final Runnable submitTask = + () -> { + if (!enabled || !checkServiceEnabledSupplier.get()) { + // Submitting data or service is disabled + scheduler.shutdown(); + return; + } + if (submitTaskConsumer != null) { + submitTaskConsumer.accept(this::submitData); + } else { + this.submitData(); + } + }; + // Many servers tend to restart at a fixed time at xx:00 which causes an uneven + // distribution of requests on the + // bStats backend. To circumvent this problem, we introduce some randomness into + // the initial and second delay. + // WARNING: You must not modify and part of this Metrics class, including the + // submit delay or frequency! + // WARNING: Modifying this code will get your plugin banned on bStats. Just + // don't do it! + long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); + long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); + scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); + scheduler.scheduleAtFixedRate( + submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); + } + + private void submitData() { + final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder(); + appendPlatformDataConsumer.accept(baseJsonBuilder); + final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder(); + appendServiceDataConsumer.accept(serviceJsonBuilder); + JsonObjectBuilder.JsonObject[] chartData = + customCharts.stream() + .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors)) + .filter(Objects::nonNull) + .toArray(JsonObjectBuilder.JsonObject[]::new); + serviceJsonBuilder.appendField("id", serviceId); + serviceJsonBuilder.appendField("customCharts", chartData); + baseJsonBuilder.appendField("service", serviceJsonBuilder.build()); + baseJsonBuilder.appendField("serverUUID", serverUuid); + baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION); + JsonObjectBuilder.JsonObject data = baseJsonBuilder.build(); + scheduler.execute( + () -> { + try { + // Send the data + sendData(data); + } catch (Exception e) { + // Something went wrong! :( + if (logErrors) { + errorLogger.accept("Could not submit bStats metrics data", e); + } + } + }); + } + + private void sendData(JsonObjectBuilder.JsonObject data) throws Exception { + if (logSentData) { + infoLogger.accept("Sent bStats metrics data: " + data.toString()); + } + String url = String.format(REPORT_URL, platform); + HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); + // Compress the data to save bandwidth + byte[] compressedData = compress(data.toString()); + connection.setRequestMethod("POST"); + connection.addRequestProperty("Accept", "application/json"); + connection.addRequestProperty("Connection", "close"); + connection.addRequestProperty("Content-Encoding", "gzip"); + connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("User-Agent", "Metrics-Service/1"); + connection.setDoOutput(true); + try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { + outputStream.write(compressedData); + } + StringBuilder builder = new StringBuilder(); + try (BufferedReader bufferedReader = + new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + builder.append(line); + } + } + if (logResponseStatusText) { + infoLogger.accept("Sent data to bStats and received response: " + builder); + } + } + + /** Checks that the class was properly relocated. */ + private void checkRelocation() { + // You can use the property to disable the check in your test environment + if (System.getProperty("bstats.relocatecheck") == null + || !System.getProperty("bstats.relocatecheck").equals("false")) { + // Maven's Relocate is clever and changes strings, too. So we have to use this + // little "trick" ... :D + final String defaultPackage = + new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'}); + final String examplePackage = + new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); + // We want to make sure no one just copy & pastes the example and uses the wrong + // package names + if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage) + || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) { + throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); + } + } + } + + /** + * Gzips the given string. + * + * @param str The string to gzip. + * @return The gzipped string. + */ + private static byte[] compress(final String str) throws IOException { + if (str == null) { + return null; + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { + gzip.write(str.getBytes(StandardCharsets.UTF_8)); + } + return outputStream.toByteArray(); + } + } + + public static class SimplePie extends CustomChart { + + private final Callable callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SimplePie(String chartId, Callable callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + String value = callable.call(); + if (value == null || value.isEmpty()) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("value", value).build(); + } + } + + public static class MultiLineChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public MultiLineChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class AdvancedPie extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public AdvancedPie(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class SimpleBarChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SimpleBarChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + for (Map.Entry entry : map.entrySet()) { + valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()}); + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class AdvancedBarChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public AdvancedBarChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue().length == 0) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class DrilldownPie extends CustomChart { + + private final Callable>> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public DrilldownPie(String chartId, Callable>> callable) { + super(chartId); + this.callable = callable; + } + + @Override + public JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map> map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean reallyAllSkipped = true; + for (Map.Entry> entryValues : map.entrySet()) { + JsonObjectBuilder valueBuilder = new JsonObjectBuilder(); + boolean allSkipped = true; + for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { + valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue()); + allSkipped = false; + } + if (!allSkipped) { + reallyAllSkipped = false; + valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build()); + } + } + if (reallyAllSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public abstract static class CustomChart { + + private final String chartId; + + protected CustomChart(String chartId) { + if (chartId == null) { + throw new IllegalArgumentException("chartId must not be null"); + } + this.chartId = chartId; + } + + public JsonObjectBuilder.JsonObject getRequestJsonObject( + BiConsumer errorLogger, boolean logErrors) { + JsonObjectBuilder builder = new JsonObjectBuilder(); + builder.appendField("chartId", chartId); + try { + JsonObjectBuilder.JsonObject data = getChartData(); + if (data == null) { + // If the data is null we don't send the chart. + return null; + } + builder.appendField("data", data); + } catch (Throwable t) { + if (logErrors) { + errorLogger.accept("Failed to get data for custom chart with id " + chartId, t); + } + return null; + } + return builder.build(); + } + + protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception; + } + + public static class SingleLineChart extends CustomChart { + + private final Callable callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SingleLineChart(String chartId, Callable callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + int value = callable.call(); + if (value == 0) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("value", value).build(); + } + } + + /** + * An extremely simple JSON builder. + * + *

While this class is neither feature-rich nor the most performant one, it's sufficient enough + * for its use-case. + */ + public static class JsonObjectBuilder { + + private StringBuilder builder = new StringBuilder(); + + private boolean hasAtLeastOneField = false; + + public JsonObjectBuilder() { + builder.append("{"); + } + + /** + * Appends a null field to the JSON. + * + * @param key The key of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendNull(String key) { + appendFieldUnescaped(key, "null"); + return this; + } + + /** + * Appends a string field to the JSON. + * + * @param key The key of the field. + * @param value The value of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, String value) { + if (value == null) { + throw new IllegalArgumentException("JSON value must not be null"); + } + appendFieldUnescaped(key, "\"" + escape(value) + "\""); + return this; + } + + /** + * Appends an integer field to the JSON. + * + * @param key The key of the field. + * @param value The value of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, int value) { + appendFieldUnescaped(key, String.valueOf(value)); + return this; + } + + /** + * Appends an object to the JSON. + * + * @param key The key of the field. + * @param object The object. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, JsonObject object) { + if (object == null) { + throw new IllegalArgumentException("JSON object must not be null"); + } + appendFieldUnescaped(key, object.toString()); + return this; + } + + /** + * Appends a string array to the JSON. + * + * @param key The key of the field. + * @param values The string array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, String[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values) + .map(value -> "\"" + escape(value) + "\"") + .collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends an integer array to the JSON. + * + * @param key The key of the field. + * @param values The integer array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, int[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends an object array to the JSON. + * + * @param key The key of the field. + * @param values The integer array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, JsonObject[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends a field to the object. + * + * @param key The key of the field. + * @param escapedValue The escaped value of the field. + */ + private void appendFieldUnescaped(String key, String escapedValue) { + if (builder == null) { + throw new IllegalStateException("JSON has already been built"); + } + if (key == null) { + throw new IllegalArgumentException("JSON key must not be null"); + } + if (hasAtLeastOneField) { + builder.append(","); + } + builder.append("\"").append(escape(key)).append("\":").append(escapedValue); + hasAtLeastOneField = true; + } + + /** + * Builds the JSON string and invalidates this builder. + * + * @return The built JSON string. + */ + public JsonObject build() { + if (builder == null) { + throw new IllegalStateException("JSON has already been built"); + } + JsonObject object = new JsonObject(builder.append("}").toString()); + builder = null; + return object; + } + + /** + * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt. + * + *

This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. + * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). + * + * @param value The value to escape. + * @return The escaped value. + */ + private static String escape(String value) { + final StringBuilder builder = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '"') { + builder.append("\\\""); + } else if (c == '\\') { + builder.append("\\\\"); + } else if (c <= '\u000F') { + builder.append("\\u000").append(Integer.toHexString(c)); + } else if (c <= '\u001F') { + builder.append("\\u00").append(Integer.toHexString(c)); + } else { + builder.append(c); + } + } + return builder.toString(); + } + + /** + * A super simple representation of a JSON object. + * + *

This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not + * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String, + * JsonObject)}. + */ + public static class JsonObject { + + private final String value; + + private JsonObject(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..32ae426 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,15 @@ +api-server: http://localhost:30030/api + +server-name: Alset 1.18.2 +server-desc: Confirm to connect to Alset Minecraft Server + +channel-id: §f8356nc42vy +reference-page: https://meowdream.cn/wiki + +enable-captcha: true + +captcha-not-authed: §9你还没有完成验证呐 +captcha-code-error: §c验证码错误,请重试 +captcha-needed: §9请输入验证码以继续 + +captcha-countdown-duration: 30 \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..85fd775 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,11 @@ +name: MeowLogin +author: Mewn_Lynsi +version: 1.0.0-SNAPSHOT +main: cn.meowdream.meowlogin.Main +api-version: "1.18" + +commands: + mlreload: + description: 重载MeowLogin配置文件 + usage: /mlreload + permission: meowlogin.reload \ No newline at end of file