feat: kick_player can target a publicId (admin bot) (#4403)

## Description:

Add an optional `targetPublicId` to KickPlayerIntent; the server
resolves it against the connected clients to the live clientID, then
kicks as before. Existing clientID targeting (lobby / in-game kick) is
unchanged. That way you can kick player with both the clientID and
playerID

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory

## Please put your Discord username so you can be contacted if a bug or
regression is found:

zixer._
This commit is contained in:
Zixer1
2026-06-24 17:34:17 -04:00
committed by GitHub
parent 82efcecb80
commit 8ce5f3439c
5 changed files with 49 additions and 8 deletions
+1 -1
View File
@@ -640,7 +640,7 @@ export class Transport {
private onSendKickPlayerIntent(event: SendKickPlayerIntentEvent) {
this.sendIntent({
type: "kick_player",
target: event.target,
targetClientID: event.target,
});
}
+5 -1
View File
@@ -477,7 +477,11 @@ export const MarkDisconnectedIntentSchema = z.object({
export const KickPlayerIntentSchema = z.object({
type: z.literal("kick_player"),
target: ID,
// Either a live clientID (lobby / in-game kick) OR an account publicID, for
// callers that identify a player by account rather than per-session clientID;
// the server resolves the publicID to the live clientID. Exactly one is set.
targetClientID: ID.optional(),
targetPublicID: ID.optional(),
});
export const TogglePauseIntentSchema = z.object({
+15 -3
View File
@@ -265,7 +265,19 @@ export class GameServer {
error: "only the lobby creator or an admin can kick players",
};
}
if (stamped.clientID === stamped.target) {
// Resolve the target to a live clientID: an explicit clientID, or an
// account publicId matched against the connected clients (for callers
// that know the account but not the per-session clientID).
let target = stamped.targetClientID;
if (target === undefined && stamped.targetPublicID !== undefined) {
target = this.activeClients.find(
(c) => c.publicId === stamped.targetPublicID,
)?.clientID;
}
if (target === undefined) {
return { status: 404, error: "no matching player to kick" };
}
if (stamped.clientID === target) {
return { status: 400, error: "cannot kick yourself" };
}
const reason =
@@ -274,12 +286,12 @@ export class GameServer {
: KICK_REASON_LOBBY_CREATOR;
this.log.info("player kicked", {
kicker: stamped.clientID,
target: stamped.target,
target,
isAdmin: actor.isAdmin,
isAdminBot: actor.isAdminBot,
gameID: this.id,
});
this.kickClient(stamped.target, reason);
this.kickClient(target, reason);
return { status: 200 };
}
+27 -2
View File
@@ -115,7 +115,7 @@ describe("GameServer.handleIntent (admin bot)", () => {
const spy = vi.spyOn(game, "kickClient");
const result = apply(game, {
type: "kick_player",
target: "abcdABCD",
targetClientID: "abcdABCD",
} as any);
expect(result.status).toBe(200);
expect(spy).toHaveBeenCalledWith("abcdABCD", expect.any(String));
@@ -126,10 +126,35 @@ describe("GameServer.handleIntent (admin bot)", () => {
expect(
apply(game, {
type: "kick_player",
target: "abcdABCD",
targetClientID: "abcdABCD",
} as any).status,
).toBe(403);
});
it("resolves a publicID target to the connected client's clientID", () => {
const game = makeGame();
(game as any).activeClients.push({
clientID: "liveCID1",
publicId: "pubABCD1",
});
const spy = vi.spyOn(game, "kickClient");
const result = apply(game, {
type: "kick_player",
targetPublicID: "pubABCD1",
} as any);
expect(result.status).toBe(200);
expect(spy).toHaveBeenCalledWith("liveCID1", expect.any(String));
});
it("404s when no connected client matches the publicID", () => {
const game = makeGame();
expect(
apply(game, {
type: "kick_player",
targetPublicID: "nobodyXX",
} as any).status,
).toBe(404);
});
});
describe("toggle_pause", () => {
+1 -1
View File
@@ -93,7 +93,7 @@ describe("GameServer - kick_player authorization", () => {
"message",
JSON.stringify({
type: "intent",
intent: { type: "kick_player", target },
intent: { type: "kick_player", targetClientID: target },
}),
);
}