diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c65bcf4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,115 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +target/ + +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next + +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.flattened-pom.xml + +# Common working directory +run/ + +.junie diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..7c6b218 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip diff --git a/api/pom.xml b/api/pom.xml new file mode 100644 index 0000000..df3ad23 --- /dev/null +++ b/api/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + + org.btuk + Proxy + 1.13.0-SNAPSHOT + + + org.btuk.proxy + api + Proxy API + + + UTF-8 + 7.4.0 + 3.6.0 + 3.1.0 + 3.1.5 + 4.0.2 + + + + + org.btuk.proxy + database + + + + org.btuk.proxy + core + + + + org.projectlombok + lombok + + + + jakarta.ws.rs + jakarta.ws.rs-api + ${jakarta-ws-rs-api.version} + + + jakarta.annotation + jakarta.annotation-api + 2.1.1 + provided + + + + + org.glassfish.jersey.containers + jersey-container-grizzly2-http + ${jersey.version} + + + org.glassfish.jersey.inject + jersey-hk2 + ${jersey.version} + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${jersey.version} + + + + + + + org.openapitools + openapi-generator-maven-plugin + ${openapi-generator.version} + + + + generate + + + ${project.basedir}/src/main/resources/openapi.yaml + jaxrs-spec + org.btuk.proxy.api + org.btuk.proxy.api.model + + true + true + true + true + false + false + false + false + false + + false + + + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + add-source + generate-sources + + add-source + + + + ${project.build.directory}/generated-sources/openapi/src/gen/java + + + + + + + + diff --git a/api/src/main/java/org/btuk/proxy/api/impl/BuildingsApiImpl.java b/api/src/main/java/org/btuk/proxy/api/impl/BuildingsApiImpl.java new file mode 100644 index 0000000..362f123 --- /dev/null +++ b/api/src/main/java/org/btuk/proxy/api/impl/BuildingsApiImpl.java @@ -0,0 +1,106 @@ +package org.btuk.proxy.api.impl; + +import jakarta.inject.Inject; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import org.btuk.proxy.api.BuildingsApi; +import org.btuk.proxy.api.model.Building; +import org.btuk.proxy.api.model.BuildingCount; +import org.btuk.proxy.api.model.BuildingGridResponse; +import org.btuk.proxy.api.model.GridCell; +import org.btuk.proxy.database.dto.BuildingDTO; +import org.btuk.proxy.database.dto.GridCellDTO; +import org.btuk.proxy.database.sql.GlobalSQL; + +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Path("/buildings") +public class BuildingsApiImpl implements BuildingsApi { + + private final GlobalSQL globalSQL; + + @Inject + public BuildingsApiImpl(GlobalSQL globalSQL) { + this.globalSQL = globalSQL; + } + + @Override + public Response getBuildingsByArea(Double minLat, Double maxLat, Double minLon, Double maxLon, UUID playerUuid) { + String playerUuidStr = playerUuid != null ? playerUuid.toString() : null; + List buildingDTOs = globalSQL.getBuildingsByArea(minLat, maxLat, minLon, maxLon, playerUuidStr); + + List buildings = buildingDTOs.stream() + .map(this::mapBuilding) + .collect(Collectors.toList()); + return Response.ok(buildings).build(); + } + + @Override + public Response getBuildingCount(List playerUuid, Double minLat, Double maxLat, Double minLon, Double maxLon, Boolean isPublic, Boolean playerBuilt) { + List playerUuidStrs = null; + if (playerUuid != null && !playerUuid.isEmpty()) { + playerUuidStrs = playerUuid.stream() + .map(UUID::toString) + .collect(Collectors.toList()); + } + + int count = globalSQL.getBuildingCount(playerUuidStrs, minLat, maxLat, minLon, maxLon, isPublic, playerBuilt); + + BuildingCount buildingCount = new BuildingCount(); + buildingCount.setCount(count); + + return Response.ok(buildingCount).build(); + } + + @Override + public Response getBuildingGridCount(Double minLat, Double maxLat, Double minLon, Double maxLon, Double stepLat, Double stepLon, UUID playerUuid) { + if (stepLat == null || stepLat <= 0 || stepLon == null || stepLon <= 0) { + return Response.status(Response.Status.BAD_REQUEST).entity("stepLat and stepLon must be greater than 0").build(); + } + + String playerUuidStr = (playerUuid != null) ? playerUuid.toString() : null; + + List dbGridCounts = globalSQL.getBuildingGridCounts(minLat, maxLat, minLon, maxLon, stepLat, stepLon, playerUuidStr); + + List cells = new ArrayList<>(); + + for (GridCellDTO dbCell : dbGridCounts) { + GridCell cell = new GridCell(); + cell.setLat(dbCell.lat()); + cell.setLon(dbCell.lon()); + cell.setMinLat(dbCell.minLat()); + cell.setMaxLat(dbCell.maxLat()); + cell.setMinLon(dbCell.minLon()); + cell.setMaxLon(dbCell.maxLon()); + cell.setRow(dbCell.row()); + cell.setCol(dbCell.col()); + cell.setCount(dbCell.count()); + cells.add(cell); + } + + BuildingGridResponse response = new BuildingGridResponse(); + response.setCells(cells); + + return Response.ok(response).build(); + } + + private Building mapBuilding(BuildingDTO dto) { + Building building = new Building(); + building.setBuildingId(dto.buildingId()); + building.setPlayerName(dto.playerName()); + building.setPlayerId(dto.playerId() != null ? UUID.fromString(dto.playerId()) : null); + building.setIsPublic(dto.isPublic()); + building.setPlayerBuilt(dto.playerBuilt()); + if (dto.timeAdded() != null) { + building.setTimeAdded(Date.from(dto.timeAdded().toInstant(ZoneOffset.UTC))); + } + building.setLat(dto.lat()); + building.setLon(dto.lon()); + return building; + } +} diff --git a/api/src/main/java/org/btuk/proxy/api/impl/PlayerApiImpl.java b/api/src/main/java/org/btuk/proxy/api/impl/PlayerApiImpl.java new file mode 100644 index 0000000..dcf0e2e --- /dev/null +++ b/api/src/main/java/org/btuk/proxy/api/impl/PlayerApiImpl.java @@ -0,0 +1,81 @@ +package org.btuk.proxy.api.impl; + +import jakarta.inject.Inject; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import org.btuk.network.lib.dto.DirectMessage; +import org.btuk.network.lib.utils.ChatUtils; +import org.btuk.proxy.api.PlayerApi; +import org.btuk.proxy.api.model.Message; +import org.btuk.proxy.api.model.Player; +import org.btuk.proxy.core.chat.ChatManager; +import org.btuk.proxy.database.dto.PlayerDTO; +import org.btuk.proxy.database.sql.GlobalSQL; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.btuk.network.lib.enums.ChatChannels.GLOBAL; +import static org.btuk.proxy.core.utils.Constants.SERVER_SENDER; +@Path("/player") +public class PlayerApiImpl implements PlayerApi { + + private final GlobalSQL globalSQL; + private final ChatManager chatManager; + + @Inject + public PlayerApiImpl(GlobalSQL globalSQL, ChatManager chatManager) { + this.globalSQL = globalSQL;this.chatManager = chatManager; + } + + @Override + public Response getOnlinePlayers() { + List onlinePlayers = globalSQL.getOnlinePlayers(); + List players = onlinePlayers.stream() + .map(dto -> { + Player p = new Player(); + p.setUuid(UUID.fromString(dto.uuid())); + p.setName(dto.name()); + return p; + }) + .collect(Collectors.toList()); + return Response.ok(players).build(); + } + + @Override + public Response getPlayerUuid(String name) { + String uuid = globalSQL.getPlayerUuidByName(name); + if (uuid == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(UUID.fromString(uuid)).build(); + } + + @Override + public Response getPlayerUsername(String uuid) { + String username = globalSQL.getPlayerUsernameByUuid(uuid); + if (uuid == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(username).build(); + } + + @Override + public Response sendPlayerMessage(String playerID, Message message){ + + try { + String messagePlainText = message.getMessage(); + DirectMessage m = new DirectMessage(GLOBAL.getChannelName(), playerID, SERVER_SENDER, ChatUtils.success(messagePlainText), true); + chatManager.sendDirectMessage(m); + return Response.status(Response.Status.CREATED).build(); + }catch (Exception e) + { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Failed to deliver message to player.") + .build(); + } + + + } +} diff --git a/api/src/main/java/org/btuk/proxy/api/impl/StatusApiImpl.java b/api/src/main/java/org/btuk/proxy/api/impl/StatusApiImpl.java new file mode 100644 index 0000000..6326ba6 --- /dev/null +++ b/api/src/main/java/org/btuk/proxy/api/impl/StatusApiImpl.java @@ -0,0 +1,17 @@ +package org.btuk.proxy.api.impl; + +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import org.btuk.proxy.api.StatusApi; +import org.btuk.proxy.api.model.Status; + +@Path("/status") +public class StatusApiImpl implements StatusApi { + + @Override + public Response getStatus() { + Status status = new Status(); + status.setStatus("UP"); + return Response.ok(status).build(); + } +} diff --git a/api/src/main/java/org/btuk/proxy/api/server/ProxyApi.java b/api/src/main/java/org/btuk/proxy/api/server/ProxyApi.java new file mode 100644 index 0000000..5723b78 --- /dev/null +++ b/api/src/main/java/org/btuk/proxy/api/server/ProxyApi.java @@ -0,0 +1,90 @@ +package org.btuk.proxy.api.server; + +import lombok.extern.java.Log; +import org.btuk.proxy.api.impl.BuildingsApiImpl; +import org.btuk.proxy.api.impl.PlayerApiImpl; +import org.btuk.proxy.api.impl.StatusApiImpl; +import org.btuk.proxy.core.chat.ChatManager; +import org.btuk.proxy.database.sql.GlobalSQL; + +import org.glassfish.grizzly.http.server.HttpServer; +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.server.ResourceConfig; + +import java.net.URI; + +@Log +public class ProxyApi { + + private HttpServer server; + private final boolean enabled; + private final int port; + private final GlobalSQL globalSQL; + private final ChatManager chatManager; + + + public ProxyApi(boolean enabled, int port, GlobalSQL globalSQL, ChatManager chatManager) { + this.enabled = enabled; + this.port = port; + this.globalSQL = globalSQL; + this.chatManager = chatManager; + } + + public void start() { + if (!enabled) { + return; + } + + int apiPort = port; + if (apiPort == 0) { + log.warning("API port is not set or 0, defaulting to 8080"); + apiPort = 8080; + } + + String baseUri = "http://0.0.0.0:" + apiPort + "/api/"; + + + ResourceConfig rc = new ResourceConfig(); + + // 1. Disable WADL warning + rc.property("jersey.config.server.wadl.disableWadl", true); + + // 2. Bind SQL and ChatManager dependencies for injection + rc.register(new AbstractBinder() { + @Override + protected void configure() { + bind(globalSQL).to(GlobalSQL.class); + bind(chatManager).to(ChatManager.class); + } + }); + + // 3. Register the implementation CLASSES (or scan the package) + rc.register(StatusApiImpl.class); + rc.register(PlayerApiImpl.class); + rc.register(BuildingsApiImpl.class); + +// ResourceConfig rc = new ResourceConfig() +// .property("jersey.config.server.wadl.disableWadl", true) +// .packages("org.btuk.proxy.api.impl"); + +// ResourceConfig rc = new ResourceConfig() +// .register(new StatusApiImpl()) +// .register(new PlayerApiImpl(globalSQL,chatManager)) +// .register(new BuildingsApiImpl(globalSQL)); + + try { + server = GrizzlyHttpServerFactory.createHttpServer(URI.create(baseUri), rc); + log.info("API server started at " + baseUri); + } catch (Exception e) { + log.severe("Failed to start API server: " + e.getMessage()); + } + } + + public void stop() { + if (server != null && server.isStarted()) { + server.shutdownNow(); + log.info("API server stopped"); + } + } +} diff --git a/api/src/main/resources/openapi.yaml b/api/src/main/resources/openapi.yaml new file mode 100644 index 0000000..3eb16fe --- /dev/null +++ b/api/src/main/resources/openapi.yaml @@ -0,0 +1,370 @@ +openapi: 3.0.3 +info: + title: Proxy API + description: Lightweight API for Proxy internal communication. + version: 1.0.0 +servers: + - url: http://172.18.0.1:51101/api +paths: + /status: + get: + tags: + - Status + summary: Get API status + operationId: getStatus + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /player/uuid/{name}: + get: + tags: + - Player + summary: Get player uuid by name + operationId: getPlayerUuid + parameters: + - name: name + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: string + format: uuid + '404': + description: Player not found + /player/username/{uuid}: + get: + tags: + - Player + summary: Get player username by uuid + operationId: getPlayerUsername + parameters: + - name: uuid + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: string + '404': + description: Player not found + /player/message/{uuid}: + post: + tags: + - Player + summary: Send a message to the player on the server + operationId: sendPlayerMessage + parameters: + - name: uuid + in: path + required: true + schema: + type: string + requestBody: + description: The message to send and the player to send to + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + responses: + "201": + description: Message Added + /player/online: + get: + tags: + - Player + summary: Get online players + operationId: getOnlinePlayers + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Player' + /buildings/area: + get: + tags: + - Buildings + summary: Get buildings within an area with privacy filtering + description: Returns full building objects within a bounding box. Excludes private buildings unless they belong to the specified `playerUuid`. + operationId: getBuildingsByArea + parameters: + - name: minLat + in: query + required: true + schema: + type: number + format: double + - name: maxLat + in: query + required: true + schema: + type: number + format: double + - name: minLon + in: query + required: true + schema: + type: number + format: double + - name: maxLon + in: query + required: true + schema: + type: number + format: double + - name: playerUuid + in: query + required: false + description: Context player UUID. If provided, includes private buildings owned by this player. + schema: + type: string + format: uuid + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Building' + + /buildings/count: + get: + tags: + - Buildings + summary: Get building counts with filters + description: Returns the total count of buildings matching optional area, player, visibility, and source filters. + operationId: getBuildingCount + parameters: + - name: playerUuid + in: query + required: false + description: Filter by one or multiple player UUIDs. Repeat parameter for multiple (`?playerUuid=...&playerUuid=...`). + style: form + explode: true + schema: + type: array + items: + type: string + format: uuid + - name: minLat + in: query + required: false + schema: + type: number + format: double + - name: maxLat + in: query + required: false + schema: + type: number + format: double + - name: minLon + in: query + required: false + schema: + type: number + format: double + - name: maxLon + in: query + required: false + schema: + type: number + format: double + - name: isPublic + in: query + required: false + description: Filter specifically for public or private buildings. + schema: + type: boolean + - name: playerBuilt + in: query + required: false + description: Filter by player-built status. + schema: + type: boolean + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/BuildingCount' + + /buildings/grid-count: + get: + tags: + - Buildings + summary: Get aggregated building counts mapped over a grid + description: Divides a bounding box into a grid of cells and returns building counts per cell. Automatically filters out private buildings unless owned by the requesting player. + operationId: getBuildingGridCount + parameters: + - name: minLat + in: query + required: true + schema: + type: number + format: double + - name: maxLat + in: query + required: true + schema: + type: number + format: double + - name: minLon + in: query + required: true + schema: + type: number + format: double + - name: maxLon + in: query + required: true + schema: + type: number + format: double + - name: stepLat + in: query + required: true + description: Number of grid divisions along latitude. + schema: + type: number + format: double + - name: stepLon + in: query + required: true + description: Number of grid divisions along longitude. + schema: + type: number + format: double + - name: playerUuid + in: query + required: false + description: Context player UUID to include their own private buildings in the cell counts. + schema: + type: string + format: uuid + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/BuildingGridResponse' + +components: + schemas: + Status: + type: object + properties: + status: + type: string + example: UP + Player: + type: object + properties: + uuid: + type: string + format: uuid + name: + type: string + Message: + type: object + properties: + message: + type: string + Building: + type: object + properties: + buildingId: + type: integer + playerName: + type: string + playerId: + type: string + format: uuid + isPublic: + type: boolean + playerBuilt: + type: boolean + timeAdded: + type: string + format: date-time + lat: + type: number + format: double + lon: + type: number + format: double + BuildingCount: + type: object + properties: + count: + type: integer + example: 42 + + GridCell: + type: object + properties: + lat: + type: number + format: double + lon: + type: number + format: double + maxLat: + type: number + format: double + maxLon: + type: number + format: double + minLat: + type: number + format: double + minLon: + type: number + format: double + row: + type: integer + col: + type: integer + count: + type: integer + example: 12 + + BuildingGridResponse: + type: object + properties: + rows: + type: integer + example: 10 + cols: + type: integer + example: 10 + totalCount: + type: integer + example: 142 + cells: + type: array + items: + $ref: '#/components/schemas/GridCell' \ No newline at end of file diff --git a/app/pom.xml b/app/pom.xml new file mode 100644 index 0000000..db0e890 --- /dev/null +++ b/app/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + org.btuk + Proxy + 1.13.0-SNAPSHOT + + + org.btuk.proxy + app + App + + + 21 + 21 + UTF-8 + + + + + org.btuk.proxy + api + + + org.btuk.proxy + core + + + org.btuk.proxy + database + + + + com.github.BuildtheUK + NetworkLib + + + + org.projectlombok + lombok + + + + org.apache.logging.log4j + log4j-core + provided + + + org.apache.logging.log4j + log4j-slf4j2-impl + provided + + + org.slf4j + jul-to-slf4j + + + + net.dv8tion + JDA + + + + net.kyori + adventure-text-serializer-plain + 4.16.0 + + + net.kyori + adventure-text-minimessage + 4.16.0 + + + + org.yaml + snakeyaml + + + + org.apache.commons + commons-lang3 + + + \ No newline at end of file diff --git a/core/src/main/java/org/btuk/proxy/core/ProxyController.java b/app/src/main/java/org.btuk.proxy.app/ProxyController.java similarity index 90% rename from core/src/main/java/org/btuk/proxy/core/ProxyController.java rename to app/src/main/java/org.btuk.proxy.app/ProxyController.java index 3440eb4..b7e9aab 100644 --- a/core/src/main/java/org/btuk/proxy/core/ProxyController.java +++ b/app/src/main/java/org.btuk.proxy.app/ProxyController.java @@ -1,28 +1,12 @@ -package org.btuk.proxy.core; +package org.btuk.proxy.app; import lombok.Getter; import lombok.extern.java.Log; -import net.bteuk.network.lib.dto.OnlineUserRemove; - -import org.btuk.proxy.core.chat.automod.AutoMod; -import org.btuk.proxy.database.DatabaseInit; -import org.btuk.proxy.database.sql.GlobalSQL; -import org.btuk.proxy.database.sql.PlotSQL; -import org.btuk.proxy.database.sql.RegionSQL; - -import org.slf4j.bridge.SLF4JBridgeHandler; - -import javax.sql.DataSource; -import java.io.File; -import java.io.IOException; -import java.sql.SQLException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; - +import org.btuk.network.lib.dto.OnlineUserRemove; +import org.btuk.proxy.api.server.ProxyApi; import org.btuk.proxy.core.chat.ChatHandler; import org.btuk.proxy.core.chat.ChatManager; +import org.btuk.proxy.core.chat.automod.AutoMod; import org.btuk.proxy.core.config.Config; import org.btuk.proxy.core.discord.Discord; import org.btuk.proxy.core.discord.ReviewStatus; @@ -37,9 +21,22 @@ import org.btuk.proxy.core.utils.Analytics; import org.btuk.proxy.core.utils.Constants; import org.btuk.proxy.core.utils.Moderation; +import org.btuk.proxy.database.DatabaseInit; +import org.btuk.proxy.database.sql.GlobalSQL; +import org.btuk.proxy.database.sql.PlotSQL; +import org.btuk.proxy.database.sql.RegionSQL; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import javax.sql.DataSource; +import java.io.File; +import java.io.IOException; +import java.sql.SQLException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import static java.awt.Color.RED; -import static org.btuk.proxy.core.utils.Constants.LEAVE_MESSAGE; /** * Controller of the core proxy functionality; can be enabled by the proxy or external plugins that want to use the proxy functions. @@ -74,6 +71,8 @@ public class ProxyController { private UserManager userManager; + private ProxyApi proxyApi; + private static final String PROXY_CONFIG_NAME = "proxy-config.yml"; private static final String AUTOMOD_CONFIG_NAME = "automod.yml"; @@ -99,8 +98,8 @@ public ProxyController(File dataFolder) { this.enabled = true; } - - public void start(ChatHandler chatHandler, Scheduler scheduler, CoreServerManager coreServerManager, PlayerManager playerManager, TabManager tabManager, Consumer socketInitializer) throws IOException { + public void start(ChatHandler chatHandler, Scheduler scheduler, CoreServerManager coreServerManager, PlayerManager playerManager, TabManager tabManager, + Consumer socketInitializer) throws IOException { if (!enabled) { log.severe("Proxy is not enabled, see previous logs for errors."); @@ -116,9 +115,10 @@ public void start(ChatHandler chatHandler, Scheduler scheduler, CoreServerManage Moderation moderation = new Moderation(globalSQL); AutoMod automod = new AutoMod(coreUserManager, new Config(dataFolder, AUTOMOD_CONFIG_NAME), moderation, discord, chatHandler, tabManager); - ChatManager chatManager = new ChatManager(chatHandler, coreUserManager, analytics, globalSQL, moderation, automod); + ChatManager chatManager = new ChatManager(chatHandler, coreUserManager, analytics, globalSQL, moderation, automod, discord); - this.userManager = new UserManager(coreUserManager, chatHandler, tabManager, globalSQL, plotSQL, regionSQL, coreServerManager, scheduler, chatManager, playerManager, analytics, discord, automod); + this.userManager = new UserManager(coreUserManager, chatHandler, tabManager, globalSQL, plotSQL, regionSQL, coreServerManager, scheduler, chatManager, playerManager, + analytics, discord, automod); ServerManager serverManager = new ServerManager(coreServerManager, scheduler, globalSQL, chatHandler, tabManager, coreUserManager, userManager); @@ -126,15 +126,20 @@ public void start(ChatHandler chatHandler, Scheduler scheduler, CoreServerManage new ReviewStatus(config, globalSQL, plotSQL, regionSQL, discord, scheduler); this.discord.addJDAEventListeners(chatManager, coreUserManager, tabManager, plotSQL); - + this.proxyApi = new ProxyApi(config.getBoolean("api.enabled"), config.getInt("api.port"), globalSQL, chatManager); serverManager.initOnlineServers(); - socketInitializer.accept(new ProxySocketHandler(chatManager, discord, userManager, serverManager, tabManager)); + socketInitializer.accept(new ProxySocketHandler(chatManager, discord, userManager, serverManager)); + + proxyApi.start(); started = true; } public void stop() { + if (proxyApi != null) { + proxyApi.stop(); + } if (started) { // Show the disconnect message for all players in discord. if (discord != null) { @@ -143,7 +148,7 @@ public void stop() { coreUserManager.runForEachOnline(user -> { if (user.isOnline()) { - discord.sendConnectEmbed(LEAVE_MESSAGE, user.getName(), user.getUuid(), user.getPlayerSkin(), RED, (reply) -> { + discord.sendConnectEmbed(Constants.LEAVE_MESSAGE, user.getName(), user.getUuid(), user.getPlayerSkin(), RED, (reply) -> { users.decrementAndGet(); disconnectLatch.countDown(); }); @@ -164,7 +169,7 @@ public void stop() { // Clear JDA listeners if (discord.getJda() != null) { - //Unregister listeners. + // Unregister listeners. discord.getJda().getEventManager().getRegisteredListeners().forEach(listener -> discord.getJda().getEventManager().unregister(listener)); } diff --git a/core/pom.xml b/core/pom.xml index 785ae1f..76da412 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -6,7 +6,7 @@ org.btuk Proxy - 1.11.1 + 1.13.0-SNAPSHOT org.btuk.proxy @@ -26,7 +26,7 @@ - com.github.BTEUK + com.github.BuildtheUK NetworkLib @@ -75,6 +75,11 @@ org.apache.commons commons-lang3 + + org.junit.jupiter + junit-jupiter + test + @@ -82,16 +87,6 @@ org.apache.maven.plugins maven-compiler-plugin - ${maven-compiler-plugin.version} - - - - org.projectlombok - lombok - ${lombok.version} - - - diff --git a/core/src/main/java/org/btuk/proxy/core/chat/ChatHandler.java b/core/src/main/java/org/btuk/proxy/core/chat/ChatHandler.java index 8280fdf..434d042 100644 --- a/core/src/main/java/org/btuk/proxy/core/chat/ChatHandler.java +++ b/core/src/main/java/org/btuk/proxy/core/chat/ChatHandler.java @@ -1,6 +1,6 @@ package org.btuk.proxy.core.chat; -import net.bteuk.network.lib.dto.AbstractTransferObject; +import org.btuk.network.lib.dto.AbstractTransferObject; import org.btuk.proxy.core.exceptions.ServerNotFoundException; diff --git a/core/src/main/java/org/btuk/proxy/core/chat/ChatManager.java b/core/src/main/java/org/btuk/proxy/core/chat/ChatManager.java index 8e99beb..fa64c5f 100644 --- a/core/src/main/java/org/btuk/proxy/core/chat/ChatManager.java +++ b/core/src/main/java/org/btuk/proxy/core/chat/ChatManager.java @@ -1,26 +1,25 @@ package org.btuk.proxy.core.chat; -import net.bteuk.network.lib.dto.ChatMessage; -import net.bteuk.network.lib.dto.DirectMessage; -import net.bteuk.network.lib.dto.PrivateMessage; -import net.bteuk.network.lib.dto.ReplyMessage; -import net.bteuk.network.lib.utils.ChatUtils; - -import org.btuk.proxy.core.chat.automod.AutoMod; -import org.btuk.proxy.database.sql.GlobalSQL; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; - -import java.util.List; - +import org.btuk.network.lib.dto.ChatMessage; +import org.btuk.network.lib.dto.DirectMessage; +import org.btuk.network.lib.dto.PrivateMessage; +import org.btuk.network.lib.dto.ReplyMessage; +import org.btuk.network.lib.utils.ChatUtils; +import org.btuk.proxy.core.chat.automod.AutoMod; +import org.btuk.proxy.core.discord.Discord; import org.btuk.proxy.core.user.CoreUserManager; import org.btuk.proxy.core.user.User; import org.btuk.proxy.core.utils.Analytics; import org.btuk.proxy.core.utils.Moderation; import org.btuk.proxy.core.utils.Time; +import org.btuk.proxy.database.sql.GlobalSQL; + +import java.util.List; -import static net.bteuk.network.lib.enums.ChatChannels.GLOBAL; +import static org.btuk.network.lib.enums.ChatChannels.GLOBAL; import static org.btuk.proxy.core.utils.Constants.DISCORD_SENDER; import static org.btuk.proxy.core.utils.Constants.SERVER_SENDER; @@ -41,17 +40,20 @@ public class ChatManager { private final AutoMod autoMod; + private final Discord discord; + private static final List SERVER_USERS = List.of(new String[]{SERVER_SENDER, DISCORD_SENDER}); private static final String FOCUS_ENABLED_PRESET = "%s is in focus mode, unable to send message."; - public ChatManager(ChatHandler chatHandler, CoreUserManager userManager, Analytics analytics, GlobalSQL globalSQL, Moderation moderation, AutoMod autoMod) { + public ChatManager(ChatHandler chatHandler, CoreUserManager userManager, Analytics analytics, GlobalSQL globalSQL, Moderation moderation, AutoMod autoMod, Discord discord) { this.chatHandler = chatHandler; this.userManager = userManager; this.analytics = analytics; this.globalSQL = globalSQL; this.moderation = moderation; this.autoMod = autoMod; + this.discord = discord; } /** @@ -70,6 +72,10 @@ public void handle(ChatMessage chatMessage) { if (player) { analytics.addMessage(chatMessage.getSender(), Time.getDate(Time.currentTime())); } + // Send the message to discord. + if (player) { + discord.handle(chatMessage); + } } /** diff --git a/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoMod.java b/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoMod.java index 00c9d28..2cbd378 100644 --- a/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoMod.java +++ b/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoMod.java @@ -1,16 +1,10 @@ package org.btuk.proxy.core.chat.automod; import lombok.extern.java.Log; -import net.bteuk.network.lib.dto.DirectMessage; -import net.bteuk.network.lib.utils.ChatUtils; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; - -import java.time.Duration; -import java.util.Collections; -import java.util.List; -import java.util.Map; - +import org.btuk.network.lib.dto.DirectMessage; +import org.btuk.network.lib.utils.ChatUtils; import org.btuk.proxy.core.chat.ChatHandler; import org.btuk.proxy.core.config.Config; import org.btuk.proxy.core.discord.Discord; @@ -20,7 +14,11 @@ import org.btuk.proxy.core.utils.Moderation; import org.btuk.proxy.core.utils.Time; -import static net.bteuk.network.lib.enums.ChatChannels.GLOBAL; +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +import static org.btuk.network.lib.enums.ChatChannels.GLOBAL; import static org.btuk.proxy.core.utils.Constants.SERVER_SENDER; /** @@ -93,7 +91,7 @@ public boolean moderate(String sender, Component messageComponent) { * @return true if the message should be blocked */ private boolean checkMessage(User user, String message) { - Map candidateWords = AutoModRule.getCandidateWords(message); + List candidateWords = AutoModRule.getCandidateWords(message); boolean blockMessage = false; for (AutoModRule rule : autoModConfig.getRules()) { blockMessage |= checkRule(rule, candidateWords, user, message); @@ -123,7 +121,7 @@ private void checkUser(User user) { } } - private boolean checkRule(AutoModRule rule, Map candidateWords, User user, String message) { + private boolean checkRule(AutoModRule rule, List candidateWords, User user, String message) { List matches = rule.getMatches(candidateWords); if (matches.isEmpty()) { return false; diff --git a/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoModRule.java b/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoModRule.java index a69fcd5..c7af2a2 100644 --- a/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoModRule.java +++ b/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoModRule.java @@ -4,13 +4,13 @@ import java.text.Normalizer; import java.time.Duration; -import java.util.LinkedHashMap; +import java.util.ArrayList; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; public abstract class AutoModRule { @@ -19,6 +19,8 @@ public abstract class AutoModRule { private static final Pattern NON_WHITESPACE_PATTERN = Pattern.compile("\\S+"); private static final Pattern NON_ALPHANUMERIC = Pattern.compile("[^\\p{L}\\p{N}]"); + private static final char SPACE = ' '; + private final Set flaggedWords; @Getter private final String id; @@ -26,13 +28,29 @@ public abstract class AutoModRule { @Getter private final Duration duration; + private final int maxLength; + public AutoModRule(String id, List flaggedWords, Duration duration) { + int maxLength = 0; this.id = id; - this.flaggedWords = flaggedWords.stream() - .map(AutoModRule::normalize) - .filter(word -> !word.isBlank()) - .collect(java.util.stream.Collectors.toUnmodifiableSet()); + java.util.Set normalizedSet = new java.util.HashSet<>(); + for (String word : flaggedWords) { + String normalized = normalize(word); + List tokens = new ArrayList<>(); + Matcher matcher = TOKEN_PATTERN.matcher(normalized); + while (matcher.find()) { + tokens.add(matcher.group()); + } + if (tokens.isEmpty()) continue; + + normalizedSet.add(String.join(" ", tokens)); + String joined = String.join("", tokens); + maxLength = Math.max(maxLength, joined.length()); + normalizedSet.add(joined); + } + this.flaggedWords = java.util.Collections.unmodifiableSet(normalizedSet); this.duration = duration; + this.maxLength = maxLength; } public abstract boolean blockMessage(); @@ -40,58 +58,112 @@ public AutoModRule(String id, List flaggedWords, Duration duration) { /** * Matches words against flagged words. * - * @param candidateWords map of candidate words keyed by normalized value + * @param candidateWords list of candidate words in order * @return list of matches */ - public List getMatches(Map candidateWords) { - return candidateWords.values().stream() - .filter(candidateWord -> flaggedWords.contains(candidateWord.normalized())) - .flatMap(candidateWord -> candidateWord.originals().stream() - .map(original -> new AutoModMatch(original, candidateWord.normalized()))) - .toList(); + public List getMatches(List candidateWords) { + List matches = new ArrayList<>(); + int totalCandidates = candidateWords.size(); + for (int i = 0; i < totalCandidates; i++) { + StringBuilder sbNormalizedWithSpaces = new StringBuilder(); + StringBuilder sbNormalizedNoSpaces = new StringBuilder(); + + for (int j = i; j < totalCandidates; j++) { + CandidateWord cw = candidateWords.get(j); + String normalized = cw.normalized(); + + sbNormalizedNoSpaces.append(normalized); + if (sbNormalizedNoSpaces.length() > maxLength) { + break; + } + + if (j > i) { + sbNormalizedWithSpaces.append(SPACE); + } + sbNormalizedWithSpaces.append(normalized); + + String phraseWithSpaces = sbNormalizedWithSpaces.toString(); + if (flaggedWords.contains(phraseWithSpaces)) { + StringBuilder sbOriginal = new StringBuilder(); + for (int k = i; k <= j; k++) { + if (k > i) { + sbOriginal.append(SPACE); + } + sbOriginal.append(candidateWords.get(k).original()); + } + matches.add(new AutoModMatch(sbOriginal.toString(), phraseWithSpaces)); + } else if (j > i) { + // Try joining without spaces to catch things like "b a d" if "bad" is flagged + String phraseNoSpaces = sbNormalizedNoSpaces.toString(); + if (flaggedWords.contains(phraseNoSpaces)) { + StringBuilder sbOriginal = new StringBuilder(); + for (int k = i; k <= j; k++) { + sbOriginal.append(candidateWords.get(k).original()); + } + matches.add(new AutoModMatch(sbOriginal.toString(), phraseNoSpaces)); + } + } + } + } + return matches; } /** - * Gets a map of candidate words based on a message. + * Gets a list of candidate words based on a message. * * @param message the message to get candidates for - * @return map of candidate words keyed by normalized value + * @return list of candidate words in order */ - public static LinkedHashMap getCandidateWords(String message) { - LinkedHashMap candidates = new LinkedHashMap<>(); - - Matcher tokenMatcher = TOKEN_PATTERN.matcher(message); - while (tokenMatcher.find()) { - String originalToken = tokenMatcher.group(); - String normalizedToken = normalize(originalToken); - - addCandidate(candidates, normalizedToken, originalToken); - } + public static List getCandidateWords(String message) { + List candidates = new ArrayList<>(); Matcher chunkMatcher = NON_WHITESPACE_PATTERN.matcher(message); while (chunkMatcher.find()) { String originalChunk = chunkMatcher.group(); - - // Plain alphanumeric words are already handled by TOKEN_PATTERN, - // so only process chunks that contain punctuation/symbols. - if (originalChunk.chars().allMatch(Character::isLetterOrDigit)) { - continue; - } - String normalizedChunk = NON_ALPHANUMERIC.matcher(normalize(originalChunk)).replaceAll(""); - addCandidate(candidates, normalizedChunk, originalChunk); - } + if (normalizedChunk.isBlank()) continue; - return candidates; - } + // Try to see if this chunk contains multiple words + Matcher tokenMatcher = TOKEN_PATTERN.matcher(originalChunk); + List subTokens = new ArrayList<>(); + while (tokenMatcher.find()) { + subTokens.add(tokenMatcher.group()); + } - private static void addCandidate(LinkedHashMap candidates, String normalized, String original) { - if (normalized.isBlank() || original.isBlank()) { - return; + if (subTokens.size() > 1) { + // If it contains multi-character tokens, it's likely multiple words (e.g., "bad-word") + boolean hasMultiCharToken = false; + for (String token : subTokens) { + if (token.length() > 1) { + hasMultiCharToken = true; + break; + } + } + + if (hasMultiCharToken) { + StringBuilder joinedSubBuilder = new StringBuilder(); + for (String sub : subTokens) { + String normalizedSub = normalize(sub); + candidates.add(new CandidateWord(normalizedSub, sub)); + joinedSubBuilder.append(normalizedSub); + } + // Also add the combined version if it's different from simple concatenation + String joinedSub = joinedSubBuilder.toString(); + if (!normalizedChunk.equals(joinedSub)) { + candidates.add(new CandidateWord(normalizedChunk, originalChunk)); + } + } else { + // All single letters, likely a single word broken up (e.g., "b.a.d") + candidates.add(new CandidateWord(normalizedChunk, originalChunk)); + } + } else { + // Single token or no sub-tokens (punctuation only) + candidates.add(new CandidateWord(normalizedChunk, originalChunk)); + } } - candidates.computeIfAbsent(normalized, CandidateWord::new).addOriginal(original); + return candidates; } private static String normalize(String input) { diff --git a/core/src/main/java/org/btuk/proxy/core/chat/automod/CandidateWord.java b/core/src/main/java/org/btuk/proxy/core/chat/automod/CandidateWord.java index b57d434..8bb43e3 100644 --- a/core/src/main/java/org/btuk/proxy/core/chat/automod/CandidateWord.java +++ b/core/src/main/java/org/btuk/proxy/core/chat/automod/CandidateWord.java @@ -1,17 +1,4 @@ package org.btuk.proxy.core.chat.automod; -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -public record CandidateWord(String normalized, List originals) { - - public CandidateWord(String normalized) { - this(normalized, new ArrayList<>()); - } - - public void addOriginal(String original) { - originals.add(original); - } +public record CandidateWord(String normalized, String original) { } \ No newline at end of file diff --git a/core/src/main/java/org/btuk/proxy/core/discord/BotChatListener.java b/core/src/main/java/org/btuk/proxy/core/discord/BotChatListener.java index 87945af..03bbc68 100644 --- a/core/src/main/java/org/btuk/proxy/core/discord/BotChatListener.java +++ b/core/src/main/java/org/btuk/proxy/core/discord/BotChatListener.java @@ -1,16 +1,15 @@ package org.btuk.proxy.core.discord; import lombok.extern.java.Log; -import net.bteuk.network.lib.dto.DiscordLinking; import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import org.apache.commons.lang3.StringUtils; +import org.btuk.network.lib.dto.DiscordLinking; +import org.btuk.proxy.core.chat.ChatHandler; import java.util.List; -import org.btuk.proxy.core.chat.ChatHandler; - @Log public class BotChatListener extends ListenerAdapter { diff --git a/core/src/main/java/org/btuk/proxy/core/discord/Discord.java b/core/src/main/java/org/btuk/proxy/core/discord/Discord.java index a435367..afcda52 100644 --- a/core/src/main/java/org/btuk/proxy/core/discord/Discord.java +++ b/core/src/main/java/org/btuk/proxy/core/discord/Discord.java @@ -3,20 +3,15 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.java.Log; -import net.bteuk.network.lib.dto.ChatMessage; -import net.bteuk.network.lib.dto.DiscordDirectMessage; -import net.bteuk.network.lib.dto.DiscordEmbed; -import net.bteuk.network.lib.dto.DiscordLinking; -import net.bteuk.network.lib.dto.DiscordRole; - -import org.btuk.proxy.core.chat.automod.AutoModMatch; -import org.btuk.proxy.core.user.User; -import org.btuk.proxy.database.sql.GlobalSQL; -import org.btuk.proxy.database.sql.PlotSQL; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; -import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.UserSnowflake; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.utils.ChunkingFilter; @@ -28,6 +23,22 @@ import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.apache.commons.lang3.time.DurationFormatUtils; +import org.btuk.network.lib.dto.ChatMessage; +import org.btuk.network.lib.dto.DiscordDirectMessage; +import org.btuk.network.lib.dto.DiscordEmbed; +import org.btuk.network.lib.dto.DiscordLinking; +import org.btuk.network.lib.dto.DiscordRole; +import org.btuk.proxy.core.chat.ChatHandler; +import org.btuk.proxy.core.chat.ChatManager; +import org.btuk.proxy.core.chat.automod.AutoModMatch; +import org.btuk.proxy.core.config.Config; +import org.btuk.proxy.core.discord.command.CommandManager; +import org.btuk.proxy.core.scheduler.Scheduler; +import org.btuk.proxy.core.tab.TabManager; +import org.btuk.proxy.core.user.CoreUserManager; +import org.btuk.proxy.core.user.User; +import org.btuk.proxy.database.sql.GlobalSQL; +import org.btuk.proxy.database.sql.PlotSQL; import java.awt.Color; import java.nio.charset.StandardCharsets; @@ -39,14 +50,6 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import org.btuk.proxy.core.chat.ChatHandler; -import org.btuk.proxy.core.chat.ChatManager; -import org.btuk.proxy.core.config.Config; -import org.btuk.proxy.core.discord.command.CommandManager; -import org.btuk.proxy.core.scheduler.Scheduler; -import org.btuk.proxy.core.tab.TabManager; -import org.btuk.proxy.core.user.CoreUserManager; - @Log public class Discord { @@ -116,7 +119,7 @@ public Discord(Config config, GlobalSQL globalSQL, ChatHandler chatHandler, Sche this.moderatorChat = jda.getTextChannelById(moderatorChannel); //Load all members into cache. - chat.getGuild().loadMembers().onSuccess(members -> { + chat.getGuild().loadMembers().onSuccess(_ -> { log.info("Loaded all discord members into cache"); //Enable role syncing. @@ -132,7 +135,7 @@ public void addJDAEventListeners(ChatManager chatManager, CoreUserManager coreUs jda.addEventListener(new DiscordChatListener(this, chatManager, chatChannelId, staffChannelId)); jda.addEventListener(new BotChatListener(chatHandler, linking)); - CommandManager commandManager = new CommandManager(coreUserManager, tabManager, globalSQL, plotSQL); + CommandManager commandManager = new CommandManager(coreUserManager, tabManager, globalSQL, plotSQL, config); jda.addEventListener(commandManager); jda.getGuilds().forEach(commandManager::registerCommands); } @@ -338,7 +341,7 @@ public void addRole(long userId, long role_id, boolean sync) { if (!member.getRoles().contains(role)) { // If successful, resync if enabled. chat.getGuild().addRoleToMember(member, role).queue( - (user) -> { + _ -> { if (sync && hasRoles != null && giveRoles != null) { syncRoles(); } @@ -368,7 +371,7 @@ public void removeRole(long userId, long role_id, boolean sync) { // If the member does not have the role, add it. if (member.getRoles().contains(role)) { chat.getGuild().removeRoleFromMember(member, role).queue( - (user) -> { + _ -> { if (sync && hasRoles != null && giveRoles != null) { syncRoles(); } diff --git a/core/src/main/java/org/btuk/proxy/core/discord/DiscordChatListener.java b/core/src/main/java/org/btuk/proxy/core/discord/DiscordChatListener.java index 8e075e1..c3673d2 100644 --- a/core/src/main/java/org/btuk/proxy/core/discord/DiscordChatListener.java +++ b/core/src/main/java/org/btuk/proxy/core/discord/DiscordChatListener.java @@ -1,9 +1,6 @@ package org.btuk.proxy.core.discord; import lombok.extern.java.Log; -import net.bteuk.network.lib.dto.ChatMessage; -import net.bteuk.network.lib.enums.ChatChannels; -import net.bteuk.network.lib.utils.ChatUtils; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.kyori.adventure.text.Component; @@ -11,7 +8,9 @@ import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; import org.apache.commons.lang3.StringUtils; - +import org.btuk.network.lib.dto.ChatMessage; +import org.btuk.network.lib.enums.ChatChannels; +import org.btuk.network.lib.utils.ChatUtils; import org.btuk.proxy.core.chat.ChatManager; import static org.btuk.proxy.core.utils.Constants.DISCORD_SENDER; diff --git a/core/src/main/java/org/btuk/proxy/core/discord/command/CommandManager.java b/core/src/main/java/org/btuk/proxy/core/discord/command/CommandManager.java index 54c1fb5..fccf6c1 100644 --- a/core/src/main/java/org/btuk/proxy/core/discord/command/CommandManager.java +++ b/core/src/main/java/org/btuk/proxy/core/discord/command/CommandManager.java @@ -1,8 +1,5 @@ package org.btuk.proxy.core.discord.command; -import org.btuk.proxy.database.sql.GlobalSQL; -import org.btuk.proxy.database.sql.PlotSQL; - import lombok.extern.java.Log; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.events.guild.GuildJoinEvent; @@ -14,14 +11,16 @@ import net.dv8tion.jda.api.interactions.commands.build.CommandData; import net.dv8tion.jda.api.interactions.commands.build.Commands; import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import org.btuk.proxy.core.config.Config; +import org.btuk.proxy.core.tab.TabManager; +import org.btuk.proxy.core.user.CoreUserManager; +import org.btuk.proxy.database.sql.GlobalSQL; +import org.btuk.proxy.database.sql.PlotSQL; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; -import org.btuk.proxy.core.tab.TabManager; -import org.btuk.proxy.core.user.CoreUserManager; - /** * Manages all Discord commands. * Each command is stored in a map so when a command interaction is run it can be routed accordingly. @@ -39,11 +38,14 @@ public class CommandManager extends ListenerAdapter { private final PlotSQL plotSQL; - public CommandManager(CoreUserManager userManager, TabManager tabManager, GlobalSQL globalSQL, PlotSQL plotSQL) { + private final Config config; + + public CommandManager(CoreUserManager userManager, TabManager tabManager, GlobalSQL globalSQL, PlotSQL plotSQL, Config config) { this.userManager = userManager; this.tabManager = tabManager; this.globalSQL = globalSQL; this.plotSQL = plotSQL; + this.config = config; commands = new ArrayList<>(); } @@ -97,7 +99,17 @@ public void registerCommands(Guild guild) { //Create commands. commands.add(new Playerlist(userManager, tabManager, "playerlist", "List all online players on the Minecraft server.")); - //commands.add(new Map("map", "Sends a link to the UK progress map.")); // The progress map is no longer available. + String progressMap = config.getString("progress_map"); + if (progressMap != null && !progressMap.isBlank()) { + commands.add(new Map("map", "Sends a link to the UK progress map.", progressMap)); + commands.add(new Map("progress", "Sends a link to the UK progress map.", progressMap)); + commands.add(new Map("progressmap", "Sends a link to the UK progress map.", progressMap)); + } + + String websiteLink = config.getString("website"); + if (websiteLink != null && !websiteLink.isBlank()) { + commands.add(new Website("website", "Sends a link of the website", websiteLink)); + } commands.add(new ClaimedPlots(globalSQL, plotSQL, "claimedplots", "List all plots that are currently claimed.", playerOption)); commands.add(new SubmittedPlots(globalSQL, plotSQL, "submittedplots", "List all plots that are currently submitted.", playerOption)); diff --git a/core/src/main/java/org/btuk/proxy/core/discord/command/Map.java b/core/src/main/java/org/btuk/proxy/core/discord/command/Map.java index 9d23928..7c0d58a 100644 --- a/core/src/main/java/org/btuk/proxy/core/discord/command/Map.java +++ b/core/src/main/java/org/btuk/proxy/core/discord/command/Map.java @@ -1,29 +1,27 @@ -//package org.btuk.proxy.core.discord.command; -// -//import org.btuk.proxy.Proxy; -//import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -//import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; -// -//public class Map extends AbstractCommand { -// -// /** -// * Constructor, saved the name and description of the command. -// * Also registers the command in Discord. -// * @param name Name of the command -// * @param description Description of the command -// */ -// public Map(String name, String description) { -// super(name, description); -// } -// -// @Override -// public void onCommand(SlashCommandInteractionEvent event) { -// -// String playerListMessage = Proxy.getInstance().getConfig().getString("progress_map"); -// -// ReplyCallbackAction reply = event.reply(playerListMessage); -// reply = reply.setEphemeral(true); -// reply.queue(); -// -// } -//} \ No newline at end of file +package org.btuk.proxy.core.discord.command; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; + +public class Map extends AbstractCommand { + + private final String link; + + /** + * Constructor, saved the name and description of the command. + * Also registers the command in Discord. + * @param name Name of the command + * @param description Description of the command + */ + public Map(String name, String description, String link) { + super(name, description); + this.link = link; + } + + @Override + public void onCommand(SlashCommandInteractionEvent event) { + ReplyCallbackAction reply = event.reply(link); + reply = reply.setEphemeral(true); + reply.queue(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/btuk/proxy/core/discord/command/Playerlist.java b/core/src/main/java/org/btuk/proxy/core/discord/command/Playerlist.java index e1eef47..2004a8f 100644 --- a/core/src/main/java/org/btuk/proxy/core/discord/command/Playerlist.java +++ b/core/src/main/java/org/btuk/proxy/core/discord/command/Playerlist.java @@ -1,20 +1,19 @@ package org.btuk.proxy.core.discord.command; -import net.bteuk.network.lib.dto.OnlineUser; -import net.bteuk.network.lib.dto.TabPlayer; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.btuk.network.lib.dto.OnlineUser; +import org.btuk.network.lib.dto.TabPlayer; +import org.btuk.proxy.core.tab.TabManager; +import org.btuk.proxy.core.user.CoreUserManager; +import org.btuk.proxy.core.user.User; import java.util.ArrayList; import java.util.Comparator; import java.util.Optional; import java.util.Set; -import org.btuk.proxy.core.tab.TabManager; -import org.btuk.proxy.core.user.CoreUserManager; -import org.btuk.proxy.core.user.User; - public class Playerlist extends AbstractCommand { private final CoreUserManager userManager; diff --git a/core/src/main/java/org/btuk/proxy/core/discord/command/Website.java b/core/src/main/java/org/btuk/proxy/core/discord/command/Website.java new file mode 100644 index 0000000..098552f --- /dev/null +++ b/core/src/main/java/org/btuk/proxy/core/discord/command/Website.java @@ -0,0 +1,27 @@ +package org.btuk.proxy.core.discord.command; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; + +public class Website extends AbstractCommand { + + private final String link; + + /** + * Constructor, saved the name and description of the command. + * Also registers the command in Discord. + * @param name Name of the command + * @param description Description of the command + */ + public Website(String name, String description, String link) { + super(name, description); + this.link = link; + } + + @Override + public void onCommand(SlashCommandInteractionEvent event) { + ReplyCallbackAction reply = event.reply(link); + reply = reply.setEphemeral(true); + reply.queue(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/btuk/proxy/core/server/ServerManager.java b/core/src/main/java/org/btuk/proxy/core/server/ServerManager.java index e072d55..00986e6 100644 --- a/core/src/main/java/org/btuk/proxy/core/server/ServerManager.java +++ b/core/src/main/java/org/btuk/proxy/core/server/ServerManager.java @@ -1,18 +1,11 @@ package org.btuk.proxy.core.server; import lombok.extern.java.Log; -import net.bteuk.network.lib.dto.OnlineUserRemove; -import net.bteuk.network.lib.dto.OnlineUsersReply; -import net.bteuk.network.lib.dto.ServerShutdown; -import net.bteuk.network.lib.dto.ServerStartup; -import org.btuk.proxy.database.sql.GlobalSQL; - -import java.util.List; -import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; +import org.btuk.network.lib.dto.OnlineUserRemove; +import org.btuk.network.lib.dto.OnlineUsersReply; +import org.btuk.network.lib.dto.ServerShutdown; +import org.btuk.network.lib.dto.ServerStartup; import org.btuk.proxy.core.chat.ChatHandler; import org.btuk.proxy.core.scheduler.Scheduler; import org.btuk.proxy.core.tab.TabManager; @@ -20,12 +13,18 @@ import org.btuk.proxy.core.user.User; import org.btuk.proxy.core.user.UserManager; import org.btuk.proxy.core.utils.Time; +import org.btuk.proxy.database.sql.GlobalSQL; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; @Log public class ServerManager { private final CoreServerManager coreServerManager; - + private final Scheduler scheduler; private final GlobalSQL globalSQL; private final ChatHandler chatHandler; @@ -40,6 +39,7 @@ public class ServerManager { public ServerManager(CoreServerManager coreServerManager, Scheduler scheduler, GlobalSQL globalSQL, ChatHandler chatHandler, TabManager tabManager, CoreUserManager coreUserManager, UserManager userManager) { this.coreServerManager = coreServerManager; + this.scheduler = scheduler; this.globalSQL = globalSQL; this.chatHandler = chatHandler; this.tabManager = tabManager; @@ -58,30 +58,30 @@ public void initOnlineServers() { } public void addServer(ServerStartup serverStartup) { - // It is possible the server is already set to online, this probably means it crashed, - // first clear all players that are 'connected' to this server and remove them. - Optional optionalServer = coreServerManager.getServers().stream().filter(server -> server.getName().equals(serverStartup.getServerName())).findFirst(); - optionalServer.ifPresent(this::removeServerDueToTimeout); + String serverName = serverStartup.getServerName(); + // It is possible the server was already set to online, this probably means it crashed. + // Even if the server is not currently tracked, there might be 'ghost' players. + cleanupServer(serverName); try { - Server server = coreServerManager.createServer(serverStartup.getServerName()); + Server server = coreServerManager.createServer(serverName); threadExecutor.submit(() -> addServerIfOnline(server)); } catch (RuntimeException e) { - log.warning("Unable to add server " + serverStartup.getServerName() + ", it can not be found."); + log.warning("Unable to add server " + serverName + ", it can not be found."); } } public void removeServer(ServerShutdown serverShutdown) { - Optional optionalServer = coreServerManager.getServers().stream().filter(server -> server.getName().equals(serverShutdown.getServerName())).findFirst(); - optionalServer.ifPresent(coreServerManager::removeServer); - - // Set the server offline in the database. - globalSQL.update("UPDATE server_data SET online=0 WHERE name='" + serverShutdown.getServerName() + "';"); + cleanupServer(serverShutdown.getServerName()); } private void addServerIfOnline(Server server) { + addServerIfOnline(server, 0); + } + + private void addServerIfOnline(Server server, int attempt) { // Skip if the server is already added. - if (coreServerManager.getServers().stream().anyMatch(server::equals)) { + if (coreServerManager.getServers().stream().anyMatch(s -> s.getName().equals(server.getName()))) { return; } if (server.canPing()) { @@ -95,34 +95,48 @@ private void addServerIfOnline(Server server) { tabManager.sendAddTeam(); } else { // The server is not online. - log.warning(String.format("Server " + server.getName() + " is not online.")); + if (attempt < 30) { + scheduler.createDelayedTask(() -> threadExecutor.submit(() -> addServerIfOnline(server, attempt + 1)), 1, TimeUnit.SECONDS); + } else { + log.warning(String.format("Server " + server.getName() + " is not online.")); + } } } private void pingServers() { coreServerManager.getServers().forEach(server -> threadExecutor.submit(() -> updatePing(server))); - // If any server has a ping of more than 120 seconds, set the server to offline and remove all online players that were connected to the server. + // If any server has a ping of more than 60 seconds, set the server to offline and remove all online players that were connected to the server. // This probably means the server crashed. - List offlineServers = coreServerManager.getServers().stream().filter(server -> server.getLastPing() < Time.currentTime() - 1000 * 120).toList(); + List offlineServers = coreServerManager.getServers().stream().filter(server -> server.getLastPing() < Time.currentTime() - 1000 * 60).toList(); offlineServers.forEach(this::removeServerDueToTimeout); } - private void removeServerDueToTimeout(Server server) { + private void cleanupServer(String serverName) { // Set the server offline in the database. - globalSQL.update("UPDATE server_data SET online=0 WHERE name='" + server.getName() + "';"); + globalSQL.update("UPDATE server_data SET online=0 WHERE name='" + serverName + "';"); - // Remove all users connected to this server, + // Remove all users connected to this server // and also send a message to all other online servers to remove these users from their list. - List offlineServerUsers = coreUserManager.getUsersOnServer(server.getName()); - offlineServerUsers.forEach(user -> { - OnlineUserRemove onlineUserRemove = new OnlineUserRemove(user.getUuid()); - chatHandler.handle(onlineUserRemove); - userManager.disconnectUser(user); - } - ); + // Delay this by a second so the switch server events have time to be processed. + scheduler.createDelayedTask(() -> { + List offlineServerUsers = coreUserManager.getUsersOnServer(serverName); + offlineServerUsers.forEach(user -> { + if (user.getSwitchServer() != null && user.getSwitchServer().getFromServer().equals(serverName)) { + log.info("User " + user.getName() + " is switching servers on shutdown."); + } else { + OnlineUserRemove onlineUserRemove = new OnlineUserRemove(user.getUuid()); + chatHandler.handle(onlineUserRemove); + userManager.disconnectUser(user); + } + }); + }, 1, TimeUnit.SECONDS); + + // Remove server from the list if it exists. + coreServerManager.getServer(serverName).ifPresent(coreServerManager::removeServer); + } - // Remove server from the list. - coreServerManager.removeServer(server); + private void removeServerDueToTimeout(Server server) { + cleanupServer(server.getName()); } private void updatePing(Server server) { diff --git a/core/src/main/java/org/btuk/proxy/core/socket/ProxySocketHandler.java b/core/src/main/java/org/btuk/proxy/core/socket/ProxySocketHandler.java index c59fe66..0324333 100644 --- a/core/src/main/java/org/btuk/proxy/core/socket/ProxySocketHandler.java +++ b/core/src/main/java/org/btuk/proxy/core/socket/ProxySocketHandler.java @@ -1,13 +1,30 @@ package org.btuk.proxy.core.socket; import lombok.extern.java.Log; -import net.bteuk.network.lib.dto.*; -import net.bteuk.network.lib.socket.SocketHandler; - +import org.btuk.network.lib.dto.AbstractTransferObject; +import org.btuk.network.lib.dto.ChatMessage; +import org.btuk.network.lib.dto.DirectMessage; +import org.btuk.network.lib.dto.DiscordDirectMessage; +import org.btuk.network.lib.dto.DiscordEmbed; +import org.btuk.network.lib.dto.DiscordLinking; +import org.btuk.network.lib.dto.DiscordRole; +import org.btuk.network.lib.dto.FocusEvent; +import org.btuk.network.lib.dto.ModerationEvent; +import org.btuk.network.lib.dto.MuteEvent; +import org.btuk.network.lib.dto.PlotMessage; +import org.btuk.network.lib.dto.PrivateMessage; +import org.btuk.network.lib.dto.ReplyMessage; +import org.btuk.network.lib.dto.ServerShutdown; +import org.btuk.network.lib.dto.ServerStartup; +import org.btuk.network.lib.dto.SwitchServerEvent; +import org.btuk.network.lib.dto.TeleportEvent; +import org.btuk.network.lib.dto.UserConnectRequest; +import org.btuk.network.lib.dto.UserDisconnect; +import org.btuk.network.lib.dto.UserUpdate; +import org.btuk.network.lib.socket.SocketHandler; import org.btuk.proxy.core.chat.ChatManager; import org.btuk.proxy.core.discord.Discord; import org.btuk.proxy.core.server.ServerManager; -import org.btuk.proxy.core.tab.TabManager; import org.btuk.proxy.core.user.UserManager; @Log @@ -17,14 +34,12 @@ public class ProxySocketHandler implements SocketHandler { private final Discord discord; private final UserManager userManager; private final ServerManager serverManager; - private final TabManager tabManager; - public ProxySocketHandler(ChatManager chatManager, Discord discord, UserManager userManager, ServerManager serverManager, TabManager tabManager) { + public ProxySocketHandler(ChatManager chatManager, Discord discord, UserManager userManager, ServerManager serverManager) { this.chatManager = chatManager; this.discord = discord; this.userManager = userManager; this.serverManager = serverManager; - this.tabManager = tabManager; } @Override @@ -33,7 +48,6 @@ public synchronized AbstractTransferObject handle(AbstractTransferObject abstrac switch (abstractTransferObject) { case ChatMessage chatMessage -> { chatManager.handle(chatMessage); - discord.handle(chatMessage); } case DirectMessage directMessage -> chatManager.handle(directMessage); case PrivateMessage privateMessage -> chatManager.handle(privateMessage); diff --git a/core/src/main/java/org/btuk/proxy/core/tab/AbstractTabManager.java b/core/src/main/java/org/btuk/proxy/core/tab/AbstractTabManager.java index 125ff8e..d1ab730 100644 --- a/core/src/main/java/org/btuk/proxy/core/tab/AbstractTabManager.java +++ b/core/src/main/java/org/btuk/proxy/core/tab/AbstractTabManager.java @@ -1,14 +1,20 @@ package org.btuk.proxy.core.tab; import lombok.Getter; -import net.bteuk.network.lib.dto.AddTeamEvent; -import net.bteuk.network.lib.dto.TabPlayer; -import net.bteuk.network.lib.utils.ChatUtils; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.minimessage.MiniMessage; +import org.btuk.network.lib.dto.AddTeamEvent; +import org.btuk.network.lib.dto.TabPlayer; +import org.btuk.network.lib.utils.ChatUtils; +import org.btuk.proxy.core.chat.ChatHandler; +import org.btuk.proxy.core.config.Config; +import org.btuk.proxy.core.player.Player; +import org.btuk.proxy.core.scheduler.Scheduler; +import org.btuk.proxy.core.user.CoreUserManager; +import org.btuk.proxy.core.user.User; import org.jetbrains.annotations.Nullable; import java.util.HashSet; @@ -17,13 +23,6 @@ import java.util.Set; import java.util.concurrent.TimeUnit; -import org.btuk.proxy.core.chat.ChatHandler; -import org.btuk.proxy.core.config.Config; -import org.btuk.proxy.core.player.Player; -import org.btuk.proxy.core.scheduler.Scheduler; -import org.btuk.proxy.core.user.CoreUserManager; -import org.btuk.proxy.core.user.User; - public abstract class AbstractTabManager implements TabManager { private final Config config; diff --git a/core/src/main/java/org/btuk/proxy/core/tab/TabManager.java b/core/src/main/java/org/btuk/proxy/core/tab/TabManager.java index c50ef3a..b552bb6 100644 --- a/core/src/main/java/org/btuk/proxy/core/tab/TabManager.java +++ b/core/src/main/java/org/btuk/proxy/core/tab/TabManager.java @@ -1,11 +1,10 @@ package org.btuk.proxy.core.tab; -import net.bteuk.network.lib.dto.TabPlayer; +import org.btuk.network.lib.dto.TabPlayer; +import org.btuk.proxy.core.user.User; import java.util.Optional; -import org.btuk.proxy.core.user.User; - public interface TabManager { void updatePlayerInTablistOfPlayer(User user, User userToUpdate); diff --git a/core/src/main/java/org/btuk/proxy/core/user/CoreUserManager.java b/core/src/main/java/org/btuk/proxy/core/user/CoreUserManager.java index 1145525..b8b13c2 100644 --- a/core/src/main/java/org/btuk/proxy/core/user/CoreUserManager.java +++ b/core/src/main/java/org/btuk/proxy/core/user/CoreUserManager.java @@ -1,6 +1,6 @@ package org.btuk.proxy.core.user; -import net.bteuk.network.lib.dto.OnlineUser; +import org.btuk.network.lib.dto.OnlineUser; import java.util.ArrayList; import java.util.Collections; diff --git a/core/src/main/java/org/btuk/proxy/core/user/User.java b/core/src/main/java/org/btuk/proxy/core/user/User.java index e4dec96..673a388 100644 --- a/core/src/main/java/org/btuk/proxy/core/user/User.java +++ b/core/src/main/java/org/btuk/proxy/core/user/User.java @@ -5,30 +5,37 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.java.Log; -import net.bteuk.network.lib.dto.DirectMessage; -import net.bteuk.network.lib.dto.TeleportEvent; -import net.bteuk.network.lib.dto.UserConnectReply; -import net.bteuk.network.lib.dto.UserConnectRequest; -import net.bteuk.network.lib.enums.ChatChannels; -import net.bteuk.network.lib.enums.TeleportRequestType; -import net.bteuk.network.lib.utils.ChatUtils; - -import org.btuk.proxy.core.exceptions.ServerNotFoundException; -import org.btuk.proxy.core.utils.TeleportRequest; - +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.btuk.network.lib.dto.DirectMessage; +import org.btuk.network.lib.dto.TeleportEvent; +import org.btuk.network.lib.dto.UserConnectReply; +import org.btuk.network.lib.dto.UserConnectRequest; +import org.btuk.network.lib.enums.ChatChannels; +import org.btuk.network.lib.enums.TeleportRequestType; +import org.btuk.network.lib.utils.ChatUtils; +import org.btuk.proxy.core.chat.ChatHandler; import org.btuk.proxy.core.chat.automod.AutoMod; import org.btuk.proxy.core.chat.automod.AutoModFlag; import org.btuk.proxy.core.chat.automod.AutoModFlagRule; import org.btuk.proxy.core.chat.automod.AutoModMatch; import org.btuk.proxy.core.chat.automod.AutoModRule; +import org.btuk.proxy.core.exceptions.ServerNotFoundException; +import org.btuk.proxy.core.player.Player; +import org.btuk.proxy.core.scheduler.ScheduledTask; +import org.btuk.proxy.core.scheduler.Scheduler; +import org.btuk.proxy.core.scheduler.TaskStatus; +import org.btuk.proxy.core.tab.TabManager; +import org.btuk.proxy.core.utils.Analytics; +import org.btuk.proxy.core.utils.SwitchServer; +import org.btuk.proxy.core.utils.TeleportRequest; +import org.btuk.proxy.core.utils.Time; import org.btuk.proxy.database.dto.AutoModFlagDTO; import org.btuk.proxy.database.sql.GlobalSQL; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.Style; -import net.kyori.adventure.text.format.TextDecoration; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import java.io.IOException; import java.net.HttpURLConnection; @@ -47,16 +54,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import org.btuk.proxy.core.chat.ChatHandler; -import org.btuk.proxy.core.player.Player; -import org.btuk.proxy.core.scheduler.ScheduledTask; -import org.btuk.proxy.core.scheduler.Scheduler; -import org.btuk.proxy.core.scheduler.TaskStatus; -import org.btuk.proxy.core.tab.TabManager; -import org.btuk.proxy.core.utils.Analytics; -import org.btuk.proxy.core.utils.SwitchServer; -import org.btuk.proxy.core.utils.Time; - import static org.btuk.proxy.core.utils.Constants.SERVER_SENDER; /** @@ -135,10 +132,6 @@ public class User { @Getter private boolean focusEnabled; - @Getter - @Setter - private boolean blockNextDisconnect = false; - @Getter @Setter private int previousPlotSubmissionCount = 0; @@ -229,6 +222,8 @@ public void disconnect(Runnable runnable) { analytics.save(this, Time.getDate(time), time); online = false; + // Ensure no existing disconnect exists. + cancelDisconnectTask(); // Run a delayed task to remove the user. disconnectTask = scheduler.createDelayedTask(runnable, 5L, TimeUnit.MINUTES); } @@ -243,6 +238,10 @@ public void reconnect() { online = true; // Can't be afk on reconnect. afk = false; + cancelDisconnectTask(); + } + + public void cancelDisconnectTask() { if (disconnectTask != null && disconnectTask.getStatus() == TaskStatus.SCHEDULED) { disconnectTask.cancel(); } @@ -253,10 +252,7 @@ public void reconnect() { * Delete the user instance. */ public void delete() { - // If the disconnectTask is running cancel. - if (disconnectTask != null && disconnectTask.getStatus() == TaskStatus.SCHEDULED) { - disconnectTask.cancel(); - } + cancelDisconnectTask(); } public void mute(User user) { @@ -295,7 +291,12 @@ public boolean isMuted() { public UserConnectReply createUserConnectReply() { // Create database object if not exists. - if (newUser && globalSQL.createUser(uuid, name, playerSkin)) { + if (newUser) { + if (!globalSQL.createUser(uuid, name, playerSkin)) { + // We don't want to send a reply to the server since this could cause issues. + // The user won't be able to do anything, so this is not a perfect solution. + throw new RuntimeException("Failed to create user " + uuid + " in database."); + } newUser = false; } @@ -550,6 +551,12 @@ public void saveAutoModFlags() { globalSQL.saveAutoModFlags(uuid, flags); } + public void updatePlayerSkin() { + if (playerSkin != null) { + globalSQL.update("UPDATE player_data SET player_skin='" + playerSkin + "' WHERE uuid='" + uuid + "';"); + } + } + private static JsonNode getJsonNodeFromUrl(URL url) throws IOException { StringBuilder inline = new StringBuilder(); Scanner scanner = new Scanner(url.openStream()); diff --git a/core/src/main/java/org/btuk/proxy/core/user/UserManager.java b/core/src/main/java/org/btuk/proxy/core/user/UserManager.java index dc680fb..a2ac078 100644 --- a/core/src/main/java/org/btuk/proxy/core/user/UserManager.java +++ b/core/src/main/java/org/btuk/proxy/core/user/UserManager.java @@ -2,37 +2,55 @@ import lombok.Getter; import lombok.extern.java.Log; -import net.bteuk.network.lib.dto.*; -import net.bteuk.network.lib.enums.ChatChannels; -import net.bteuk.network.lib.enums.ModerationAction; -import net.bteuk.network.lib.enums.TeleportRequestType; -import net.bteuk.network.lib.utils.ChatUtils; -import org.btuk.proxy.database.sql.GlobalSQL; -import org.btuk.proxy.database.sql.PlotSQL; -import org.btuk.proxy.database.sql.RegionSQL; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; - -import java.awt.Color; -import java.util.*; -import java.util.concurrent.TimeUnit; - -import org.btuk.proxy.core.chat.automod.AutoMod; -import org.btuk.proxy.core.discord.Discord; -import org.btuk.proxy.core.tab.TabManager; +import org.btuk.network.lib.dto.ChatMessage; +import org.btuk.network.lib.dto.DirectMessage; +import org.btuk.network.lib.dto.FocusEvent; +import org.btuk.network.lib.dto.ModerationEvent; +import org.btuk.network.lib.dto.MuteEvent; +import org.btuk.network.lib.dto.OnlineUser; +import org.btuk.network.lib.dto.OnlineUserAdd; +import org.btuk.network.lib.dto.OnlineUserRemove; +import org.btuk.network.lib.dto.PlotMessage; +import org.btuk.network.lib.dto.SwitchServerEvent; +import org.btuk.network.lib.dto.TeleportEvent; +import org.btuk.network.lib.dto.UserConnectReply; +import org.btuk.network.lib.dto.UserConnectRequest; +import org.btuk.network.lib.dto.UserDisconnect; +import org.btuk.network.lib.dto.UserRemove; +import org.btuk.network.lib.dto.UserUpdate; +import org.btuk.network.lib.enums.ChatChannels; +import org.btuk.network.lib.enums.ModerationAction; +import org.btuk.network.lib.enums.TeleportRequestType; +import org.btuk.network.lib.utils.ChatUtils; import org.btuk.proxy.core.chat.ChatHandler; import org.btuk.proxy.core.chat.ChatManager; +import org.btuk.proxy.core.chat.automod.AutoMod; +import org.btuk.proxy.core.discord.Discord; import org.btuk.proxy.core.exceptions.ErrorMessage; import org.btuk.proxy.core.exceptions.ServerNotFoundException; import org.btuk.proxy.core.player.PlayerManager; import org.btuk.proxy.core.scheduler.Scheduler; import org.btuk.proxy.core.server.CoreServerManager; +import org.btuk.proxy.core.tab.TabManager; import org.btuk.proxy.core.utils.Analytics; import org.btuk.proxy.core.utils.SwitchServer; import org.btuk.proxy.core.utils.Time; +import org.btuk.proxy.database.sql.GlobalSQL; +import org.btuk.proxy.database.sql.PlotSQL; +import org.btuk.proxy.database.sql.RegionSQL; -import static net.bteuk.network.lib.enums.ChatChannels.GLOBAL; -import static org.btuk.proxy.core.utils.Constants.*; +import java.awt.Color; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static org.btuk.network.lib.enums.ChatChannels.GLOBAL; +import static org.btuk.proxy.core.utils.Constants.JOIN_MESSAGE; +import static org.btuk.proxy.core.utils.Constants.LEAVE_MESSAGE; +import static org.btuk.proxy.core.utils.Constants.RECONNECT_MESSAGE; +import static org.btuk.proxy.core.utils.Constants.SERVER_SENDER; +import static org.btuk.proxy.core.utils.Constants.WELCOME_MESSAGE; /** * Class to manage the users on the network. @@ -113,9 +131,9 @@ public void handleUserDisconnect(UserDisconnect disconnect) { return; } - if (user.isBlockNextDisconnect()) { - log.warning("User has already reconnected, cancelling disconnect."); - user.setBlockNextDisconnect(false); + // If the player is not on the server they disconnected from, it implies they switched server. + if (!user.getServer().equals(disconnect.getServer())) { + log.warning("User disconnect received but they are on a different server, cancelling disconnect."); return; } @@ -271,11 +289,8 @@ public User addUser(UserConnectRequest request) { switchServer.cancelTimeout(); user.setSwitchServer(null); + user.cancelDisconnectTask(); } else { - // If the user is still online, quickly cancel the disconnect event. - if (user.isOnline()) { - user.setBlockNextDisconnect(true); - } // Cancel disconnect task. user.reconnect(); @@ -298,6 +313,9 @@ public User addUser(UserConnectRequest request) { } else { // Send the connect message. joinMessage = JOIN_MESSAGE; + + // Update the player skin in the database. + user.updatePlayerSkin(); } } diff --git a/core/src/main/java/org/btuk/proxy/core/utils/Moderation.java b/core/src/main/java/org/btuk/proxy/core/utils/Moderation.java index b7860c5..f2d4174 100644 --- a/core/src/main/java/org/btuk/proxy/core/utils/Moderation.java +++ b/core/src/main/java/org/btuk/proxy/core/utils/Moderation.java @@ -2,6 +2,8 @@ import org.btuk.proxy.database.sql.GlobalSQL; +import java.util.UUID; + public class Moderation { private final GlobalSQL globalSQL; diff --git a/core/src/main/java/org/btuk/proxy/core/utils/SwitchServer.java b/core/src/main/java/org/btuk/proxy/core/utils/SwitchServer.java index 20049d3..176ba56 100644 --- a/core/src/main/java/org/btuk/proxy/core/utils/SwitchServer.java +++ b/core/src/main/java/org/btuk/proxy/core/utils/SwitchServer.java @@ -29,6 +29,7 @@ public class SwitchServer { private final User user; + @Getter private final String fromServer; @Getter diff --git a/core/src/main/resources/proxy-config.yml b/core/src/main/resources/proxy-config.yml index d7b73fb..0350500 100644 --- a/core/src/main/resources/proxy-config.yml +++ b/core/src/main/resources/proxy-config.yml @@ -1,5 +1,5 @@ #Config version (do not update) -version: "1.11.1" +version: "1.13.0-SNAPSHOT" #Bot token token: "insert token here" @@ -63,6 +63,11 @@ role_syncing: give: [ ] +#API Configuration +api: + enabled: true + port: 61101 + #Database login host: localhost port: 3306 @@ -81,4 +86,7 @@ tab: footer: "\nServer Info: /help\nMore Info: /discord" #Discord user activity playing status (your server name or IP) -DiscordPlaying: "server.net" \ No newline at end of file +DiscordPlaying: "server.net" + +progress_map: "https://progress.buildtheuk.org" +website: "https://btuk.org" \ No newline at end of file diff --git a/core/src/test/java/org/btuk/proxy/core/chat/automod/AutoModRuleTest.java b/core/src/test/java/org/btuk/proxy/core/chat/automod/AutoModRuleTest.java new file mode 100644 index 0000000..beaa868 --- /dev/null +++ b/core/src/test/java/org/btuk/proxy/core/chat/automod/AutoModRuleTest.java @@ -0,0 +1,59 @@ +package org.btuk.proxy.core.chat.automod; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AutoModRuleTest { + + @Test + void testTooBadMatch() { + AutoModRule rule = new AutoModFlagRule("test", List.of("too bad"), 1, Duration.ZERO, true); + List candidates = AutoModRule.getCandidateWords("this is too bad"); + List matches = rule.getMatches(candidates); + assertFalse(matches.isEmpty(), "Should match 'too bad'"); + assertEquals("too bad", matches.getFirst().flaggedWord()); + assertEquals("too bad", matches.getFirst().messageWord()); + } + + @Test + void testTooBadLeetspeakMatch() { + AutoModRule rule = new AutoModFlagRule("test", List.of("too bad"), 1, Duration.ZERO, true); + List candidates = AutoModRule.getCandidateWords("this is t00 b.a.d"); + List matches = rule.getMatches(candidates); + assertFalse(matches.isEmpty(), "Should match 't00 b.a.d' against 'too bad'"); + assertEquals("too bad", matches.getFirst().flaggedWord()); + } + + @Test + void testPunctuationInFlaggedWord() { + // Test case where flagged word has punctuation + AutoModRule rule = new AutoModFlagRule("test", List.of("bad-word"), 1, Duration.ZERO, true); + List candidates = AutoModRule.getCandidateWords("you are a bad-word"); + List matches = rule.getMatches(candidates); + + assertFalse(matches.isEmpty(), "Should match 'bad-word' even if flagged as 'bad-word'"); + assertTrue(matches.stream().anyMatch(m -> m.flaggedWord().equals("badword") || m.flaggedWord().equals("bad word"))); + } + + @Test + void testDoubleSpaceInMessage() { + AutoModRule rule = new AutoModFlagRule("test", List.of("too bad"), 1, Duration.ZERO, true); + List candidates = AutoModRule.getCandidateWords("this is too bad"); + List matches = rule.getMatches(candidates); + assertFalse(matches.isEmpty(), "Should match 'too bad' (double space) against 'too bad'"); + } + + @Test + void testMixedPunctuationAndLeetspeak() { + AutoModRule rule = new AutoModFlagRule("test", List.of("bad word"), 1, Duration.ZERO, true); + List candidates = AutoModRule.getCandidateWords("you are b.a.d-w.0.r.d"); + List matches = rule.getMatches(candidates); + assertFalse(matches.isEmpty(), "Should match 'b.a.d-w.0.r.d' against 'bad word'"); + } +} diff --git a/database/pom.xml b/database/pom.xml index a7b7a8e..194960b 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -4,7 +4,7 @@ org.btuk Proxy - 1.11.1 + 1.13.0-SNAPSHOT org.btuk.proxy @@ -13,7 +13,7 @@ - com.github.BTEUK + com.github.BuildtheUK NetworkLib @@ -36,23 +36,4 @@ 2.0.0-1.21.4 - - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven-compiler-plugin.version} - - - - org.projectlombok - lombok - ${lombok.version} - - - - - - \ No newline at end of file diff --git a/database/src/main/java/org/btuk/proxy/database/DatabaseUpdates.java b/database/src/main/java/org/btuk/proxy/database/DatabaseUpdates.java index e1c9039..1f69302 100644 --- a/database/src/main/java/org/btuk/proxy/database/DatabaseUpdates.java +++ b/database/src/main/java/org/btuk/proxy/database/DatabaseUpdates.java @@ -18,7 +18,7 @@ public class DatabaseUpdates { // Version of the database that this build expects. - private static final Version EXPECTED_VERSION = Version.of(1, 10, 0); + private static final Version EXPECTED_VERSION = Version.of(1, 12, 0); private final GlobalSQL globalSQL; @@ -78,10 +78,19 @@ private List migrationSteps() { new MigrationStep(Version.of(1, 7, 3), this::update1_7_3), new MigrationStep(Version.of(1, 9, 4), this::update1_9_4), new MigrationStep(Version.of(1, 9, 5), this::update1_9_5), - new MigrationStep(Version.of(1, 11, 0), () -> {}) + new MigrationStep(Version.of(1, 12, 0), this::update1_12_0) ); } + private void update1_12_0() { + // Add indexes for performance optimization. + globalSQL.update("CREATE INDEX idx_player_data_1 ON player_data(name);"); + globalSQL.update("CREATE INDEX idx_buildings_1 ON buildings(player_id);"); + globalSQL.update("CREATE INDEX idx_buildings_2 ON buildings(lat, lon);"); + globalSQL.update("CREATE INDEX idx_messages_1 ON messages(recipient);"); + globalSQL.update("CREATE INDEX idx_moderation_1 ON moderation(uuid, end_time, type);"); + } + private void update1_9_5() { //add the new fields isPublic, playerBuilt and timeAdded to the buildings database globalSQL.update("ALTER TABLE buildings ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT TRUE, ADD COLUMN player_built BOOLEAN NOT NULL DEFAULT TRUE, ADD COLUMN time_added DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP;"); @@ -292,10 +301,14 @@ private MigrationStep(Version targetVersion, Runnable migration) { private Version migrate(Version currentVersion) { log.info("Migrating database from version " + currentVersion + " to version " + targetVersion); - migration.run(); - - globalSQL.update("UPDATE unique_data SET data_value='" + targetVersion + "' WHERE data_key='version';"); - return targetVersion; + try { + migration.run(); + globalSQL.update("UPDATE unique_data SET data_value='" + targetVersion + "' WHERE data_key='version';"); + return targetVersion; + } catch (Exception e) { + log.severe("Failed to migrate database to version " + targetVersion + ": " + e.getMessage()); + throw e; + } } } diff --git a/database/src/main/java/org/btuk/proxy/database/dto/BuildingDTO.java b/database/src/main/java/org/btuk/proxy/database/dto/BuildingDTO.java new file mode 100644 index 0000000..d797948 --- /dev/null +++ b/database/src/main/java/org/btuk/proxy/database/dto/BuildingDTO.java @@ -0,0 +1,15 @@ +package org.btuk.proxy.database.dto; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record BuildingDTO( + int buildingId, + String playerId, + String playerName, // Added field + boolean isPublic, + boolean playerBuilt, + LocalDateTime timeAdded, + double lat, + double lon +) {} diff --git a/database/src/main/java/org/btuk/proxy/database/dto/GridCellDTO.java b/database/src/main/java/org/btuk/proxy/database/dto/GridCellDTO.java new file mode 100644 index 0000000..1c7c4a3 --- /dev/null +++ b/database/src/main/java/org/btuk/proxy/database/dto/GridCellDTO.java @@ -0,0 +1,13 @@ +package org.btuk.proxy.database.dto; + +public record GridCellDTO( + double lat, + double lon, + double minLat, + double maxLat, + double minLon, + double maxLon, + int row, + int col, + int count +) {} diff --git a/database/src/main/java/org/btuk/proxy/database/dto/PlayerDTO.java b/database/src/main/java/org/btuk/proxy/database/dto/PlayerDTO.java new file mode 100644 index 0000000..b63661f --- /dev/null +++ b/database/src/main/java/org/btuk/proxy/database/dto/PlayerDTO.java @@ -0,0 +1,6 @@ +package org.btuk.proxy.database.dto; + +import java.util.UUID; + +public record PlayerDTO(String uuid, String name) { +} diff --git a/database/src/main/java/org/btuk/proxy/database/sql/GlobalSQL.java b/database/src/main/java/org/btuk/proxy/database/sql/GlobalSQL.java index 2c23c1a..9ee7513 100644 --- a/database/src/main/java/org/btuk/proxy/database/sql/GlobalSQL.java +++ b/database/src/main/java/org/btuk/proxy/database/sql/GlobalSQL.java @@ -2,16 +2,20 @@ import lombok.extern.java.Log; +import org.btuk.proxy.database.dto.AutoModFlagDTO; +import org.btuk.proxy.database.dto.BuildingDTO; +import org.btuk.proxy.database.dto.GridCellDTO; +import org.btuk.proxy.database.dto.PlayerDTO; + import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; -import org.btuk.proxy.database.dto.AutoModFlagDTO; - @Log public class GlobalSQL extends AbstractSQL { public GlobalSQL(DataSource datasource) { @@ -19,11 +23,15 @@ public GlobalSQL(DataSource datasource) { } public boolean createUser(String uuid, String name, String playerSkin) { - if (uuid == null || name == null || playerSkin == null) { - log.warning("createUser called with null argument(s)"); + if (uuid == null || name == null) { + log.severe("createUser called with null argument(s)"); return false; } + if (playerSkin == null) { + log.warning("createUser called without a player skin."); + } + final String sql = """ INSERT INTO player_data(uuid, name, last_online, last_submit, player_skin) VALUES(?, ?, ?, ?, ?) @@ -201,4 +209,231 @@ INSERT INTO moderation(uuid, start_time, end_time, reason, type) log.severe("Failed to insert moderation record for " + uuid + ": " + e.getMessage()); } } + + public String getPlayerUuidByName(String name) { + if (name == null) { + return null; + } + + try (Connection conn = conn(); PreparedStatement statement = conn.prepareStatement("SELECT uuid FROM player_data WHERE name=?;")) { + statement.setString(1, name); + try (ResultSet results = statement.executeQuery()) { + if (results.next()) { + return results.getString(1); + } + } + } catch (SQLException e) { + log.severe("Failed to get player uuid for " + name + ": " + e.getMessage()); + } + return null; + } + + public String getPlayerUsernameByUuid(String uuid) { + if (uuid == null) { + return null; + } + + try (Connection conn = conn(); PreparedStatement statement = conn.prepareStatement("SELECT name FROM player_data WHERE uuid=?;")) { + statement.setString(1, uuid); + try (ResultSet results = statement.executeQuery()) { + if (results.next()) { + return results.getString(1); + } + } + } catch (SQLException e) { + log.severe("Failed to get player uuid for " + uuid + ": " + e.getMessage()); + } + return null; + } + + public List getOnlinePlayers() { + List players = new ArrayList<>(); + try (Connection conn = conn(); PreparedStatement statement = conn.prepareStatement("SELECT uuid, name FROM player_data WHERE uuid IN (SELECT uuid FROM online_users);"); + ResultSet results = statement.executeQuery()) { + while (results.next()) { + players.add(new PlayerDTO(results.getString(1), results.getString(2))); + } + } catch (SQLException e) { + log.severe("Failed to get online players: " + e.getMessage()); + } + return players; + } + + private BuildingDTO mapBuilding(ResultSet results) throws SQLException { + Timestamp timestamp = results.getTimestamp("time_added"); + return new BuildingDTO( + results.getInt("building_id"), + results.getString("player_id"), + results.getString("player_name"), + results.getBoolean("is_public"), + results.getBoolean("player_built"), + timestamp != null ? timestamp.toLocalDateTime() : null, + results.getDouble("lat"), + results.getDouble("lon") + ); + } + + // Dynamic flexible building count with optional spatial, player, and status filters + public int getBuildingCount(List playerUuids, Double minLat, Double maxLat, Double minLon, Double maxLon, Boolean isPublic, Boolean playerBuilt) { + StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM buildings WHERE 1=1"); + List params = new ArrayList<>(); + + if (minLat != null && maxLat != null) { + sql.append(" AND lat BETWEEN ? AND ?"); + params.add(minLat); + params.add(maxLat); + } + if (minLon != null && maxLon != null) { + sql.append(" AND lon BETWEEN ? AND ?"); + params.add(minLon); + params.add(maxLon); + } + if (isPublic != null) { + sql.append(" AND is_public = ?"); + params.add(isPublic); + } + if (playerBuilt != null) { + sql.append(" AND player_built = ?"); + params.add(playerBuilt); + } + if (playerUuids != null && !playerUuids.isEmpty()) { + sql.append(" AND player_id IN ("); + for (int i = 0; i < playerUuids.size(); i++) { + sql.append(i == 0 ? "?" : ", ?"); + params.add(playerUuids.get(i)); + } + sql.append(")"); + } + + try (Connection conn = conn(); PreparedStatement statement = conn.prepareStatement(sql.toString())) { + for (int i = 0; i < params.size(); i++) { + Object param = params.get(i); + if (param instanceof Double d) { + statement.setDouble(i + 1, d); + } else if (param instanceof Boolean b) { + statement.setBoolean(i + 1, b); + } else if (param instanceof String s) { + statement.setString(i + 1, s); + } + } + + try (ResultSet results = statement.executeQuery()) { + if (results.next()) { + return results.getInt(1); + } + } + } catch (SQLException e) { + log.severe("Failed to get building count: " + e.getMessage()); + } + return 0; + } + + // Area fetch with JOIN to retrieve builder username + public List getBuildingsByArea(double minLat, double maxLat, double minLon, double maxLon, String playerUuid) { + List buildings = new ArrayList<>(); + + boolean hasPlayer = (playerUuid != null && !playerUuid.isEmpty()); + + // INNER JOIN on player_data table using building.player_id = player_data.uuid + String sql = """ + SELECT + b.building_id, + b.player_id, + p.name AS player_name, + b.is_public, + b.player_built, + b.time_added, + b.lat, + b.lon + FROM buildings b + INNER JOIN player_data p ON b.player_id = p.uuid + WHERE b.lat BETWEEN ? AND ? + AND b.lon BETWEEN ? AND ? + """ + (hasPlayer ? "AND (b.is_public = TRUE OR b.player_id = ?);" : "AND b.is_public = TRUE;"); + + try (Connection conn = conn(); PreparedStatement statement = conn.prepareStatement(sql)) { + statement.setDouble(1, minLat); + statement.setDouble(2, maxLat); + statement.setDouble(3, minLon); + statement.setDouble(4, maxLon); + if (hasPlayer) { + statement.setString(5, playerUuid); + } + try (ResultSet results = statement.executeQuery()) { + while (results.next()) { + buildings.add(mapBuilding(results)); + } + } + } catch (SQLException e) { + log.severe("Failed to get buildings by area: " + e.getMessage()); + } + return buildings; + } + + //this works could be updated in the future to group buildings in a more organic way (not just a grid) + public List getBuildingGridCounts(double minLat, double maxLat, double minLon, double maxLon, double stepLat, double stepLon, String playerUuid) { + List cells = new ArrayList<>(); + boolean hasPlayer = (playerUuid != null && !playerUuid.isEmpty()); + + // Anchor cells globally to (0,0) using FLOOR(coord / step) and average building positions + String sql = """ + SELECT + CAST(FLOOR(lat / ?) AS SIGNED) AS cell_y, + CAST(FLOOR(lon / ?) AS SIGNED) AS cell_x, + AVG(lat) AS avg_lat, + AVG(lon) AS avg_lon, + COUNT(*) AS cell_count + FROM buildings + WHERE lat BETWEEN ? AND ? + AND lon BETWEEN ? AND ? + """ + (hasPlayer ? "AND (is_public = TRUE OR player_id = ?) " : "AND is_public = TRUE ") + """ + GROUP BY cell_y, cell_x; + """; + + try (Connection conn = conn(); PreparedStatement statement = conn.prepareStatement(sql)) { + int idx = 1; + statement.setDouble(idx++, stepLat); + statement.setDouble(idx++, stepLon); + + statement.setDouble(idx++, minLat); + statement.setDouble(idx++, maxLat); + statement.setDouble(idx++, minLon); + statement.setDouble(idx++, maxLon); + + if (hasPlayer) { + statement.setString(idx++, playerUuid); + } + + try (ResultSet results = statement.executeQuery()) { + while (results.next()) { + int cellY = results.getInt("cell_y"); + int cellX = results.getInt("cell_x"); + double avgLat = results.getDouble("avg_lat"); + double avgLon = results.getDouble("avg_lon"); + int count = results.getInt("cell_count"); + + // Calculate bounding box bounds for this specific cell grid + double cellMinLat = cellY * stepLat; + double cellMaxLat = (cellY + 1) * stepLat; + double cellMinLon = cellX * stepLon; + double cellMaxLon = (cellX + 1) * stepLon; + + cells.add(new GridCellDTO( + avgLat, + avgLon, + cellMinLat, + cellMaxLat, + cellMinLon, + cellMaxLon, + cellY, + cellX, + count + )); + } + } + } catch (SQLException e) { + log.severe("Failed to calculate building grid count in SQL: " + e.getMessage()); + } + return cells; + } } diff --git a/database/src/main/java/org/btuk/proxy/database/sql/PlotSQL.java b/database/src/main/java/org/btuk/proxy/database/sql/PlotSQL.java index 2274dbd..a277f92 100644 --- a/database/src/main/java/org/btuk/proxy/database/sql/PlotSQL.java +++ b/database/src/main/java/org/btuk/proxy/database/sql/PlotSQL.java @@ -1,8 +1,8 @@ package org.btuk.proxy.database.sql; import lombok.extern.java.Log; -import net.bteuk.network.lib.enums.PlotDifficulties; -import net.bteuk.network.lib.utils.Reviewing; +import org.btuk.network.lib.enums.PlotDifficulties; +import org.btuk.network.lib.utils.Reviewing; import org.btuk.proxy.database.sql.migration.AcceptData; import org.btuk.proxy.database.sql.migration.DenyData; import org.btuk.proxy.database.sql.migration.PlotSubmissions; diff --git a/database/src/main/resources/dbsetup_global.sql b/database/src/main/resources/dbsetup_global.sql index 3a30940..7102d15 100644 --- a/database/src/main/resources/dbsetup_global.sql +++ b/database/src/main/resources/dbsetup_global.sql @@ -42,7 +42,8 @@ CREATE TABLE IF NOT EXISTS player_data player_skin TEXT NULL DEFAULT NULL, tips_enabled TINYINT(1) NOT NULL DEFAULT 1, display_name TEXT NULL DEFAULT NULL, - PRIMARY KEY (uuid) + PRIMARY KEY (uuid), + INDEX idx_player_data_1 (name) ); CREATE TABLE IF NOT EXISTS messages @@ -51,7 +52,8 @@ CREATE TABLE IF NOT EXISTS messages recipient CHAR(36) NOT NULL, message TEXT NOT NULL, PRIMARY KEY(id), - CONSTRAINT fk_messages_1 FOREIGN KEY(recipient) REFERENCES player_data(uuid) + CONSTRAINT fk_messages_1 FOREIGN KEY(recipient) REFERENCES player_data(uuid), + INDEX idx_messages_1 (recipient) ); CREATE TABLE IF NOT EXISTS join_events @@ -155,7 +157,8 @@ CREATE TABLE IF NOT EXISTS moderation type ENUM('ban', 'mute') NOT NULL, PRIMARY KEY(uuid,start_time), - CONSTRAINT fk_moderation_1 FOREIGN KEY(uuid) REFERENCES player_data(uuid) + CONSTRAINT fk_moderation_1 FOREIGN KEY(uuid) REFERENCES player_data(uuid), + INDEX idx_moderation_1 (uuid, end_time, type) ); CREATE TABLE IF NOT EXISTS coins @@ -221,7 +224,9 @@ CREATE TABLE IF NOT EXISTS buildings lon DOUBLE DEFAULT 0, PRIMARY KEY(building_id), CONSTRAINT fk_buildings_1 FOREIGN KEY(coordinate_id) REFERENCES coordinates(id), - CONSTRAINT fk_buildings_2 FOREIGN KEY(player_id) REFERENCES player_data(uuid) + CONSTRAINT fk_buildings_2 FOREIGN KEY(player_id) REFERENCES player_data(uuid), + INDEX idx_buildings_1 (player_id), + INDEX idx_buildings_2 (lat, lon) ); CREATE TABLE IF NOT EXISTS survey diff --git a/jitpack.yml b/jitpack.yml index 13ceb6c..fb27ac1 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,4 +1,4 @@ jdk: - - openjdk21 + - openjdk25 install: - - mvn install -Dmaven.javadoc.skip=true -DskipTests -pl :database,:core -am \ No newline at end of file + - ./mvnw install -Dmaven.javadoc.skip=true -DskipTests -pl :database,:core,:app -am \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/plugin/pom.xml b/plugin/pom.xml index bde6367..9df8608 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -7,7 +7,7 @@ org.btuk Proxy - 1.11.1 + 1.13.0-SNAPSHOT org.btuk.proxy @@ -30,7 +30,11 @@ false Proxy-${project.version} true - + + + io.netty:* + + *:* @@ -57,20 +61,24 @@ - com.github.BTEUK:NetworkLib + com.github.BuildtheUK:NetworkLib com/fasterxml/** com/google/gson/** net/kyori/** org/jetbrains/** org/intellij/** - META-INF/** + META-INF/MANIFEST.MF + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA - + @@ -79,7 +87,6 @@ org.apache.maven.plugins maven-compiler-plugin - ${maven-compiler-plugin.version} @@ -105,10 +112,22 @@ + + org.btuk.proxy + app + org.btuk.proxy core + + org.btuk.proxy + api + + + org.btuk.proxy + database + com.velocitypowered diff --git a/plugin/src/main/java/org/btuk/proxy/Proxy.java b/plugin/src/main/java/org/btuk/proxy/Proxy.java index 9fcf4b8..9ca99d9 100644 --- a/plugin/src/main/java/org/btuk/proxy/Proxy.java +++ b/plugin/src/main/java/org/btuk/proxy/Proxy.java @@ -10,21 +10,18 @@ import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.server.RegisteredServer; import lombok.Getter; -import net.bteuk.network.lib.socket.InputSocket; - +import org.btuk.network.lib.socket.InputSocket; +import org.btuk.proxy.app.ProxyController; import org.btuk.proxy.chat.ProxyChatHandler; - +import org.btuk.proxy.core.socket.ProxySocketHandler; import org.btuk.proxy.listener.CommandListener; import org.btuk.proxy.listener.ServerConnectListener; import org.btuk.proxy.player.ProxyPlayerManager; import org.btuk.proxy.scheduler.ProxyScheduler; import org.btuk.proxy.server.ProxyCoreServerManager; -import org.btuk.proxy.core.socket.ProxySocketHandler; - import org.btuk.proxy.tab.ProxyTabManager; import org.slf4j.Logger; - import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -38,9 +35,7 @@ import java.util.UUID; import java.util.function.Consumer; -import org.btuk.proxy.core.ProxyController; - -@Plugin(id = "proxy", name = "Proxy", version = "1.11.1", +@Plugin(id = "proxy", name = "Proxy", version = "1.13.0-SNAPSHOT", url = "https://github.com/BTEUK/Proxy", description = "Proxy plugin, managed chat, discord and server related actions.", authors = {"ELgamer"}) public class Proxy { @@ -71,7 +66,6 @@ public Proxy(ProxyServer server, Logger logger) { @Subscribe public void onProxyInitialization(ProxyInitializeEvent event) throws IOException { - this.proxyController = new ProxyController(getDataFolder()); this.defaultServer = proxyController.getConfig().getString("default_server"); diff --git a/plugin/src/main/java/org/btuk/proxy/chat/ProxyChatHandler.java b/plugin/src/main/java/org/btuk/proxy/chat/ProxyChatHandler.java index 38baaf6..25e6f27 100644 --- a/plugin/src/main/java/org/btuk/proxy/chat/ProxyChatHandler.java +++ b/plugin/src/main/java/org/btuk/proxy/chat/ProxyChatHandler.java @@ -1,21 +1,20 @@ package org.btuk.proxy.chat; import lombok.extern.java.Log; -import net.bteuk.network.lib.dto.AbstractTransferObject; -import net.bteuk.network.lib.socket.OutputSocket; +import org.btuk.network.lib.dto.AbstractTransferObject; +import org.btuk.network.lib.socket.OutputSocket; +import org.btuk.proxy.core.chat.ChatHandler; +import org.btuk.proxy.core.config.Config; +import org.btuk.proxy.core.config.ConfigSocket; import org.btuk.proxy.core.exceptions.ServerNotFoundException; +import org.btuk.proxy.core.server.CoreServerManager; +import org.btuk.proxy.core.server.Server; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import org.btuk.proxy.core.chat.ChatHandler; -import org.btuk.proxy.core.config.Config; -import org.btuk.proxy.core.config.ConfigSocket; -import org.btuk.proxy.core.server.Server; -import org.btuk.proxy.core.server.CoreServerManager; - @Log public class ProxyChatHandler implements ChatHandler { diff --git a/plugin/src/main/java/org/btuk/proxy/tab/ProxyTabManager.java b/plugin/src/main/java/org/btuk/proxy/tab/ProxyTabManager.java index 6534f61..e8c03d7 100644 --- a/plugin/src/main/java/org/btuk/proxy/tab/ProxyTabManager.java +++ b/plugin/src/main/java/org/btuk/proxy/tab/ProxyTabManager.java @@ -3,13 +3,16 @@ import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.util.GameProfile; -import net.bteuk.network.lib.dto.TabPlayer; - +import net.kyori.adventure.text.Component; +import org.btuk.network.lib.dto.TabPlayer; +import org.btuk.proxy.core.chat.ChatHandler; +import org.btuk.proxy.core.config.Config; +import org.btuk.proxy.core.player.Player; import org.btuk.proxy.core.scheduler.Scheduler; import org.btuk.proxy.core.tab.AbstractTabManager; +import org.btuk.proxy.core.user.CoreUserManager; +import org.btuk.proxy.core.user.User; import org.btuk.proxy.player.ProxyPlayer; - -import net.kyori.adventure.text.Component; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -17,12 +20,6 @@ import java.util.List; import java.util.Optional; -import org.btuk.proxy.core.chat.ChatHandler; -import org.btuk.proxy.core.config.Config; -import org.btuk.proxy.core.player.Player; -import org.btuk.proxy.core.user.CoreUserManager; -import org.btuk.proxy.core.user.User; - /** * Keeps track of all users and their tab information. * Sends updates to the servers when things change. diff --git a/pom.xml b/pom.xml index cba95a7..813c04a 100644 --- a/pom.xml +++ b/pom.xml @@ -6,29 +6,31 @@ org.btuk Proxy - 1.11.1 + 1.13.0-SNAPSHOT pom Proxy database - plugin + api core + plugin + app - 21 + 25 UTF-8 ${java-version} - 3.11.0 - 3.6.1 + 3.15.0 + 3.6.2 - 955372d721 + 5dee061bcb - 3.5.0-SNAPSHOT + 4.1.0-SNAPSHOT - 1.18.34 + 1.18.46 2.25.2 2.0.17 @@ -70,7 +72,12 @@ org.btuk.proxy - database + api + ${project.version} + + + org.btuk.proxy + app ${project.version} @@ -78,9 +85,14 @@ core ${project.version} + + org.btuk.proxy + database + ${project.version} + - com.github.BTEUK + com.github.BuildtheUK NetworkLib ${networklib.version} @@ -154,4 +166,25 @@ + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + \ No newline at end of file