Skip to content

Add DPC community API integration for cross-server faction sharing#1959

Open
Copilot wants to merge 22 commits intodevelopfrom
copilot/add-dpc-community-api-integration
Open

Add DPC community API integration for cross-server faction sharing#1959
Copilot wants to merge 22 commits intodevelopfrom
copilot/add-dpc-community-api-integration

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 27, 2026

Integrates Medieval Factions with the DPC community API (POST /api/v1/factions) to allow opt-in sharing of faction data to the DPC website. Strictly opt-in, disabled by default.

Config (config.yml)

  • New dpc-api section: enabled, url, key, server-id, login-reminder, share-server-ip, server-address, discord-link, sync-interval-minutes — all safe defaults
  • server-id is a dedicated config option for unique server identification in the DPC registry
  • server-address allows explicit server address (e.g. play.myserver.com:25565) for proxied setups; auto-detects from server binding when blank
  • key includes inline comment directing operators to https://dansplugins.com to register and generate an API key
  • sync-interval-minutes controls how often faction data is synced (default: 10 minutes); also used as initial delay
  • url is the full endpoint URL (default: https://dansplugins.com/api/v1/factions); supports port numbers (e.g. https://dansplugins.com:8080/api/v1/factions) and is used directly without any path appending

Commands (MfFactionDpcCommand)

All behind mf.dpc permission (declared in plugin.yml with default: op), wrappers around config writes:

Command Effect
/mf dpc optin|optout Toggle dpc-api.enabled
/mf dpc reminder on|off Toggle dpc-api.login-reminder
/mf dpc shareip on|off Toggle dpc-api.share-server-ip
/mf dpc discord <link>|clear Set/clear dpc-api.discord-link

The /mf dpc command is discoverable via /mf help.

HTTP Sync (MfDpcApiService)

  • Uses java.net.http.HttpClient.sendAsync — no blocking the server thread
  • HttpClient is an injectable constructor parameter (default: standard client) for testability
  • Scheduled via runTaskTimerAsynchronously with configurable interval from dpc-api.sync-interval-minutes (default: 10 min)
  • dpc-api.url is the full endpoint URL used directly (no path appending); validated on each sync: must be non-blank, parseable, absolute, and use an http or https scheme; clear warnings logged on validation failure
  • X-API-Key header on all requests; serverIp/discordLink conditionally included
  • serverIp resolved once per sync cycle (outside the per-faction loop) from dpc-api.server-address config when set, otherwise builds proper host:port from server binding; omitted entirely when IP is empty; empty-IP warning logged once per plugin enable (not every cycle) with explicit guidance to set dpc-api.server-address
  • serverId sourced from dpc-api.server-id config (required; sync skips with warning if blank)
  • Failed requests logged, never crash the plugin
  • Payload serialization: Uses a DpcFactionPayload data class with Gson for JSON serialization instead of manual JsonObject/JsonArray construction
  • Payload field truncation: All string fields are truncated to match the DPC API schema limits (name: 64, serverId: 64, description: 512, serverIp: 253, discordLink: 512)
  • memberCount: Included as a non-negative integer from the faction's member list size
  • discordLink validation: Only included if the configured value starts with https://discord.gg/ or https://discord.com/; invalid values are omitted with a once-per-cycle warning

Operator Login Reminder

  • Fires on PlayerJoinEvent when isOp && !enabled && loginReminder
  • Directs operators to https://dansplugins.com to obtain an API key, and to set dpc-api.key and dpc-api.server-id before opting in
  • Mentions optin, shareip, discord, and reminder off commands

API Key Acquisition

API keys are managed on the DPC website — operators visit dansplugins.com, create an account or sign in, then generate a key from their account page. This is documented in the config comments, CONFIG.md, and the login reminder message. Account creation is handled by the DPC website, not this plugin.

bStats

Four SimplePie charts: dpc_api_opt_in, dpc_api_login_reminder, dpc_api_share_server_ip, dpc_api_discord_link_set

Docs & i18n

  • CONFIG.md and COMMANDS.md updated
  • Language strings added to all 5 locale files (en_US, en_GB, de_DE, fr_FR, pt_BR)
  • Fixed pre-existing locale bug: CannotInteractBlockInWilderness and CmdFactionAdmin were concatenated on one line in all 5 locale files
  • Help text in non-English locales corrected to describe config/sharing management rather than account management
  • docs/dpc-api-schema.asn1: ASN.1 schema documenting the DPC API payload structure and field limits
  • .github/copilot-instructions.md: Copilot instructions covering stack, build, DPC API conventions, and testing guidance

Build & CI

  • build.gradle: Added mavenLocal() as the first repository so locally-built Ponder artifacts are resolved when repo.dansplugins.com is unavailable
  • build.yml: Added a "Build Ponder dependency" step that clones Dans-Plugins/Ponder, checks out the 2.0.0 tag, and runs publishToMavenLocal before the main build
  • build.yml: Added a docker-build job that runs docker build . to verify the Dockerfile builds successfully on every push and PR
  • release.yml: Same Ponder build step added before shadowJar
  • Dockerfile: Now self-contained — builds Ponder from source, then builds the Medieval Factions plugin (shadowJar) inside the container, eliminating the need for pre-built JARs in ./build/libs
  • Dockerfile: Installs both openjdk-17-jdk (for Gradle toolchain compilation) and openjdk-21-jdk (for BuildTools and MC 1.21 server runtime), with JAVA_HOME set to JDK 17 for Gradle builds and JDK 21 for server runtime

Tests

  • MfFactionDpcCommandTest — 12 cases covering permission denial, all subcommands, and edge cases
  • MfDpcApiServiceTest — 9 cases using mock HttpClient to verify payload composition: correct fields for normal factions, serverIp omitted when disabled, discordLink omitted when blank or invalid, memberCount present and non-negative, field truncation for oversized values, sync skipped when serverId is blank, non-default port includes :port in serverIp, configured server-address overrides server binding; test helper collects raw bytes and decodes once to handle multi-byte UTF-8 correctly

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits March 27, 2026 05:54
- Add dpc-api config entries with safe defaults (enabled: false, login-reminder: true, etc.)
- Add MfDpcApiService for async HTTP faction data sync via java.net.http.HttpClient
- Add MfFactionDpcCommand with subcommands: optin, optout, reminder, shareip, discord
- Register dpc subcommand in MfFactionCommand with aliases and tab completion
- Add operator login reminder to PlayerJoinListener (shown when !enabled && loginReminder)
- Add periodic async sync task (every 10 minutes) in MedievalFactions.kt
- Add 4 bStats pie charts: dpc_api_opt_in, dpc_api_login_reminder, dpc_api_share_server_ip, dpc_api_discord_link_set
- Add language strings to all 5 lang files (en_US, en_GB, de_DE, fr_FR, pt_BR)
- Add DPC command documentation to COMMANDS.md
- Add DPC config documentation to CONFIG.md
- Add unit tests for MfFactionDpcCommand

Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/e497d56e-78a1-43e7-a56e-4c7764cf07f6

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
…t, decouple DPC reminder

Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/e497d56e-78a1-43e7-a56e-4c7764cf07f6

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copilot AI changed the title [WIP] Add DPC community API integration for faction sharing Add DPC community API integration for cross-server faction sharing Mar 27, 2026
Copilot AI requested a review from dmccoystephenson March 27, 2026 05:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in integration that periodically syncs faction data to the DPC community API and exposes server-admin controls via a new /mf dpc subcommand, with associated config/docs/i18n and metrics.

Changes:

  • Introduces MfDpcApiService and schedules periodic async sync of faction data to POST /api/v1/factions.
  • Adds /mf dpc ... command handlers (opt-in/out + toggles) and an operator login reminder when not opted in.
  • Extends config, docs, bStats charts, and locale strings to support the new integration; adds command unit tests.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/main/kotlin/com/dansplugins/factionsystem/dpc/MfDpcApiService.kt New service that builds JSON payloads and POSTs them asynchronously to the DPC API.
src/main/kotlin/com/dansplugins/factionsystem/MedievalFactions.kt Registers new bStats charts and schedules periodic DPC sync task.
src/main/kotlin/com/dansplugins/factionsystem/command/faction/dpc/MfFactionDpcCommand.kt New /mf dpc subcommand implementation with config-backed toggles.
src/main/kotlin/com/dansplugins/factionsystem/command/faction/MfFactionCommand.kt Routes the new dpc subcommand via aliases and tab completion.
src/main/kotlin/com/dansplugins/factionsystem/listener/PlayerJoinListener.kt Adds op-only opt-in reminder message on join (when configured).
src/main/resources/config.yml Adds dpc-api configuration section with defaults.
CONFIG.md Documents the new dpc-api.* configuration options.
COMMANDS.md Documents the new /faction dpc / /mf dpc commands and permission.
src/main/resources/lang/lang_en_US.properties Adds DPC command/reminder strings.
src/main/resources/lang/lang_en_GB.properties Adds DPC command/reminder strings.
src/main/resources/lang/lang_de_DE.properties Adds DPC command/reminder strings.
src/main/resources/lang/lang_fr_FR.properties Adds DPC command/reminder strings.
src/main/resources/lang/lang_pt_BR.properties Adds DPC command/reminder strings.
src/test/kotlin/com/dansplugins/factionsystem/command/faction/dpc/MfFactionDpcCommandTest.kt Adds unit tests covering the new command behavior.

Comment on lines +36 to +42
val serverName = plugin.server.name

val jsonArray = JsonArray()
for (faction in factions) {
val obj = JsonObject()
obj.addProperty("name", faction.name)
obj.addProperty("serverId", serverName)
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serverId is currently derived from plugin.server.name, which in Bukkit/Paper is the server implementation name (e.g., "CraftBukkit"/"Paper") rather than a unique identifier for this specific server. That will cause different servers to collide on the same serverId value in the DPC API payload. Use a stable per-server identifier instead (e.g., plugin.server.serverName if available, or a dedicated config value such as dpc-api.server-id).

Suggested change
val serverName = plugin.server.name
val jsonArray = JsonArray()
for (faction in factions) {
val obj = JsonObject()
obj.addProperty("name", faction.name)
obj.addProperty("serverId", serverName)
val serverIdConfig = plugin.config.getString("dpc-api.server-id")
val serverId = serverIdConfig?.takeIf { it.isNotBlank() } ?: plugin.server.serverName
if (serverId.isBlank()) {
plugin.logger.warning("DPC server ID is not configured and server name is blank. Skipping faction sync.")
return
}
val jsonArray = JsonArray()
for (faction in factions) {
val obj = JsonObject()
obj.addProperty("name", faction.name)
obj.addProperty("serverId", serverId)

Copilot uses AI. Check for mistakes.
@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot address comments and also how do operators get an API key are we gonna have account creation in this PR?

…ition

- Replace plugin.server.name with dpc-api.server-id config for unique server identification
- Add API key instructions to login reminder, CONFIG.md, and config.yml comments
- Update all 5 lang files with setup instructions (visit dansplugins.com for key)

Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/cf701e83-94fd-4778-87bd-fc73ae05dc90

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot address comments and also how do operators get an API key are we gonna have account creation in this PR?

Addressed in 4890171:

  1. Reviewer comment fixedserverId now comes from a dedicated dpc-api.server-id config option instead of plugin.server.name (which returns "CraftBukkit"/"Paper"). The sync will log a warning and skip if server-id is blank.

  2. API key acquisition — Account creation is on the DPC website side (dansplugins.com), not in this PR. The plugin now documents the workflow in three places:

    • config.yml comment: # API key from https://dansplugins.com — register or sign in, then generate a key from your account page
    • CONFIG.md: full description pointing operators to dansplugins.com
    • Login reminder message: tells operators to "Get an API key at https://dansplugins.com, set dpc-api.key and dpc-api.server-id in config.yml, then run /mf dpc optin"

@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot align the solution with the latest changes in Dans-Plugins/dansplugins-dot-com#110

New /mf dpc subcommands: register, login, profile, generatekey, deletekey.
MfDpcApiService gains account API methods (register, login, profile, API
key create/delete) with JWT token storage per player.
Updated all 5 locale files, COMMANDS.md, CONFIG.md, and tests (24 cases).

Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/6b5b26ea-fd68-45ee-bfec-29f54a488143

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot align the solution with the latest changes in Dans-Plugins/dansplugins-dot-com#110

Implemented in e56dbcd. The plugin now aligns with the DPC API from dansplugins-dot-com PR #110:

New /mf dpc subcommands — players can register, login, view profile, generate API keys, and delete API keys directly from in-game without needing a browser:

  • /mf dpc register <username> <password>POST /api/v1/accounts/register
  • /mf dpc login <username> <password>POST /api/v1/accounts/login
  • /mf dpc profileGET /api/v1/accounts/me
  • /mf dpc generatekey <serverName>POST /api/v1/accounts/me/api-keys
  • /mf dpc deletekey <keyId>DELETE /api/v1/accounts/me/api-keys/{keyId}

MfDpcApiService stores JWT tokens per player UUID (ConcurrentHashMap), auto-clears on 401. All HTTP calls are async via HttpClient.sendAsync. Login reminder updated to mention in-game registration as primary flow.

Tests expanded to 24 cases covering all new subcommands. All 5 locale files, COMMANDS.md, and CONFIG.md updated.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • hub.spigotmc.org
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=256m (dns block)
  • jitpack.io
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=256m (dns block)
  • repo.dansplugins.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=256m (dns block)
  • repo.extendedclip.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=256m (dns block)
  • repo.mikeprimm.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=256m (dns block)
  • repo.rpkit.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=256m (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title Add DPC community API integration for cross-server faction sharing Add DPC community API integration with in-game account management Mar 28, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 6 comments.

Comment on lines +258 to +260
if (shareServerIp) {
obj.addProperty("serverIp", plugin.server.ip.ifEmpty { plugin.server.port.toString() })
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When Server#getIp is empty, the code sends only the port as serverIp (e.g., 25565), which is not a valid address and is very likely not what the API expects. Build a proper host:port (and/or introduce a dedicated dpc-api.server-address config) instead of falling back to port.toString().

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +124
val username = args[1]
val password = args[2]
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepting passwords as command arguments is risky because commands are commonly logged by servers/proxies and may be visible to staff tooling. Prefer a non-logging input flow (e.g., conversation-style prompt collected from chat with cancellation/timeout, or directing users to register/login via the website), and avoid storing/echoing raw credentials anywhere.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +105
} else if (response.statusCode() == 401) {
onFailure("InvalidCredentials")
} else {
onFailure("HTTP ${response.statusCode()}")
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service returns internal/error-code strings like InvalidCredentials, NotLoggedIn, SessionExpired, and HTTP 500 which end up shown to players (via {0} placeholders). Map these to localized, player-friendly messages (or structured error types) and consider including a sanitized server-provided error message (e.g., JSON message) for non-2xx responses to make failures actionable.

Copilot uses AI. Check for mistakes.
GateCreateAreaRestrictedBlock=O port�o que voc� est� tentando criar cont�m um bloco restrito.
CannotBreakBlockInWilderness=Voc� n�o pode quebrar blocos na natureza.
CannotPlaceBlockInWilderness=Voc� n�o pode colocar blocos na natureza.
CannotInteractBlockInWilderness=Voc� n�o pode interagir com blocos na natureza.CmdFactionAdmin=admin
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line appears to accidentally concatenate two properties (CannotInteractBlockInWilderness and CmdFactionAdmin), which would prevent CmdFactionAdmin from being parsed as its own key and would corrupt the wilderness message. Split it into two separate lines so both keys are correctly defined.

Suggested change
CannotInteractBlockInWilderness=Voc� n�o pode interagir com blocos na natureza.CmdFactionAdmin=admin
CannotInteractBlockInWilderness=Voc� n�o pode interagir com blocos na natureza.
CmdFactionAdmin=admin

Copilot uses AI. Check for mistakes.
GateCreateAreaRestrictedBlock=Das Tor, das Sie zu erstellen versuchen, enth�lt einen eingeschr�nkten Block.
CannotBreakBlockInWilderness=Du kannst keine Bl�cke in der Wildnis brechen.
CannotPlaceBlockInWilderness=Du kannst keine Bl�cke in der Wildnis platzieren.
CannotInteractBlockInWilderness=Du kannst nicht mit Bl�cken in der Wildnis interagieren.CmdFactionAdmin=admin
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the pt_BR locale: two properties are concatenated on one line, which breaks parsing for CmdFactionAdmin and pollutes the wilderness message. Split into two separate property lines.

Suggested change
CannotInteractBlockInWilderness=Du kannst nicht mit Bl�cken in der Wildnis interagieren.CmdFactionAdmin=admin
CannotInteractBlockInWilderness=Du kannst nicht mit Bl�cken in der Wildnis interagieren.
CmdFactionAdmin=admin

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +21
class MfDpcApiService(private val plugin: MedievalFactions) {

private val httpClient: HttpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build()
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are thorough tests for MfFactionDpcCommand, but the new MfDpcApiService behavior is untested (token storage/clearing on 401, request/response parsing, and sync payload composition/headers). Consider adding tests by injecting an HTTP abstraction (or a configurable HttpClient) so responses can be simulated deterministically without actual network calls.

Suggested change
class MfDpcApiService(private val plugin: MedievalFactions) {
private val httpClient: HttpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build()
class MfDpcApiService(
private val plugin: MedievalFactions,
private val httpClient: HttpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build()
) {

Copilot uses AI. Check for mistakes.
@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot address comments

@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot there are some failing unit tests and the docker build is running into java.lang.IllegalArgumentException: Unsupported class file major version 65 (see CI checks)

resolve

…match)

Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/192d8dec-54ca-4deb-bca6-069282bbe893

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 30, 2026

@copilot there are some failing unit tests and the docker build is running into java.lang.IllegalArgumentException: Unsupported class file major version 65 (see CI checks)

resolve

Fixed both issues in cb82c3a:

  1. Unit testsMfDpcApiServiceTest had a Kotlin+Mockito incompatibility: ArgumentMatchers.any() returns null, and casting that to a non-null Kotlin type throws NPE. Fixed by adding an anyNonNull<T>() helper with @Suppress("UNCHECKED_CAST") and using doReturn().when() instead of when().thenReturn(). The MfFactionMemberTest failure was a cascade from the corrupted Mockito state.

  2. Docker build — Ponder and MF both use Gradle 7.x which doesn't support Java 21 (class file major version 65). Changed JAVA_HOME to JDK 17 for all Gradle builds, switching to JDK 21 only for BuildTools and MC server runtime.

@copilot add CI check for verifying Docker build

Added in b655c9f. New docker-build job in build.yml runs docker build in parallel with the Gradle build on every push/PR. The Docker build itself was failing due to JDK version mismatch — fixed in cb82c3a by using JDK 17 for Gradle builds and JDK 21 only for BuildTools/server.

Comment on lines +97 to +113
val jsonArray = JsonArray()
for (faction in factions) {
val obj = JsonObject()
obj.addProperty("name", truncate(faction.name, MAX_NAME))
obj.addProperty("serverId", truncate(serverId, MAX_SERVER_ID))
obj.addProperty("memberCount", maxOf(0, faction.members.size))
obj.addProperty("description", truncate(faction.description, MAX_DESCRIPTION))
if (serverIp != null) {
obj.addProperty("serverIp", serverIp)
}
if (discordLink != null) {
obj.addProperty("discordLink", discordLink)
}
jsonArray.add(obj)
}

val body = gson.toJson(jsonArray)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid instantiating JsonArray / JsonObject directly; instead use models and Jackson for serialization

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced JsonArray/JsonObject with a DpcFactionPayload data class and Gson serialization in 14c1dd8. The project uses Gson (not Jackson), so null fields in DpcFactionPayload are automatically omitted by Gson's default behavior. Required fields (name, serverId, description) are non-nullable; optional fields (serverIp, discordLink) are nullable with null default.


val dpcApiService = MfDpcApiService(this)
// 12000 ticks = 10 minutes (20 ticks/sec * 60 sec * 10 min)
server.scheduler.runTaskTimerAsynchronously(this, Runnable { dpcApiService.syncFactions() }, 12000L, 12000L)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the initial delay and interval configurable via the config.yml

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added dpc-api.sync-interval-minutes config option (default: 10, minimum: 1) in 14c1dd8. Documented in config.yml and CONFIG.md.

- *buttons
dpc-api:
enabled: false
url: "https://dansplugins.com"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it is necessary to use a port here, so we should either document that or make that configurable

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the config comment to document that a port can be included in the URL (e.g. "https://dansplugins.com:8080"). Also updated CONFIG.md with the same documentation. Since the url field is already a free-form URL string, operators can include any port — no additional config field needed. Done in 14c1dd8.

@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot address comments

Copilot AI and others added 2 commits March 30, 2026 02:29
…sync interval configurable, document URL port

Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/c9c0cce8-17e1-4634-ba3e-36fbf151814a

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
… for sync interval

Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/c9c0cce8-17e1-4634-ba3e-36fbf151814a

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 30, 2026

@copilot address comments

Addressed all 3 review comments in 14c1dd8 and f67bb41:

  1. JsonArray/JsonObject → data model — Created DpcFactionPayload data class with Gson serialization. Required fields are non-nullable; optional serverIp/discordLink are nullable (omitted from JSON when null).
  2. Configurable sync interval — Added dpc-api.sync-interval-minutes (default 10, min 1) to config.yml, CONFIG.md, and MedievalFactions.kt.
  3. URL port documentation — Updated config.yml comment and CONFIG.md to document that a port can be included in the URL.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated 2 comments.

name UTF8String (SIZE(1..64)),
serverId UTF8String (SIZE(1..64)),
memberCount INTEGER (0..MAX),
description UTF8String (SIZE(0..512)) OPTIONAL,
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema marks description as OPTIONAL, but the implementation/tests always include description in the JSON payload (and DpcFactionPayload.description is non-nullable). Please align the schema with the actual payload contract (either make description required in the schema, or update the payload to omit it when empty/null).

Suggested change
description UTF8String (SIZE(0..512)) OPTIONAL,
description UTF8String (SIZE(0..512)),

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +86
val serverIp: String? = if (shareServerIp) {
val serverAddress = plugin.config.getString("dpc-api.server-address")?.takeIf { it.isNotBlank() }
if (serverAddress != null) {
truncate(serverAddress, MAX_SERVER_IP)
} else {
val ip = plugin.server.ip
val port = plugin.server.port
if (ip.isNotEmpty()) {
val addr = if (port != 25565) "$ip:$port" else ip
truncate(addr, MAX_SERVER_IP)
} else {
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serverIp formatting/selection logic (explicit dpc-api.server-address vs auto-detected ip and conditional :port when port != 25565) isn't covered by tests. Adding test cases for (1) non-default port includes ":port" and (2) configured server-address overrides server binding would help prevent regressions.

Copilot uses AI. Check for mistakes.
@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot address comments

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 4, 2026

@copilot address comments

Addressed both reviewer comments in cf079db:

  1. Schema description alignment — Removed OPTIONAL from description in docs/dpc-api-schema.asn1 since DpcFactionPayload.description is non-nullable and always included in the payload.
  2. serverIp test coverage — Added 2 test cases: (a) non-default port includes :port in serverIp, (b) configured server-address overrides server binding.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated 3 comments.

Comment on lines +106 to +108
val body = gson.toJson(payloads)
val url = apiUrl.trimEnd('/') + "/api/v1/factions"

Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dpc-api.url is treated as a base URL, but the code unconditionally appends /api/v1/factions. If an operator follows documentation/examples that include the endpoint path (or any base path), the request URL becomes duplicated (e.g. .../api/v1/factions/api/v1/factions). Consider either (1) documenting/enforcing that dpc-api.url must be the base host only, with validation that rejects/strips /api/v1/factions, or (2) allowing dpc-api.url to be the full endpoint and removing the hard-coded append (or appending only when missing).

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +88
if (ip.isNotEmpty()) {
val addr = if (port != 25565) "$ip:$port" else ip
truncate(addr, MAX_SERVER_IP)
} else {
plugin.logger.warning("Server IP is empty and dpc-api.server-address is not configured. Omitting serverIp from sync payload.")
null
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When share-server-ip is enabled and dpc-api.server-address is blank, many Bukkit servers report server.ip as an empty string (bind-all). This will log a warning every sync cycle and always omit serverIp, even though the operator likely just needs to set dpc-api.server-address. Consider reducing log spam (e.g., log once per plugin run / per enable) and/or making the warning explicitly instruct to set dpc-api.server-address for typical setups where the bound IP is empty.

Copilot uses AI. Check for mistakes.
Comment on lines +272 to +274
override fun onNext(item: ByteBuffer) {
body.append(StandardCharsets.UTF_8.decode(item))
}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

captureRequestBody() decodes each emitted ByteBuffer chunk independently via UTF_8.decode(item). If the publisher splits a multi-byte UTF-8 sequence across buffers, this can corrupt the decoded string and make tests flaky for non-ASCII payloads. Consider collecting the bytes (or using a CharsetDecoder that preserves partial sequences across chunks) and decoding once at the end.

Copilot uses AI. Check for mistakes.
@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot You are a coding agent tasked with resolving all open comments and review threads
on this pull request. Work through every comment systematically and apply the
appropriate code changes.


Instructions

1. Discover all comments

  • Read every review thread, inline comment, and general PR comment
  • Include comments from: Copilot, human reviewers, and any other bots
  • Do not skip comments marked as suggestions — apply them if they make sense

2. For each comment, do the following:

  • Understand what is being requested or flagged
  • Locate the relevant file(s) and line(s)
  • Apply the fix directly in code
  • If a Copilot suggestion diff is provided, apply it (or an equivalent fix)
  • If the comment is ambiguous, use your best judgment and reply in the PR thread or review comment explaining what you did; only add a code comment when explicitly requested or when future maintainers would benefit from it
  • After addressing a comment, reply to the thread summarising what you did and mark it as resolved if your environment supports it

3. Types of issues to handle:

  • Code quality / style issues (naming, formatting, readability)
  • Logic bugs or edge cases flagged by reviewers
  • Security concerns (e.g. input validation, secrets, injection risks)
  • Performance suggestions
  • Missing or incomplete error handling
  • Test coverage gaps
  • Documentation / comment improvements
  • Dependency or import issues

4. Do NOT:

  • Dismiss or ignore any comment without acting on it — note that "acting" can also mean replying to the thread with a rationale (e.g., won't fix, already addressed elsewhere) and leaving the code unchanged when appropriate
  • Leave TODO comments as a substitute for fixing the issue
  • Break existing tests while addressing comments
  • Change functionality beyond the scope of what the comment asks for

After making changes

  • Ensure all existing tests still pass
  • Add or update tests if a comment explicitly asks for them, or if a bug fix warrants it
  • Double-check that every open thread has been addressed, replied to, and resolved

Context

  • This PR may contain Copilot-generated review comments — treat these with the same priority as human comments
  • If two comments conflict with each other, prefer the human reviewer's intent and document the conflict and resolution in the PR conversation or commit message, not in a code comment
  • Commit changes in logical groups if there are many unrelated fixes

…teBuffer decoding

Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/c04624da-5bf9-4802-b321-1fd8e297e5df

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 12, 2026

@copilot You are a coding agent tasked with resolving all open comments and review threads
on this pull request. Work through every comment systematically and apply the
appropriate code changes....

Addressed all 3 open review comments in 665b3be:

  1. URL duplication fixdpc-api.url is now the full endpoint URL (default: https://dansplugins.com/api/v1/factions). Removed the hard-coded /api/v1/factions append that could cause URL duplication if operators included the path.
  2. Empty server IP log spam — Added emptyServerIpWarningLogged flag to log the warning only once per plugin enable instead of every sync cycle. Warning now explicitly instructs operators to set dpc-api.server-address.
  3. ByteBuffer decodingcaptureRequestBody() now collects raw bytes from all chunks then decodes once at the end using String(combined, UTF_8) to correctly handle multi-byte UTF-8 sequences split across buffers.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • hub.spigotmc.org
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=256m (dns block)
  • jitpack.io
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=256m (dns block)
  • repo.dansplugins.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=256m (dns block)
  • repo.extendedclip.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=256m (dns block)
  • repo.mikeprimm.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=256m (dns block)
  • repo.rpkit.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=256m (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants