add namelayer sync message contracts

This commit is contained in:
okx-code
2026-05-04 00:14:14 +01:00
parent eccbf91fdf
commit 1d9b12b177
12 changed files with 281 additions and 0 deletions

View File

@@ -24,6 +24,7 @@ commons-math3 = { group = "org.apache.commons", name = "commons-math3", version
cron-utils = { group = "com.cronutils", name = "cron-utils", version = "9.2.1" } cron-utils = { group = "com.cronutils", name = "cron-utils", version = "9.2.1" }
configurate-yaml = { group = "org.spongepowered", name = "configurate-yaml", version.ref = "configurate" } configurate-yaml = { group = "org.spongepowered", name = "configurate-yaml", version.ref = "configurate" }
gson = { group = "com.google.code.gson", name = "gson", version = "2.13.2" }
rabbitmq-client = { group = "com.rabbitmq", name = "amqp-client", version = "5.26.0" } rabbitmq-client = { group = "com.rabbitmq", name = "amqp-client", version = "5.26.0" }

View File

@@ -0,0 +1,7 @@
plugins {
id("java-library")
}
dependencies {
api(libs.gson)
}

View File

@@ -0,0 +1,32 @@
package net.civmc.namelayer.sync;
import java.util.Objects;
import java.util.Set;
public record NameLayerInvalidationMessage(Set<Integer> affectedGroupIds, boolean requiresFullResync) {
public NameLayerInvalidationMessage {
affectedGroupIds = Set.copyOf(validateGroupIds(affectedGroupIds));
if (!requiresFullResync && affectedGroupIds.isEmpty()) {
throw new IllegalArgumentException("affectedGroupIds must not be empty unless requiresFullResync is true");
}
}
public static NameLayerInvalidationMessage targeted(final Set<Integer> affectedGroupIds) {
return new NameLayerInvalidationMessage(affectedGroupIds, false);
}
public static NameLayerInvalidationMessage fullResync() {
return new NameLayerInvalidationMessage(Set.of(), true);
}
private static Set<Integer> validateGroupIds(final Set<Integer> groupIds) {
Objects.requireNonNull(groupIds, "affectedGroupIds");
for (final Integer groupId : groupIds) {
if (groupId == null || groupId <= 0) {
throw new IllegalArgumentException("affectedGroupIds must contain only positive group IDs");
}
}
return groupIds;
}
}

View File

@@ -0,0 +1,16 @@
package net.civmc.namelayer.sync;
public final class NameLayerRabbitMqTopology {
public static final String CONTENT_TYPE_JSON = "application/json";
public static final String INVALIDATION_EXCHANGE = "namelayer.invalidations";
public static final String INVALIDATION_EXCHANGE_TYPE = "fanout";
public static final boolean INVALIDATION_EXCHANGE_DURABLE = true;
public static final String PAPER_INVALIDATION_QUEUE_PREFIX = "namelayer.invalidations.";
public static final String WRITE_REQUEST_QUEUE = "namelayer.write.requests";
public static final boolean WRITE_REQUEST_QUEUE_DURABLE = true;
public static final String WRITE_RESPONSE_QUEUE_PREFIX = "namelayer.write.responses.";
private NameLayerRabbitMqTopology() {
}
}

View File

@@ -0,0 +1,55 @@
package net.civmc.namelayer.sync;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public final class NameLayerSyncCodec {
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create();
private NameLayerSyncCodec() {
}
public static byte[] encodeWriteRequest(final NameLayerWriteRequest request) {
return encode(request);
}
public static NameLayerWriteRequest decodeWriteRequest(final byte[] body) {
return decode(body, NameLayerWriteRequest.class);
}
public static byte[] encodeWriteResponse(final NameLayerWriteResponse response) {
return encode(response);
}
public static NameLayerWriteResponse decodeWriteResponse(final byte[] body) {
return decode(body, NameLayerWriteResponse.class);
}
public static byte[] encodeInvalidation(final NameLayerInvalidationMessage invalidation) {
return encode(invalidation);
}
public static NameLayerInvalidationMessage decodeInvalidation(final byte[] body) {
return decode(body, NameLayerInvalidationMessage.class);
}
private static byte[] encode(final Object message) {
Objects.requireNonNull(message, "message");
return GSON.toJson(message).getBytes(StandardCharsets.UTF_8);
}
private static <T> T decode(final byte[] body, final Class<T> messageType) {
Objects.requireNonNull(body, "body");
Objects.requireNonNull(messageType, "messageType");
try {
return GSON.fromJson(new String(body, StandardCharsets.UTF_8), messageType);
} catch (final JsonParseException exception) {
throw new IllegalArgumentException("Invalid NameLayer synchronization message", exception);
}
}
}

View File

@@ -0,0 +1,13 @@
package net.civmc.namelayer.sync;
public enum NameLayerWriteFailureCode {
AUTHORIZATION_FAILED,
DATABASE_UNAVAILABLE,
GROUP_NOT_FOUND,
INVALID_REQUEST,
MEMBER_NOT_FOUND,
NAME_CONFLICT,
PLAYER_NOT_FOUND,
UNKNOWN_OPERATION,
WRITE_FAILED
}

View File

@@ -0,0 +1,24 @@
package net.civmc.namelayer.sync;
public enum NameLayerWriteOperation {
ADD_BLACKLIST,
ADD_INVITATION,
ADD_PERMISSION,
ACCEPT_INVITATION,
CREATE_GROUP,
DELETE_GROUP,
JOIN_GROUP,
LEAVE_GROUP,
REMOVE_BLACKLIST,
REMOVE_INVITATION,
REMOVE_MEMBER,
REMOVE_PERMISSION,
RENAME_GROUP,
SET_AUTO_ACCEPT,
SET_DEFAULT_GROUP,
SET_GROUP_COLOR,
SET_GROUP_DISCIPLINE,
SET_GROUP_OWNER,
SET_GROUP_PASSWORD,
SET_MEMBER_ROLE
}

View File

@@ -0,0 +1,52 @@
package net.civmc.namelayer.sync;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
public record NameLayerWriteRequest(
UUID requestId,
String originServerId,
UUID actorUuid,
NameLayerWriteOperation operation,
Map<String, String> arguments,
long createdAtEpochMillis
) {
public NameLayerWriteRequest {
Objects.requireNonNull(requestId, "requestId");
originServerId = requireNonBlank(originServerId, "originServerId");
Objects.requireNonNull(actorUuid, "actorUuid");
Objects.requireNonNull(operation, "operation");
arguments = Map.copyOf(Objects.requireNonNull(arguments, "arguments"));
if (createdAtEpochMillis <= 0) {
throw new IllegalArgumentException("createdAtEpochMillis must be positive");
}
}
public static NameLayerWriteRequest create(
final String originServerId,
final UUID actorUuid,
final NameLayerWriteOperation operation,
final Map<String, String> arguments
) {
return new NameLayerWriteRequest(
UUID.randomUUID(),
originServerId,
actorUuid,
operation,
new LinkedHashMap<>(arguments),
System.currentTimeMillis()
);
}
private static String requireNonBlank(final String value, final String fieldName) {
Objects.requireNonNull(value, fieldName);
final String trimmed = value.trim();
if (trimmed.isEmpty()) {
throw new IllegalArgumentException(fieldName + " must not be blank");
}
return trimmed;
}
}

View File

@@ -0,0 +1,10 @@
package net.civmc.namelayer.sync;
public final class NameLayerWriteRequestPolicy {
public static final long DEFAULT_TIMEOUT_MILLIS = 5_000L;
public static final int AUTOMATIC_RETRY_ATTEMPTS = 0;
private NameLayerWriteRequestPolicy() {
}
}

View File

@@ -0,0 +1,69 @@
package net.civmc.namelayer.sync;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
public record NameLayerWriteResponse(
UUID requestId,
boolean success,
NameLayerWriteFailureCode failureCode,
String message,
Set<Integer> affectedGroupIds,
boolean requiresFullResync,
long completedAtEpochMillis
) {
public NameLayerWriteResponse {
Objects.requireNonNull(requestId, "requestId");
if (success && failureCode != null) {
throw new IllegalArgumentException("successful responses must not include a failureCode");
}
if (!success) {
Objects.requireNonNull(failureCode, "failureCode");
}
message = message == null ? "" : message;
affectedGroupIds = Set.copyOf(validateGroupIds(affectedGroupIds));
if (completedAtEpochMillis <= 0) {
throw new IllegalArgumentException("completedAtEpochMillis must be positive");
}
}
public static NameLayerWriteResponse success(final UUID requestId, final Set<Integer> affectedGroupIds) {
return new NameLayerWriteResponse(
requestId,
true,
null,
"",
affectedGroupIds,
false,
System.currentTimeMillis()
);
}
public static NameLayerWriteResponse failure(
final UUID requestId,
final NameLayerWriteFailureCode failureCode,
final String message
) {
return new NameLayerWriteResponse(
requestId,
false,
failureCode,
message,
Set.of(),
false,
System.currentTimeMillis()
);
}
private static Set<Integer> validateGroupIds(final Set<Integer> groupIds) {
Objects.requireNonNull(groupIds, "affectedGroupIds");
for (final Integer groupId : groupIds) {
if (groupId == null || groupId <= 0) {
throw new IllegalArgumentException("affectedGroupIds must contain only positive group IDs");
}
}
return groupIds;
}
}

View File

@@ -12,4 +12,5 @@ dependencies {
compileOnly(project(":plugins:civmodcore-paper")) compileOnly(project(":plugins:civmodcore-paper"))
api(project(":libraries:name-api")) api(project(":libraries:name-api"))
api(project(":libraries:namelayer-sync"))
} }

View File

@@ -14,6 +14,7 @@ plugins {
include(":ansible") include(":ansible")
include(":libraries:name-api") include(":libraries:name-api")
include(":libraries:namelayer-sync")
include(":plugins:announcements-velocity") include(":plugins:announcements-velocity")
include(":plugins:banstick-paper") include(":plugins:banstick-paper")