diff --git a/Assembly/dependency-reduced-pom.xml b/Assembly/dependency-reduced-pom.xml
index ccd0cfd..976a05c 100644
--- a/Assembly/dependency-reduced-pom.xml
+++ b/Assembly/dependency-reduced-pom.xml
@@ -3,7 +3,7 @@
AntiVPN
dev.brighten.antivpn
- 1.4.0
+ 1.5.0
4.0.0
Assembly
diff --git a/Assembly/pom.xml b/Assembly/pom.xml
index 03f5fef..cda33af 100644
--- a/Assembly/pom.xml
+++ b/Assembly/pom.xml
@@ -5,7 +5,7 @@
AntiVPN
dev.brighten.antivpn
- 1.4.0
+ 1.5.0
4.0.0
@@ -50,6 +50,12 @@
${version}
compile
+
+ dev.brighten.antivpn
+ Velocity
+ ${version}
+ compile
+
dev.brighten.antivpn
Bukkit
diff --git a/Bukkit/dependency-reduced-pom.xml b/Bukkit/dependency-reduced-pom.xml
index 3e7b35e..27bad2f 100644
--- a/Bukkit/dependency-reduced-pom.xml
+++ b/Bukkit/dependency-reduced-pom.xml
@@ -3,7 +3,7 @@
AntiVPN
dev.brighten.antivpn
- 1.4.0
+ 1.5.0
4.0.0
Bukkit
@@ -56,7 +56,7 @@
dev.brighten.antivpn
Common
- 1.4.0
+ 1.5.0
provided
diff --git a/Bukkit/pom.xml b/Bukkit/pom.xml
index c006c24..e05f8e1 100644
--- a/Bukkit/pom.xml
+++ b/Bukkit/pom.xml
@@ -5,7 +5,7 @@
AntiVPN
dev.brighten.antivpn
- 1.4.0
+ 1.5.0
4.0.0
@@ -69,7 +69,7 @@
dev.brighten.antivpn
Common
- 1.4.0
+ 1.5.0
provided
diff --git a/Bungee/dependency-reduced-pom.xml b/Bungee/dependency-reduced-pom.xml
index 4017f08..a3e5d91 100644
--- a/Bungee/dependency-reduced-pom.xml
+++ b/Bungee/dependency-reduced-pom.xml
@@ -3,7 +3,7 @@
AntiVPN
dev.brighten.antivpn
- 1.4.0
+ 1.5.0
4.0.0
Bungee
@@ -50,7 +50,7 @@
dev.brighten.antivpn
Common
- 1.4.0
+ 1.5.0
provided
diff --git a/Bungee/pom.xml b/Bungee/pom.xml
index f483674..5bf654b 100644
--- a/Bungee/pom.xml
+++ b/Bungee/pom.xml
@@ -5,7 +5,7 @@
AntiVPN
dev.brighten.antivpn
- 1.4.0
+ 1.5.0
4.0.0
@@ -63,7 +63,7 @@
dev.brighten.antivpn
Common
- 1.4.0
+ 1.5.0
provided
diff --git a/Common/pom.xml b/Common/pom.xml
index a8482ee..b0e1abb 100644
--- a/Common/pom.xml
+++ b/Common/pom.xml
@@ -5,7 +5,7 @@
AntiVPN
dev.brighten.antivpn
- 1.4.0
+ 1.5.0
4.0.0
diff --git a/Velocity/pom.xml b/Velocity/pom.xml
new file mode 100644
index 0000000..4b1ef2b
--- /dev/null
+++ b/Velocity/pom.xml
@@ -0,0 +1,62 @@
+
+
+
+ AntiVPN
+ dev.brighten.antivpn
+ 1.5.0
+
+ 4.0.0
+
+ Velocity
+
+
+ 8
+ 8
+
+
+
+
+ velocity
+ https://nexus.velocitypowered.com/repository/maven-public/
+
+
+
+
+
+ com.velocitypowered
+ velocity-api
+ 3.0.1
+ provided
+
+
+ dev.brighten.antivpn
+ Common
+ 1.5.0
+ provided
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.7.0
+
+ 8
+ 8
+ -XDignore.symbol.file
+
+
+
+
+
+ src/main/resources
+ true
+
+
+
+
+
\ No newline at end of file
diff --git a/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityCommandExecutor.java b/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityCommandExecutor.java
new file mode 100644
index 0000000..b1ee9bd
--- /dev/null
+++ b/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityCommandExecutor.java
@@ -0,0 +1,39 @@
+package dev.brighten.antivpn.velocity;
+
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.api.proxy.Player;
+import dev.brighten.antivpn.AntiVPN;
+import dev.brighten.antivpn.api.APIPlayer;
+import dev.brighten.antivpn.command.CommandExecutor;
+import lombok.RequiredArgsConstructor;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+
+import java.util.Optional;
+
+@RequiredArgsConstructor
+public class VelocityCommandExecutor implements CommandExecutor {
+
+ private final CommandSource sender;
+
+ @Override
+ public void sendMessage(String message) {
+ sender.sendMessage(LegacyComponentSerializer.builder().character('&').build().deserialize(message));
+ }
+
+ @Override
+ public boolean hasPermission(String permission) {
+ return sender.hasPermission(permission);
+ }
+
+ @Override
+ public Optional getPlayer() {
+ if(!isPlayer()) return Optional.empty();
+
+ return AntiVPN.getInstance().getPlayerExecutor().getPlayer(((Player) sender).getUniqueId());
+ }
+
+ @Override
+ public boolean isPlayer() {
+ return sender instanceof Player;
+ }
+}
diff --git a/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityConfig.java b/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityConfig.java
new file mode 100644
index 0000000..55b11fe
--- /dev/null
+++ b/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityConfig.java
@@ -0,0 +1,166 @@
+package dev.brighten.antivpn.velocity;
+
+import dev.brighten.antivpn.api.VPNConfig;
+import dev.brighten.antivpn.velocity.util.ConfigDefault;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class VelocityConfig implements VPNConfig {
+ private final ConfigDefault licenseDefault = new ConfigDefault<>("",
+ "license", VelocityPlugin.INSTANCE), kickStringDefault =
+ new ConfigDefault<>("Proxies are not allowed on our server",
+ "kickMessage", VelocityPlugin.INSTANCE),
+ defaultDatabaseType = new ConfigDefault<>("MySQL",
+ "database.type", VelocityPlugin.INSTANCE),
+ defaultDatabaseName = new ConfigDefault<>("kaurivpn",
+ "database.database", VelocityPlugin.INSTANCE),
+ defaultUsername = new ConfigDefault<>("root",
+ "database.username", VelocityPlugin.INSTANCE),
+ defaultPassword = new ConfigDefault<>("password",
+ "database.password", VelocityPlugin.INSTANCE),
+ defaultAuthDatabase = new ConfigDefault<>("admin",
+ "database.auth", VelocityPlugin.INSTANCE),
+ defaultIp = new ConfigDefault<>("localhost", "database.ip", VelocityPlugin.INSTANCE),
+ defaultAlertMsg = new ConfigDefault<>("&8[&6KauriVPN&8] &e%player% &7has joined on a VPN/proxy" +
+ " &8(&f%reason%&8) &7in location &8(&f%city%&7, &f%country%&8)", "alerts.message",
+ VelocityPlugin.INSTANCE);
+ private final ConfigDefault cacheResultsDefault = new ConfigDefault<>(true,
+ "cachedResults", VelocityPlugin.INSTANCE),
+ defaultDatabaseEnabled = new ConfigDefault<>(false, "database.enabled",
+ VelocityPlugin.INSTANCE), defaultCommandsEnable = new ConfigDefault<>(false,
+ "commands.enabled", VelocityPlugin.INSTANCE), defaultKickPlayers
+ = new ConfigDefault<>(true, "kickPlayers", VelocityPlugin.INSTANCE),
+ defaultAlertToStaff = new ConfigDefault<>(true, "alerts.enabled",
+ VelocityPlugin.INSTANCE),
+ defaultMetrics = new ConfigDefault<>(true, "bstats", VelocityPlugin.INSTANCE);
+ private final ConfigDefault
+ defaultPort = new ConfigDefault<>(-1, "database.port", VelocityPlugin.INSTANCE);
+ private final ConfigDefault> prefixWhitelistsDefault = new ConfigDefault<>(new ArrayList<>(),
+ "prefixWhitelists", VelocityPlugin.INSTANCE), defaultCommands = new ConfigDefault<>(
+ Collections.singletonList("kick %player% VPNs are not allowed on our server!"), "commands.execute",
+ VelocityPlugin.INSTANCE);
+
+ private String license, kickMessage, databaseType, databaseName, username, password, ip, alertMsg;
+ private List prefixWhitelists, commands;
+ private int port;
+ private boolean cacheResults, databaseEnabled, commandsEnabled, kickPlayers, alertToStaff, metrics;
+
+ @Override
+ public String getLicense() {
+ return license;
+ }
+
+ @Override
+ public boolean cachedResults() {
+ return cacheResults;
+ }
+
+ @Override
+ public String getKickString() {
+ return kickMessage;
+ }
+
+ @Override
+ public String alertMessage() {
+ return alertMsg;
+ }
+
+ @Override
+ public boolean alertToStaff() {
+ return alertToStaff;
+ }
+
+ @Override
+ public boolean runCommands() {
+ return commandsEnabled;
+ }
+
+ @Override
+ public List commands() {
+ return commands;
+ }
+
+ @Override
+ public boolean kickPlayersOnDetect() {
+ return kickPlayers;
+ }
+
+ @Override
+ public List getPrefixWhitelists() {
+ return prefixWhitelists;
+ }
+
+ @Override
+ public boolean isDatabaseEnabled() {
+ return databaseEnabled;
+ }
+
+ @Override
+ public String getDatabaseType() {
+ return databaseType;
+ }
+
+ @Override
+ public String getDatabaseName() {
+ return databaseName;
+ }
+
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ @Override
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public String getIp() {
+ return ip;
+ }
+
+ @Override
+ public int getPort() {
+ if(port == -1) {
+ switch (getDatabaseType().toLowerCase()) {
+ case "mongodb":
+ case "mongo":
+ case "mongod":
+ return 27017;
+ case "sql":
+ case "mysql":
+ return 3306;
+ }
+ }
+
+ return port;
+ }
+
+ @Override
+ public boolean metrics() {
+ return metrics;
+ }
+
+ public void update() {
+ license = licenseDefault.get();
+ kickMessage = kickStringDefault.get();
+ cacheResults = cacheResultsDefault.get();
+ prefixWhitelists = prefixWhitelistsDefault.get();
+ databaseEnabled = defaultDatabaseEnabled.get();
+ databaseType = defaultDatabaseType.get();
+ databaseName = defaultDatabaseName.get();
+ username = defaultUsername.get();
+ password = defaultPassword.get();
+ ip = defaultIp.get();
+ port = defaultPort.get();
+ commandsEnabled = defaultCommandsEnable.get();
+ commands = defaultCommands.get();
+ kickPlayers = defaultKickPlayers.get();
+ alertToStaff = defaultAlertToStaff.get();
+ alertMsg = defaultAlertMsg.get();
+ metrics = defaultMetrics.get();
+ }
+}
diff --git a/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityListener.java b/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityListener.java
new file mode 100644
index 0000000..c4e584a
--- /dev/null
+++ b/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityListener.java
@@ -0,0 +1,91 @@
+package dev.brighten.antivpn.velocity;
+
+import com.velocitypowered.api.event.connection.LoginEvent;
+import com.velocitypowered.api.scheduler.ScheduledTask;
+import dev.brighten.antivpn.AntiVPN;
+import dev.brighten.antivpn.api.APIPlayer;
+import dev.brighten.antivpn.api.VPNExecutor;
+import dev.brighten.antivpn.velocity.util.StringUtils;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+
+public class VelocityListener extends VPNExecutor {
+
+ private ScheduledTask cacheResetTask;
+
+ @Override
+ public void registerListeners() {
+ VelocityPlugin.INSTANCE.getServer().getEventManager()
+ .register(VelocityPlugin.INSTANCE, this);
+
+ VelocityPlugin.INSTANCE.getServer().getEventManager().register(VelocityPlugin.INSTANCE, LoginEvent.class,
+ event -> {
+ if(event.getResult().isAllowed()) {
+ if(event.getPlayer().hasPermission("antivpn.bypass") //Has bypass permission
+ || AntiVPN.getInstance().getExecutor().isWhitelisted(event.getPlayer().getUniqueId()) //Is exempt
+ //Or has a name that starts with a certain prefix. This is for Bedrock exempting.
+ || AntiVPN.getInstance().getConfig().getPrefixWhitelists().stream()
+ .anyMatch(prefix -> event.getPlayer().getUsername().startsWith(prefix))) return;
+
+ checkIp(event.getPlayer().getRemoteAddress().getAddress().getHostAddress(),
+ AntiVPN.getInstance().getConfig().cachedResults(), result -> {
+ if(result.isSuccess() && result.isProxy()) {
+ if(AntiVPN.getInstance().getConfig().kickPlayersOnDetect())
+ event.getPlayer().disconnect(LegacyComponentSerializer.builder().character('&')
+ .build().deserialize(AntiVPN.getInstance().getConfig().getKickString()));
+ VelocityPlugin.INSTANCE.getLogger().info(event.getPlayer().getUsername()
+ + " joined on a VPN/Proxy (" + result.getMethod() + ")");
+
+ if(AntiVPN.getInstance().getConfig().alertToStaff()) //Ensuring the user wishes to alert to staff
+ AntiVPN.getInstance().getPlayerExecutor().getOnlinePlayers().stream()
+ .filter(APIPlayer::isAlertsEnabled)
+ .forEach(pl -> pl.sendMessage(AntiVPN.getInstance().getConfig().alertMessage()
+ .replace("%player%", event.getPlayer().getUsername())
+ .replace("%reason%", result.getMethod())
+ .replace("%country%", result.getCountryName())
+ .replace("%city%", result.getCity())));
+
+ //In case the user wants to run their own commands instead of using the built in kicking
+ if(AntiVPN.getInstance().getConfig().runCommands()) {
+ for (String command : AntiVPN.getInstance().getConfig().commands()) {
+ VelocityPlugin.INSTANCE.getServer().getCommandManager()
+ .executeAsync(VelocityPlugin.INSTANCE.getServer()
+ .getConsoleCommandSource(),
+ StringUtils.translateAlternateColorCodes('&',
+ command.replace("%player%",
+ event.getPlayer().getUsername())));
+ }
+ }
+ AntiVPN.getInstance().detections++;
+ } else if(!result.isSuccess()) {
+ VelocityPlugin.INSTANCE.getLogger()
+ .log(Level.WARNING,
+ "The API query was not a success! " +
+ "You may need to upgrade your license on https://funkemunky.cc/shop");
+ }
+ AntiVPN.getInstance().checked++;
+ });
+ }
+ });
+ }
+
+ @Override
+ public void runCacheReset() {
+ cacheResetTask = VelocityPlugin.INSTANCE.getServer().getScheduler()
+ .buildTask(VelocityPlugin.INSTANCE, this::resetCache)
+ .repeat(20, TimeUnit.MINUTES)
+ .schedule();
+ }
+
+ @Override
+ public void shutdown() {
+ if(cacheResetTask != null) {
+ cacheResetTask.cancel();
+ cacheResetTask = null;
+ }
+ threadExecutor.shutdown();
+ VelocityPlugin.INSTANCE.getServer().getEventManager().unregisterListener(VelocityPlugin.INSTANCE, this);
+ }
+}
diff --git a/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityPlayer.java b/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityPlayer.java
new file mode 100644
index 0000000..b247714
--- /dev/null
+++ b/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityPlayer.java
@@ -0,0 +1,31 @@
+package dev.brighten.antivpn.velocity;
+
+import com.velocitypowered.api.proxy.Player;
+import dev.brighten.antivpn.api.APIPlayer;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+
+public class VelocityPlayer extends APIPlayer {
+
+ private final Player player;
+ public VelocityPlayer(Player player) {
+ super(player.getUniqueId(), player.getUsername(), player.getRemoteAddress().getAddress());
+
+ this.player = player;
+ }
+
+
+ @Override
+ public void sendMessage(String message) {
+ player.sendMessage(LegacyComponentSerializer.builder().character('&').build().deserialize(message));
+ }
+
+ @Override
+ public void kickPlayer(String reason) {
+ player.disconnect(LegacyComponentSerializer.builder().character('&').build().deserialize(reason));
+ }
+
+ @Override
+ public boolean hasPermission(String permission) {
+ return player.hasPermission(permission);
+ }
+}
diff --git a/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityPlayerExecutor.java b/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityPlayerExecutor.java
new file mode 100644
index 0000000..c7edcb4
--- /dev/null
+++ b/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityPlayerExecutor.java
@@ -0,0 +1,35 @@
+package dev.brighten.antivpn.velocity;
+
+import com.velocitypowered.api.proxy.Player;
+import dev.brighten.antivpn.api.APIPlayer;
+import dev.brighten.antivpn.api.PlayerExecutor;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class VelocityPlayerExecutor implements PlayerExecutor {
+
+ private final Map cachedPlayers = new WeakHashMap<>();
+
+ @Override
+ public Optional getPlayer(String name) {
+ Optional player = VelocityPlugin.INSTANCE.getServer().getPlayer(name);
+
+ return player.map(value -> cachedPlayers.computeIfAbsent(value, VelocityPlayer::new));
+
+ }
+
+ @Override
+ public Optional getPlayer(UUID uuid) {
+ Optional player = VelocityPlugin.INSTANCE.getServer().getPlayer(uuid);
+
+ return player.map(value -> cachedPlayers.computeIfAbsent(value, VelocityPlayer::new));
+ }
+
+ @Override
+ public List getOnlinePlayers() {
+ return VelocityPlugin.INSTANCE.getServer().getAllPlayers().stream()
+ .map(pl -> cachedPlayers.computeIfAbsent(pl, VelocityPlayer::new))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityPlugin.java b/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityPlugin.java
new file mode 100644
index 0000000..fc941dd
--- /dev/null
+++ b/Velocity/src/main/java/dev/brighten/antivpn/velocity/VelocityPlugin.java
@@ -0,0 +1,93 @@
+package dev.brighten.antivpn.velocity;
+
+import com.google.inject.Inject;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.api.command.SimpleCommand;
+import com.velocitypowered.api.event.Subscribe;
+import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
+import com.velocitypowered.api.plugin.Plugin;
+import com.velocitypowered.api.plugin.annotation.DataDirectory;
+import com.velocitypowered.api.proxy.ProxyServer;
+import dev.brighten.antivpn.AntiVPN;
+import dev.brighten.antivpn.command.Command;
+import dev.brighten.antivpn.velocity.util.Config;
+import lombok.Getter;
+import lombok.val;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.TextColor;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.logging.Logger;
+import java.util.stream.IntStream;
+
+@Getter
+@Plugin(id = "kaurivpn", name = "KauriVPN", version = "${project.version}", authors = {"funkemunky"})
+public class VelocityPlugin {
+
+ private final ProxyServer server;
+ private final Logger logger;
+ public static VelocityPlugin INSTANCE;
+
+ @Inject
+ @DataDirectory
+ private Path configDir;
+
+ private Config config;
+
+ @Inject
+ public VelocityPlugin(ProxyServer server, Logger logger) {
+ this.server = server;
+ this.logger = logger;
+ }
+
+ @Subscribe
+ public void onInit(ProxyInitializeEvent event) {
+ INSTANCE = this;
+ logger.info("Loading config...");
+ config = new Config();
+
+ //Loading plugin
+ logger.info("Starting AntiVPN services...");
+ AntiVPN.start(new VelocityConfig(), new VelocityListener(), new VelocityPlayerExecutor());
+
+ for (Command command : AntiVPN.getInstance().getCommands()) {
+ server.getCommandManager().register(server.getCommandManager().metaBuilder(command.name())
+ .aliases(command.aliases()).build(), (SimpleCommand) invocation -> {
+ CommandSource sender = invocation.source();
+ if(!invocation.source().hasPermission("antivpn.command.*")
+ && !invocation.source().hasPermission(command.permission())) {
+ invocation.source().sendMessage(Component.text("No permission").toBuilder()
+ .color(TextColor.color(255,0,0)).build());
+ return;
+ }
+
+ val children = command.children();
+
+ String[] args = invocation.arguments();
+ if(children.length > 0 && args.length > 0) {
+ for (Command child : children) {
+ if(child.name().equalsIgnoreCase(args[0]) || Arrays.stream(child.aliases())
+ .anyMatch(alias -> alias.equalsIgnoreCase(args[0]))) {
+ if(!sender.hasPermission("antivpn.command.*")
+ && !sender.hasPermission(child.permission())) {
+ invocation.source().sendMessage(Component.text("No permission")
+ .toBuilder().color(TextColor.color(255,0,0)).build());
+ return;
+ }
+ sender.sendMessage(LegacyComponentSerializer.builder().character('&').build()
+ .deserialize(child.execute(new VelocityCommandExecutor(sender), IntStream
+ .range(0, args.length - 1)
+ .mapToObj(i -> args[i + 1]).toArray(String[]::new))));
+ return;
+ }
+ }
+ }
+
+ sender.sendMessage(LegacyComponentSerializer.builder().character('&').build()
+ .deserialize(command.execute(new VelocityCommandExecutor(sender), args)));
+ });
+ }
+ }
+}
diff --git a/Velocity/src/main/java/dev/brighten/antivpn/velocity/config/Configuration.java b/Velocity/src/main/java/dev/brighten/antivpn/velocity/config/Configuration.java
new file mode 100644
index 0000000..f576f9b
--- /dev/null
+++ b/Velocity/src/main/java/dev/brighten/antivpn/velocity/config/Configuration.java
@@ -0,0 +1,414 @@
+package dev.brighten.antivpn.velocity.config;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+
+public final class Configuration
+{
+
+ private static final char SEPARATOR = '.';
+ final Map self;
+ private final Configuration defaults;
+
+ public Configuration()
+ {
+ this( null );
+ }
+
+ public Configuration(Configuration defaults)
+ {
+ this( new LinkedHashMap(), defaults );
+ }
+
+ Configuration(Map, ?> map, Configuration defaults)
+ {
+ this.self = new LinkedHashMap<>();
+ this.defaults = defaults;
+
+ for ( Map.Entry, ?> entry : map.entrySet() )
+ {
+ String key = ( entry.getKey() == null ) ? "null" : entry.getKey().toString();
+
+ if ( entry.getValue() instanceof Map )
+ {
+ this.self.put( key, new Configuration( (Map) entry.getValue(), ( defaults == null ) ? null : defaults.getSection( key ) ) );
+ } else
+ {
+ this.self.put( key, entry.getValue() );
+ }
+ }
+ }
+
+ private Configuration getSectionFor(String path)
+ {
+ int index = path.indexOf( SEPARATOR );
+ if ( index == -1 )
+ {
+ return this;
+ }
+
+ String root = path.substring( 0, index );
+ Object section = self.get( root );
+ if ( section == null )
+ {
+ section = new Configuration( ( defaults == null ) ? null : defaults.getSection( root ) );
+ self.put( root, section );
+ }
+
+ return (Configuration) section;
+ }
+
+ private String getChild(String path)
+ {
+ int index = path.indexOf( SEPARATOR );
+ return ( index == -1 ) ? path : path.substring( index + 1 );
+ }
+
+ /*------------------------------------------------------------------------*/
+ @SuppressWarnings("unchecked")
+ public T get(String path, T def)
+ {
+ Configuration section = getSectionFor( path );
+ Object val;
+ if ( section == this )
+ {
+ val = self.get( path );
+ } else
+ {
+ val = section.get( getChild( path ), def );
+ }
+
+ if ( val == null && def instanceof Configuration )
+ {
+ self.put( path, def );
+ }
+
+ return ( val != null ) ? (T) val : def;
+ }
+
+ public boolean contains(String path)
+ {
+ return get( path, null ) != null;
+ }
+
+ public Object get(String path)
+ {
+ return get( path, getDefault( path ) );
+ }
+
+ public Object getDefault(String path)
+ {
+ return ( defaults == null ) ? null : defaults.get( path );
+ }
+
+ public void set(String path, Object value)
+ {
+ if ( value instanceof Map )
+ {
+ value = new Configuration( (Map) value, ( defaults == null ) ? null : defaults.getSection( path ) );
+ }
+
+ Configuration section = getSectionFor( path );
+ if ( section == this )
+ {
+ if ( value == null )
+ {
+ self.remove( path );
+ } else
+ {
+ self.put( path, value );
+ }
+ } else
+ {
+ section.set( getChild( path ), value );
+ }
+ }
+
+ /*------------------------------------------------------------------------*/
+ public Configuration getSection(String path)
+ {
+ Object def = getDefault( path );
+ return (Configuration) get( path, ( def instanceof Configuration ) ? def : new Configuration( ( defaults == null ) ? null : defaults.getSection( path ) ) );
+ }
+
+ /**
+ * Gets keys, not deep by default.
+ *
+ * @return top level keys for this section
+ */
+ public Collection getKeys()
+ {
+ return new LinkedHashSet<>( self.keySet() );
+ }
+
+ /*------------------------------------------------------------------------*/
+ public byte getByte(String path)
+ {
+ Object def = getDefault( path );
+ return getByte( path, ( def instanceof Number ) ? ( (Number) def ).byteValue() : 0 );
+ }
+
+ public byte getByte(String path, byte def)
+ {
+ Object val = get( path, def );
+ return ( val instanceof Number ) ? ( (Number) val ).byteValue() : def;
+ }
+
+ public List getByteList(String path)
+ {
+ List> list = getList( path );
+ List result = new ArrayList<>();
+
+ for ( Object object : list )
+ {
+ if ( object instanceof Number )
+ {
+ result.add( ( (Number) object ).byteValue() );
+ }
+ }
+
+ return result;
+ }
+
+ public short getShort(String path)
+ {
+ Object def = getDefault( path );
+ return getShort( path, ( def instanceof Number ) ? ( (Number) def ).shortValue() : 0 );
+ }
+
+ public short getShort(String path, short def)
+ {
+ Object val = get( path, def );
+ return ( val instanceof Number ) ? ( (Number) val ).shortValue() : def;
+ }
+
+ public List getShortList(String path)
+ {
+ List> list = getList( path );
+ List result = new ArrayList<>();
+
+ for ( Object object : list )
+ {
+ if ( object instanceof Number )
+ {
+ result.add( ( (Number) object ).shortValue() );
+ }
+ }
+
+ return result;
+ }
+
+ public int getInt(String path)
+ {
+ Object def = getDefault( path );
+ return getInt( path, ( def instanceof Number ) ? ( (Number) def ).intValue() : 0 );
+ }
+
+ public int getInt(String path, int def)
+ {
+ Object val = get( path, def );
+ return ( val instanceof Number ) ? ( (Number) val ).intValue() : def;
+ }
+
+ public List getIntList(String path)
+ {
+ List> list = getList( path );
+ List result = new ArrayList<>();
+
+ for ( Object object : list )
+ {
+ if ( object instanceof Number )
+ {
+ result.add( ( (Number) object ).intValue() );
+ }
+ }
+
+ return result;
+ }
+
+ public long getLong(String path)
+ {
+ Object def = getDefault( path );
+ return getLong( path, ( def instanceof Number ) ? ( (Number) def ).longValue() : 0 );
+ }
+
+ public long getLong(String path, long def)
+ {
+ Object val = get( path, def );
+ return ( val instanceof Number ) ? ( (Number) val ).longValue() : def;
+ }
+
+ public List getLongList(String path)
+ {
+ List> list = getList( path );
+ List result = new ArrayList<>();
+
+ for ( Object object : list )
+ {
+ if ( object instanceof Number )
+ {
+ result.add( ( (Number) object ).longValue() );
+ }
+ }
+
+ return result;
+ }
+
+ public float getFloat(String path)
+ {
+ Object def = getDefault( path );
+ return getFloat( path, ( def instanceof Number ) ? ( (Number) def ).floatValue() : 0 );
+ }
+
+ public float getFloat(String path, float def)
+ {
+ Object val = get( path, def );
+ return ( val instanceof Number ) ? ( (Number) val ).floatValue() : def;
+ }
+
+ public List getFloatList(String path)
+ {
+ List> list = getList( path );
+ List result = new ArrayList<>();
+
+ for ( Object object : list )
+ {
+ if ( object instanceof Number )
+ {
+ result.add( ( (Number) object ).floatValue() );
+ }
+ }
+
+ return result;
+ }
+
+ public double getDouble(String path)
+ {
+ Object def = getDefault( path );
+ return getDouble( path, ( def instanceof Number ) ? ( (Number) def ).doubleValue() : 0 );
+ }
+
+ public double getDouble(String path, double def)
+ {
+ Object val = get( path, def );
+ return ( val instanceof Number ) ? ( (Number) val ).doubleValue() : def;
+ }
+
+ public List getDoubleList(String path)
+ {
+ List> list = getList( path );
+ List result = new ArrayList<>();
+
+ for ( Object object : list )
+ {
+ if ( object instanceof Number )
+ {
+ result.add( ( (Number) object ).doubleValue() );
+ }
+ }
+
+ return result;
+ }
+
+ public boolean getBoolean(String path)
+ {
+ Object def = getDefault( path );
+ return getBoolean( path, ( def instanceof Boolean ) ? (Boolean) def : false );
+ }
+
+ public boolean getBoolean(String path, boolean def)
+ {
+ Object val = get( path, def );
+ return ( val instanceof Boolean ) ? (Boolean) val : def;
+ }
+
+ public List getBooleanList(String path)
+ {
+ List> list = getList( path );
+ List result = new ArrayList<>();
+
+ for ( Object object : list )
+ {
+ if ( object instanceof Boolean )
+ {
+ result.add( (Boolean) object );
+ }
+ }
+
+ return result;
+ }
+
+ public char getChar(String path)
+ {
+ Object def = getDefault( path );
+ return getChar( path, ( def instanceof Character ) ? (Character) def : '\u0000' );
+ }
+
+ public char getChar(String path, char def)
+ {
+ Object val = get( path, def );
+ return ( val instanceof Character ) ? (Character) val : def;
+ }
+
+ public List getCharList(String path)
+ {
+ List> list = getList( path );
+ List result = new ArrayList<>();
+
+ for ( Object object : list )
+ {
+ if ( object instanceof Character )
+ {
+ result.add( (Character) object );
+ }
+ }
+
+ return result;
+ }
+
+ public String getString(String path)
+ {
+ Object def = getDefault( path );
+ return getString( path, ( def instanceof String ) ? (String) def : "" );
+ }
+
+ public String getString(String path, String def)
+ {
+ Object val = get( path, def );
+ return ( val instanceof String ) ? (String) val : def;
+ }
+
+ public List getStringList(String path)
+ {
+ List> list = getList( path );
+ List result = new ArrayList<>();
+
+ for ( Object object : list )
+ {
+ if ( object instanceof String )
+ {
+ result.add( (String) object );
+ }
+ }
+
+ return result;
+ }
+
+ /*------------------------------------------------------------------------*/
+ public List> getList(String path)
+ {
+ Object def = getDefault( path );
+ return getList( path, ( def instanceof List> ) ? (List>) def : Collections.EMPTY_LIST );
+ }
+
+ public List> getList(String path, List> def)
+ {
+ Object val = get( path, def );
+ return ( val instanceof List> ) ? (List>) val : def;
+ }
+}
\ No newline at end of file
diff --git a/Velocity/src/main/java/dev/brighten/antivpn/velocity/config/ConfigurationProvider.java b/Velocity/src/main/java/dev/brighten/antivpn/velocity/config/ConfigurationProvider.java
new file mode 100644
index 0000000..cf5189e
--- /dev/null
+++ b/Velocity/src/main/java/dev/brighten/antivpn/velocity/config/ConfigurationProvider.java
@@ -0,0 +1,60 @@
+package dev.brighten.antivpn.velocity.config;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class ConfigurationProvider
+{
+
+ private static final Map, ConfigurationProvider> providers = new HashMap<>();
+
+ static
+ {
+ try
+ {
+ providers.put( YamlConfiguration.class, new YamlConfiguration() );
+ } catch ( NoClassDefFoundError ex )
+ {
+ // Ignore, no SnakeYAML
+ }
+
+ try
+ {
+ providers.put( JsonConfiguration.class, new JsonConfiguration() );
+ } catch ( NoClassDefFoundError ex )
+ {
+ // Ignore, no Gson
+ }
+ }
+
+ public static ConfigurationProvider getProvider(Class extends ConfigurationProvider> provider)
+ {
+ return providers.get( provider );
+ }
+
+ /*------------------------------------------------------------------------*/
+ public abstract void save(Configuration config, File file) throws IOException;
+
+ public abstract void save(Configuration config, Writer writer);
+
+ public abstract Configuration load(File file) throws IOException;
+
+ public abstract Configuration load(File file, Configuration defaults) throws IOException;
+
+ public abstract Configuration load(Reader reader);
+
+ public abstract Configuration load(Reader reader, Configuration defaults);
+
+ public abstract Configuration load(InputStream is);
+
+ public abstract Configuration load(InputStream is, Configuration defaults);
+
+ public abstract Configuration load(String string);
+
+ public abstract Configuration load(String string, Configuration defaults);
+}
\ No newline at end of file
diff --git a/Velocity/src/main/java/dev/brighten/antivpn/velocity/config/JsonConfiguration.java b/Velocity/src/main/java/dev/brighten/antivpn/velocity/config/JsonConfiguration.java
new file mode 100644
index 0000000..83ea436
--- /dev/null
+++ b/Velocity/src/main/java/dev/brighten/antivpn/velocity/config/JsonConfiguration.java
@@ -0,0 +1,114 @@
+package dev.brighten.antivpn.velocity.config;
+
+import com.google.common.base.Charsets;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.lang.reflect.Type;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor(access = AccessLevel.PACKAGE)
+public class JsonConfiguration extends ConfigurationProvider
+{
+
+ private final Gson json = new GsonBuilder().serializeNulls().setPrettyPrinting().registerTypeAdapter( Configuration.class, new JsonSerializer()
+ {
+ @Override
+ public JsonElement serialize(Configuration src, Type typeOfSrc, JsonSerializationContext context)
+ {
+ return context.serialize( ( (Configuration) src ).self );
+ }
+ } ).create();
+
+ @Override
+ public void save(Configuration config, File file) throws IOException
+ {
+ try ( Writer writer = new OutputStreamWriter( new FileOutputStream( file ), Charsets.UTF_8 ) )
+ {
+ save( config, writer );
+ }
+ }
+
+ @Override
+ public void save(Configuration config, Writer writer)
+ {
+ json.toJson( config.self, writer );
+ }
+
+ @Override
+ public Configuration load(File file) throws IOException
+ {
+ return load( file, null );
+ }
+
+ @Override
+ public Configuration load(File file, Configuration defaults) throws IOException
+ {
+ try ( FileInputStream is = new FileInputStream( file ) )
+ {
+ return load( is, defaults );
+ }
+ }
+
+ @Override
+ public Configuration load(Reader reader)
+ {
+ return load( reader, null );
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Configuration load(Reader reader, Configuration defaults)
+ {
+ Map map = json.fromJson( reader, LinkedHashMap.class );
+ if ( map == null )
+ {
+ map = new LinkedHashMap<>();
+ }
+ return new Configuration( map, defaults );
+ }
+
+ @Override
+ public Configuration load(InputStream is)
+ {
+ return load( is, null );
+ }
+
+ @Override
+ public Configuration load(InputStream is, Configuration defaults)
+ {
+ return load( new InputStreamReader( is, Charsets.UTF_8 ), defaults );
+ }
+
+ @Override
+ public Configuration load(String string)
+ {
+ return load( string, null );
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Configuration load(String string, Configuration defaults)
+ {
+ Map map = json.fromJson( string, LinkedHashMap.class );
+ if ( map == null )
+ {
+ map = new LinkedHashMap<>();
+ }
+ return new Configuration( map, defaults );
+ }
+}
\ No newline at end of file
diff --git a/Velocity/src/main/java/dev/brighten/antivpn/velocity/config/YamlConfiguration.java b/Velocity/src/main/java/dev/brighten/antivpn/velocity/config/YamlConfiguration.java
new file mode 100644
index 0000000..b69584a
--- /dev/null
+++ b/Velocity/src/main/java/dev/brighten/antivpn/velocity/config/YamlConfiguration.java
@@ -0,0 +1,136 @@
+package dev.brighten.antivpn.velocity.config;
+
+import com.google.common.base.Charsets;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.Constructor;
+import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.representer.Represent;
+import org.yaml.snakeyaml.representer.Representer;
+
+@NoArgsConstructor(access = AccessLevel.PACKAGE)
+public class YamlConfiguration extends ConfigurationProvider
+{
+
+ private final ThreadLocal yaml = new ThreadLocal()
+ {
+ @Override
+ protected Yaml initialValue()
+ {
+ Representer representer = new Representer()
+ {
+ {
+ representers.put( Configuration.class, new Represent()
+ {
+ @Override
+ public Node representData(Object data)
+ {
+ return represent( ( (Configuration) data ).self );
+ }
+ } );
+ }
+ };
+
+ DumperOptions options = new DumperOptions();
+ options.setDefaultFlowStyle( DumperOptions.FlowStyle.BLOCK );
+
+ return new Yaml( new Constructor(), representer, options );
+ }
+ };
+
+ @Override
+ public void save(Configuration config, File file) throws IOException
+ {
+ try ( Writer writer = new OutputStreamWriter( new FileOutputStream( file ), Charsets.UTF_8 ) )
+ {
+ save( config, writer );
+ }
+ }
+
+ @Override
+ public void save(Configuration config, Writer writer)
+ {
+ yaml.get().dump( config.self, writer );
+ }
+
+ @Override
+ public Configuration load(File file) throws IOException
+ {
+ return load( file, null );
+ }
+
+ @Override
+ public Configuration load(File file, Configuration defaults) throws IOException
+ {
+ try ( FileInputStream is = new FileInputStream( file ) )
+ {
+ return load( is, defaults );
+ }
+ }
+
+ @Override
+ public Configuration load(Reader reader)
+ {
+ return load( reader, null );
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Configuration load(Reader reader, Configuration defaults)
+ {
+ Map map = yaml.get().loadAs( reader, LinkedHashMap.class );
+ if ( map == null )
+ {
+ map = new LinkedHashMap<>();
+ }
+ return new Configuration( map, defaults );
+ }
+
+ @Override
+ public Configuration load(InputStream is)
+ {
+ return load( is, null );
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Configuration load(InputStream is, Configuration defaults)
+ {
+ Map map = yaml.get().loadAs( is, LinkedHashMap.class );
+ if ( map == null )
+ {
+ map = new LinkedHashMap<>();
+ }
+ return new Configuration( map, defaults );
+ }
+
+ @Override
+ public Configuration load(String string)
+ {
+ return load( string, null );
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Configuration load(String string, Configuration defaults)
+ {
+ Map map = yaml.get().loadAs( string, LinkedHashMap.class );
+ if ( map == null )
+ {
+ map = new LinkedHashMap<>();
+ }
+ return new Configuration( map, defaults );
+ }
+}
\ No newline at end of file
diff --git a/Velocity/src/main/java/dev/brighten/antivpn/velocity/util/Config.java b/Velocity/src/main/java/dev/brighten/antivpn/velocity/util/Config.java
new file mode 100644
index 0000000..39fa261
--- /dev/null
+++ b/Velocity/src/main/java/dev/brighten/antivpn/velocity/util/Config.java
@@ -0,0 +1,115 @@
+package dev.brighten.antivpn.velocity.util;
+
+import com.google.common.io.ByteStreams;
+import dev.brighten.antivpn.velocity.VelocityPlugin;
+import dev.brighten.antivpn.velocity.config.Configuration;
+import dev.brighten.antivpn.velocity.config.ConfigurationProvider;
+import dev.brighten.antivpn.velocity.config.YamlConfiguration;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Author: nitramleo (Martin)
+ * Date created: 10-Aug-18
+ */
+public class Config {
+
+ private File file;
+ private Configuration configuration;
+
+ public Config() {
+ File dataFolder = VelocityPlugin.INSTANCE.getConfigDir().toFile();
+ this.file = new File(dataFolder, "config.yml");
+ try {
+ if (!this.file.exists()) {
+ if (!dataFolder.exists()) {
+ dataFolder.mkdir();
+ }
+ this.file.createNewFile();
+ try (final InputStream is =VelocityPlugin.INSTANCE.getClass().getResourceAsStream("config.yml");
+ final OutputStream os = new FileOutputStream(this.file)) {
+ ByteStreams.copy(is, os);
+ }
+ }
+ this.configuration = ConfigurationProvider.getProvider(YamlConfiguration.class).load(this.file);
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void load() {
+ File dataFolder = VelocityPlugin.INSTANCE.getConfigDir().toFile();
+ this.file = new File(dataFolder, "config.yml");
+ try {
+ this.configuration = ConfigurationProvider.getProvider(YamlConfiguration.class).load(this.file);
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void save() {
+ try {
+ ConfigurationProvider.getProvider( YamlConfiguration.class).save(this.configuration, this.file);
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public Configuration getConfiguration() {
+ return this.configuration;
+ }
+
+ public File getFile() {
+ return this.file;
+ }
+
+ public double getDouble(final String path) {
+ if (this.configuration.get(path) != null) {
+ return this.configuration.getDouble(path);
+ }
+ return 0.0;
+ }
+
+ public int getInt(final String path) {
+ if (this.configuration.get(path) != null) {
+ return this.configuration.getInt(path);
+ }
+ return 0;
+ }
+
+ public Object get(final String path) {
+ return this.configuration.get(path);
+ }
+
+ public void set(final String path, final Object object) {
+ configuration.set(path, object);
+ }
+
+ public boolean getBoolean(final String path) {
+ return this.configuration.get(path) != null && this.configuration.getBoolean(path);
+ }
+
+ public String getString(final String path) {
+ if (this.configuration.get(path) != null) {
+ return StringUtils.translateAlternateColorCodes('&', this.configuration.getString(path));
+ }
+ return "String at path: " + path + " not found!";
+ }
+
+ public List getStringList(final String path) {
+ if (this.configuration.get(path) != null) {
+ final ArrayList strings = new ArrayList();
+ for (final String string : this.configuration.getStringList(path)) {
+ strings.add(StringUtils.translateAlternateColorCodes('&', string));
+ }
+ return strings;
+ }
+ return Arrays.asList("String List at path: " + path + " not found!");
+ }
+}
diff --git a/Velocity/src/main/java/dev/brighten/antivpn/velocity/util/ConfigDefault.java b/Velocity/src/main/java/dev/brighten/antivpn/velocity/util/ConfigDefault.java
new file mode 100644
index 0000000..16c6006
--- /dev/null
+++ b/Velocity/src/main/java/dev/brighten/antivpn/velocity/util/ConfigDefault.java
@@ -0,0 +1,28 @@
+package dev.brighten.antivpn.velocity.util;
+
+import dev.brighten.antivpn.velocity.VelocityPlugin;
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+public class ConfigDefault {
+
+ private final A defaultValue;
+ private final String path;
+ private final VelocityPlugin plugin;
+
+ public A get() {
+ if(plugin.getConfig().get(path) != null)
+ return (A) plugin.getConfig().get(path);
+ else {
+ plugin.getConfig().set(path, defaultValue);
+ plugin.getConfig().save();
+ return defaultValue;
+ }
+ }
+
+ public A set(A value) {
+ plugin.getConfig().set(path, value);
+
+ return value;
+ }
+}
diff --git a/Velocity/src/main/java/dev/brighten/antivpn/velocity/util/StringUtils.java b/Velocity/src/main/java/dev/brighten/antivpn/velocity/util/StringUtils.java
new file mode 100644
index 0000000..a4f02fe
--- /dev/null
+++ b/Velocity/src/main/java/dev/brighten/antivpn/velocity/util/StringUtils.java
@@ -0,0 +1,17 @@
+package dev.brighten.antivpn.velocity.util;
+
+public class StringUtils {
+
+ public static String translateAlternateColorCodes(char altColorChar, String textToTranslate) {
+ char[] b = textToTranslate.toCharArray();
+
+ for(int i = 0; i < b.length - 1; ++i) {
+ if (b[i] == altColorChar && "0123456789AaBbCcDdEeFfKkLlMmNnOoRr".indexOf(b[i + 1]) > -1) {
+ b[i] = 167;
+ b[i + 1] = Character.toLowerCase(b[i + 1]);
+ }
+ }
+
+ return new String(b);
+ }
+}
diff --git a/pom.xml b/pom.xml
index e4810a7..cfba9fa 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,13 +7,14 @@
dev.brighten.antivpn
AntiVPN
pom
- 1.4.0
+ 1.5.0
Common
Bungee
Bukkit
Assembly
+ Velocity