@zaisma/game-server 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bench/glass.js +458 -149
- package/bin/local-play.js +7 -1
- package/bin/node.js +33 -4
- package/package.json +1 -1
- package/sdk/zaisma.d.ts +102 -12
- package/sdk/zaisma.js +90 -5
- package/src/chat.d.ts +23 -8
- package/src/chat.js +21 -11
- package/src/index.d.ts +2 -2
- package/src/index.js +1 -1
- package/src/pass.d.ts +9 -8
- package/src/pass.js +10 -8
- package/src/room.d.ts +148 -3
- package/src/room.js +203 -2
package/bin/local-play.js
CHANGED
|
@@ -1215,6 +1215,12 @@ export async function startLocalPlay(givenOptions) {
|
|
|
1215
1215
|
}
|
|
1216
1216
|
return notLocal();
|
|
1217
1217
|
}
|
|
1218
|
+
// Zaisma's chat check, as the runner's rooms ask it for room.chat (checkChat): the bench lets every message through,
|
|
1219
|
+
// nothing flagged, the way /v1/filter stands in below. Zaisma's own filter runs once the game is on Zaisma.
|
|
1220
|
+
if (request.method === "POST" && path === "/v1/chat/check") {
|
|
1221
|
+
await readBody();
|
|
1222
|
+
return reply(200, { isAllowed: true, reason: null, message: null, hasSwearing: false, hasSlur: false });
|
|
1223
|
+
}
|
|
1218
1224
|
if (request.method === "POST" && ["/v1/pass", "/v1/rooms", "/v1/report", "/v1/filter", "/v1/friends", "/v1/friends/room", "/v1/items/unhandled", "/v1/items/handled"].includes(path)) {
|
|
1219
1225
|
if (!isGameOrigin(origin)) return fail(403, "bad_origin", "Not allowed from here.");
|
|
1220
1226
|
const body = await readBody();
|
|
@@ -1225,7 +1231,7 @@ export async function startLocalPlay(givenOptions) {
|
|
|
1225
1231
|
return reply(200, await passFor(given.as, given.playerNumber));
|
|
1226
1232
|
}
|
|
1227
1233
|
if (path === "/v1/rooms") return reply(200, { roomCode: roomCode() });
|
|
1228
|
-
if (path === "/v1/filter") return reply(200, { isAllowed: true, reason: null, message: null, hasSwearing: false, text: String(body.text ?? ""), is_stand_in: true });
|
|
1234
|
+
if (path === "/v1/filter") return reply(200, { isAllowed: true, reason: null, message: null, hasSwearing: false, hasSlur: false, text: String(body.text ?? ""), is_stand_in: true });
|
|
1229
1235
|
if (path === "/v1/report") {
|
|
1230
1236
|
log(`Report (not sent anywhere): ${JSON.stringify({ roomCode: body.roomCode, playerId: body.playerId, playerName: body.playerName, reason: body.reason, details: body.details })}`);
|
|
1231
1237
|
return reply(200, { isReported: true });
|
package/bin/node.js
CHANGED
|
@@ -22,7 +22,7 @@ import { createServer } from "node:http";
|
|
|
22
22
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
23
23
|
import { resolve } from "node:path";
|
|
24
24
|
import { realpathSync } from "node:fs";
|
|
25
|
-
import { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, ERROR_TEXT, NO_ANSWER_CLOSE_CODE, PASS_ISSUER_URL, PassError, ReceiptError, REMOVED_CLOSE_CODE, REQUEST_ERROR_HTTP_STATUS, REQUEST_LIMITS, ROOM_LIMITS, ROOM_TUNING, WEBHOOK_LIMITS, GameServerError, RoomCore, restoreInfoOf, passesAreFriends, passesHaveBlockBetween, answerRequest, startSchedules, checkRoomData, clientIdle, defineGameServer, errorMessage, isRoomCode, readRequestBody, relay, requestError, runWebhook, tidyRoomInfo, verifyPass, verifyReceipt, verifyWebhook } from "../src/internal.js";
|
|
25
|
+
import { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, ERROR_TEXT, NO_ANSWER_CLOSE_CODE, PASS_ISSUER_URL, PassError, ReceiptError, REMOVED_CLOSE_CODE, REQUEST_ERROR_HTTP_STATUS, REQUEST_LIMITS, ROOM_LIMITS, ROOM_TUNING, WEBHOOK_LIMITS, GameServerError, RoomCore, chatSettingsOf, checkChat, readChatMessage, restoreInfoOf, passesAreFriends, passesHaveBlockBetween, answerRequest, startSchedules, checkRoomData, clientIdle, defineGameServer, errorMessage, isRoomCode, readRequestBody, relay, requestError, runWebhook, tidyRoomInfo, verifyPass, verifyReceipt, verifyWebhook } from "../src/internal.js";
|
|
26
26
|
import { OutsideCallError } from "../src/outside-calls.js";
|
|
27
27
|
import { checkedStartServerOptions, localDatabase, localRoomStates, passKeysFrom, profilesFrom, receiptKeysFrom, secretsFromEnvironment } from "./node-parts.js";
|
|
28
28
|
|
|
@@ -290,10 +290,11 @@ export function startServer(options) {
|
|
|
290
290
|
saved ? { state: saved.state, room: saved.savedRoom, ...(definition.ticksPerSec || saved.matchPlayers?.length ? { restoreInfo: restoreInfoOf(saved.savedAtMs, Date.now(), null, saved.isSavedEmpty), lostPlayers: saved.matchPlayers } : {}) } : { start },
|
|
291
291
|
);
|
|
292
292
|
if (saved) core.savedAtMs = saved.savedAtMs;
|
|
293
|
-
// passes: each player's checked pass, newest, for the blocks and friendships between them (passesHaveBlockBetween, passesAreFriends)
|
|
293
|
+
// passes: each player's checked pass, newest, for the blocks and friendships between them (passesHaveBlockBetween, passesAreFriends);
|
|
294
|
+
// passTexts: the same passes as the players sent them, for Zaisma's chat filter (checkChat).
|
|
294
295
|
// savedStateKey: what the room last saved (its state and its match's players), so an unchanged one isn't written again.
|
|
295
296
|
// seatedAtMs: when each player came into this room on their own, for the game's one-room rule (the oldest seat closes first).
|
|
296
|
-
room = { core, sockets, passes: new Map(), seatedAtMs: new Map(), timer: null, probeTimer: null, waiting: false, savedStateKey: saved ? stateKeyOf(saved.stateJson, saved.matchPlayers, saved.savedRoom.isKept === true) : null, hasReportedStateSize: false };
|
|
297
|
+
room = { core, sockets, passes: new Map(), passTexts: new Map(), seatedAtMs: new Map(), timer: null, probeTimer: null, waiting: false, savedStateKey: saved ? stateKeyOf(saved.stateJson, saved.matchPlayers, saved.savedRoom.isKept === true) : null, hasReportedStateSize: false };
|
|
297
298
|
rooms.set(code, room);
|
|
298
299
|
return room;
|
|
299
300
|
}
|
|
@@ -402,6 +403,24 @@ export function startServer(options) {
|
|
|
402
403
|
}
|
|
403
404
|
}
|
|
404
405
|
|
|
406
|
+
/**
|
|
407
|
+
* A player's chat message (room.chat): the room checks it and asks the game server's onChat (startChat),
|
|
408
|
+
* Zaisma's filter looks (checkChat, with the passes of those who may read it), and it goes to them
|
|
409
|
+
* (deliverChat) or the sender hears why not (stopChat). Everyone it may reach is in the sender's chat
|
|
410
|
+
* group, so the first CHAT_CHECK_LIMITS.maximumReceiverCount of them stand for the rest.
|
|
411
|
+
*/
|
|
412
|
+
async function chatIn(room, playerId, chat) {
|
|
413
|
+
const started = await room.core.startChat(playerId, chat);
|
|
414
|
+
if (!started) return;
|
|
415
|
+
let check = { isAllowed: true, reason: null, message: null, hasSwearing: false, hasSlur: false };
|
|
416
|
+
if (started.receiverPlayerIds.length) {
|
|
417
|
+
const receiverPasses = started.receiverPlayerIds.map((id) => room.passTexts.get(id)).filter((pass) => typeof pass === "string");
|
|
418
|
+
check = await checkChat({ text: started.text, senderPass: room.passTexts.get(playerId) ?? "", receiverPasses, isAdultFilterEnabled: definition.adultChatFilterEnabled === true });
|
|
419
|
+
}
|
|
420
|
+
if (check.isAllowed) room.core.deliverChat(playerId, chat.chatNumber, started.receiverPlayerIds, { text: started.text, hasSwearing: check.hasSwearing, hasSlur: check.hasSlur });
|
|
421
|
+
else room.core.stopChat(playerId, chat.chatNumber, check.reason ?? "unchecked", check.message ?? "");
|
|
422
|
+
}
|
|
423
|
+
|
|
405
424
|
/** Keep a room for players on their way, even if it empties meanwhile. */
|
|
406
425
|
function wait(code, room, durationMs) {
|
|
407
426
|
room.waiting = true;
|
|
@@ -489,6 +508,9 @@ export function startServer(options) {
|
|
|
489
508
|
if (text === "ping") return ws.send("pong");
|
|
490
509
|
if (playerId) {
|
|
491
510
|
const room = rooms.get(code);
|
|
511
|
+
// Chat goes through Zaisma's filter (checkChat) on its way, so the room takes it before the game's messages.
|
|
512
|
+
const chat = text === null ? null : readChatMessage(text);
|
|
513
|
+
if (chat && room) return chatIn(room, playerId, chat);
|
|
492
514
|
// Bytes (a binary frame) go to the game server's onBytes, under the same limits as messages.
|
|
493
515
|
const errorCode = text === null ? room?.core.receiveBytes(playerId, binaryBytes(received)) : room?.core.receive(playerId, text);
|
|
494
516
|
if (errorCode) ws.send(errorMessage(errorCode));
|
|
@@ -537,9 +559,16 @@ export function startServer(options) {
|
|
|
537
559
|
room.sockets.set(player.id, ws);
|
|
538
560
|
// Blocks between accounts come in their passes: this player's and those of everyone the room remembers.
|
|
539
561
|
// Spectators send nothing, so nothing of theirs reaches a player to block.
|
|
562
|
+
// Who the player may chat with, from their pass: the room keeps it, and the game never sees it. Spectators send nothing, chat included.
|
|
563
|
+
room.core.setChatSettings(player.id, isSpectator ? null : chatSettingsOf(claims));
|
|
540
564
|
if (!isSpectator) {
|
|
541
565
|
room.passes.set(player.id, claims);
|
|
542
|
-
|
|
566
|
+
room.passTexts.set(player.id, pass);
|
|
567
|
+
for (const id of room.passes.keys()) {
|
|
568
|
+
if (room.core.known.has(id) || id === player.id) continue;
|
|
569
|
+
room.passes.delete(id);
|
|
570
|
+
room.passTexts.delete(id);
|
|
571
|
+
}
|
|
543
572
|
room.core.setAccountBlocks(player.id, [...room.passes].filter(([id, other]) => id !== player.id && passesHaveBlockBetween(claims, other)).map(([id]) => id));
|
|
544
573
|
room.core.setFriends(player.id, [...room.passes].filter(([id, other]) => id !== player.id && passesAreFriends(claims, other)).map(([id]) => id));
|
|
545
574
|
}
|
package/package.json
CHANGED
package/sdk/zaisma.d.ts
CHANGED
|
@@ -51,8 +51,9 @@ declare namespace Zaisma {
|
|
|
51
51
|
/**
|
|
52
52
|
* This tab's player, with their player pass: the signed-in Zaisma account, with the same player ID in
|
|
53
53
|
* every game, or a guest. joinRoom gets one by itself; call this to let the player into a server of
|
|
54
|
-
* your own.
|
|
55
|
-
* under 10 minutes left or the player
|
|
54
|
+
* your own. A guest's is kept for the tab, so a guest who reloads stays the same guest, and renewed when
|
|
55
|
+
* it has under 10 minutes left or the player signs in; an account gets a new one each time the game
|
|
56
|
+
* loads, for whoever is signed in then. Fails with error.code "unchecked" when the
|
|
56
57
|
* game isn't played on its zaisma.com page, "not_on_zaisma" when it isn't hosted on Zaisma, and
|
|
57
58
|
* "account_needed" for a guest when the game needs an account for multiplayer or the whole game
|
|
58
59
|
* (its account_requirement setting): ask them to sign in with Zaisma.player.signIn().
|
|
@@ -176,10 +177,10 @@ declare namespace Zaisma {
|
|
|
176
177
|
/**
|
|
177
178
|
* Zaisma's chat check: the same check Zaisma runs on its side on every chat message to or from an
|
|
178
179
|
* under-18 account. It stops contact details in any disguise, moving to another app or meeting up,
|
|
179
|
-
* personal details
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* text, up to 2000 characters; a longer one is refused (too_long), never cut.
|
|
180
|
+
* personal details and sexual content, and answers with the words to show the sender. Swearing and
|
|
181
|
+
* slurs are flagged (hasSwearing, hasSlur) but allowed: blocking them is your game's call. For chat
|
|
182
|
+
* between players use {@link Room.chat}, which runs this check on its way; this one is for other text
|
|
183
|
+
* players type. Checks the whole text, up to 2000 characters; a longer one is refused (too_long), never cut.
|
|
183
184
|
*
|
|
184
185
|
* @example
|
|
185
186
|
* const check = await Zaisma.checkChat(typedMessage);
|
|
@@ -997,6 +998,11 @@ declare namespace Zaisma {
|
|
|
997
998
|
readonly name: string | null;
|
|
998
999
|
/** The room's labels, like { Mode: "Classic", Map: "Docks" }. */
|
|
999
1000
|
readonly labels: Readonly<Record<string, string>>;
|
|
1001
|
+
/**
|
|
1002
|
+
* Whether this player can chat at all ({@link Room.chat}): a Zaisma account whose chat setting isn't
|
|
1003
|
+
* nobody, playing (not watching). False for guests: hide your chat box, and offer preset phrases or emotes.
|
|
1004
|
+
*/
|
|
1005
|
+
readonly canChat: boolean;
|
|
1000
1006
|
/**
|
|
1001
1007
|
* When Zaisma will end this match, an ISO time, or null: set once the game's hosting pauses for the
|
|
1002
1008
|
* month, an hour after the pause (the "ending" event). Never set in a room on your own server.
|
|
@@ -1133,8 +1139,31 @@ declare namespace Zaisma {
|
|
|
1133
1139
|
* player changed their mind. A block never removes anyone from the room.
|
|
1134
1140
|
*/
|
|
1135
1141
|
block(playerId: string): Promise<boolean>;
|
|
1136
|
-
/**
|
|
1142
|
+
/**
|
|
1143
|
+
* Report a player to Zaisma, with what happened in the player's own words if they gave any (details).
|
|
1144
|
+
* The last chat lines (up to 20) they sent this player, or this player sent them, go with it from this
|
|
1145
|
+
* screen; nothing else about the reporting player is sent.
|
|
1146
|
+
*/
|
|
1137
1147
|
report(playerId: string, reason: ReportReason, details?: string): Promise<ReportAnswer>;
|
|
1148
|
+
/**
|
|
1149
|
+
* Send a chat message: through the room to the players in it (or chatOptions.toPlayerIds of them) who
|
|
1150
|
+
* may chat with this player, on Zaisma's rooms or your own server's runner. It reaches only players in
|
|
1151
|
+
* this player's chat group whose settings let them chat with this player, never anyone with a block
|
|
1152
|
+
* between them, and never spectators. Your game server's onChat may stop it or send it to fewer
|
|
1153
|
+
* players first; then Zaisma's chat filter checks it on its way (messages to and from under-18s
|
|
1154
|
+
* always, between adults when your game server turns it on), and Zaisma keeps none of it. Draw the
|
|
1155
|
+
* chat yourself: the "chat" event brings the others' messages; show this player's own when it resolves.
|
|
1156
|
+
*
|
|
1157
|
+
* Resolves { sentAt } once it went, even when nobody could read it (the sender is never told who
|
|
1158
|
+
* can't). Rejects with error.code chat_stopped (error.reason says why, like contact_details or
|
|
1159
|
+
* stopped_by_game, and error.message is what to show the player), chat_not_allowed (a guest, or chat
|
|
1160
|
+
* set to nobody: check {@link Room.canChat} first), chat_empty, chat_too_long (over 2000 characters),
|
|
1161
|
+
* chat_too_fast (over 20 in 10 seconds) or connection_closed.
|
|
1162
|
+
*
|
|
1163
|
+
* @example
|
|
1164
|
+
* room.chat(input.value).then(({ sentAt }) => addLine(room.me.name, input.value), (error) => showError(error.message));
|
|
1165
|
+
*/
|
|
1166
|
+
chat(text: string, chatOptions?: ChatOptions): Promise<ChatSent>;
|
|
1138
1167
|
|
|
1139
1168
|
/**
|
|
1140
1169
|
* Listen for an event. Listening is optional: without "move" and "back" listeners, the SDK follows
|
|
@@ -1241,8 +1270,53 @@ declare namespace Zaisma {
|
|
|
1241
1270
|
* room.on("versionUpdate", ({ appliesAt }) => showBanner(`Updating at ${new Date(appliesAt).toLocaleTimeString()}`));
|
|
1242
1271
|
*/
|
|
1243
1272
|
versionUpdate: (versionUpdate: VersionUpdate) => void;
|
|
1273
|
+
/**
|
|
1274
|
+
* Another player's chat message reached this player ({@link Room.chat}). Messages from players this
|
|
1275
|
+
* player muted or blocked never come. hasSwearing and hasSlur flag what Zaisma allows: hide or blur
|
|
1276
|
+
* them if your game doesn't. (Optional here only so games written for v1's events still fit.)
|
|
1277
|
+
*/
|
|
1278
|
+
chat?: (chatMessage: ChatMessage) => void;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/** What {@link Room.chat} takes. */
|
|
1282
|
+
interface ChatOptions {
|
|
1283
|
+
/** Send it to only these players in the room (like a team), from room.players. All players when left out. */
|
|
1284
|
+
toPlayerIds?: string[];
|
|
1244
1285
|
}
|
|
1245
1286
|
|
|
1287
|
+
/** {@link Room.chat}'s answer once the message went. */
|
|
1288
|
+
interface ChatSent {
|
|
1289
|
+
/** When the room sent it, as an ISO time. */
|
|
1290
|
+
sentAt: string;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/** A chat message another player sent ({@link Room.chat}), as the "chat" event brings it. */
|
|
1294
|
+
interface ChatMessage {
|
|
1295
|
+
fromPlayerId: string;
|
|
1296
|
+
text: string;
|
|
1297
|
+
/** It swears: Zaisma allows it; hide it if your game doesn't. */
|
|
1298
|
+
hasSwearing: boolean;
|
|
1299
|
+
/** It has a slur: Zaisma allows it too; your game and its moderators decide. */
|
|
1300
|
+
hasSlur: boolean;
|
|
1301
|
+
/** When the room sent it, as an ISO time. */
|
|
1302
|
+
sentAt: string;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* The codes {@link Room.chat} rejects with: chat_stopped (the game server's onChat or Zaisma's filter
|
|
1307
|
+
* stopped it: error.reason and error.message say why), chat_not_allowed, chat_empty, chat_too_long,
|
|
1308
|
+
* chat_too_fast, bad_player_id, busy (try again in a moment), ended, spectator_cannot_send and
|
|
1309
|
+
* connection_closed. More codes may come within v1: show the error's message.
|
|
1310
|
+
*/
|
|
1311
|
+
type ChatErrorCode = "chat_stopped" | "chat_not_allowed" | "chat_empty" | "chat_too_long" | "chat_too_fast" | "chat_not_here" | "bad_player_id" | "busy" | "ended" | "spectator_cannot_send" | "connection_closed";
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Why {@link Room.chat} was stopped (error.reason with chat_stopped): "stopped_by_game" (your game
|
|
1315
|
+
* server's onChat), the filter's {@link ChatCheckReason}s, "chat_paused" (the sender's chat is paused
|
|
1316
|
+
* for now) or "not_allowed_to_chat". More values may come.
|
|
1317
|
+
*/
|
|
1318
|
+
type ChatStoppedReason = "stopped_by_game" | ChatCheckReason | "chat_paused" | "not_allowed_to_chat";
|
|
1319
|
+
|
|
1246
1320
|
/** What {@link Room.smoothedState} takes. */
|
|
1247
1321
|
interface SmoothingOptions {
|
|
1248
1322
|
/** How far behind the room's clock to draw, in milliseconds, from 0 to 1000. By default 100. */
|
|
@@ -1467,6 +1541,7 @@ declare namespace Zaisma {
|
|
|
1467
1541
|
* - already_playing: a player in the room tried to watch it too.
|
|
1468
1542
|
* - spectator_cannot_send: a spectator tried to send to the room ({@link Room.send}, {@link Room.sendBytes}).
|
|
1469
1543
|
* - send_queue_full: the SDK's own: more than 60 reliable sends waited while the room wasn't reachable, so the oldest were dropped. Send less while connectionStatus isn't open, or use latest_only for updates that replace each other.
|
|
1544
|
+
* - chat_not_allowed, chat_empty, chat_too_long, chat_too_fast: {@link Room.chat} refused (they reject its promise, not the "error" event); chat_not_here: the room's server doesn't take chat.
|
|
1470
1545
|
*
|
|
1471
1546
|
* More codes may come within v1: handle one you don't know like an error you can show (its message).
|
|
1472
1547
|
*/
|
|
@@ -1494,7 +1569,12 @@ declare namespace Zaisma {
|
|
|
1494
1569
|
| "spectators_full"
|
|
1495
1570
|
| "already_playing"
|
|
1496
1571
|
| "spectator_cannot_send"
|
|
1497
|
-
| "send_queue_full"
|
|
1572
|
+
| "send_queue_full"
|
|
1573
|
+
| "chat_not_here"
|
|
1574
|
+
| "chat_not_allowed"
|
|
1575
|
+
| "chat_empty"
|
|
1576
|
+
| "chat_too_long"
|
|
1577
|
+
| "chat_too_fast";
|
|
1498
1578
|
|
|
1499
1579
|
/**
|
|
1500
1580
|
* The codes a request's errors have ({@link Server.request}), from Zaisma or the Node runner alike:
|
|
@@ -1551,6 +1631,7 @@ declare namespace Zaisma {
|
|
|
1551
1631
|
* - bad_idle_timeout: an idleTimeoutSec that isn't from 30 to 86400.
|
|
1552
1632
|
* - request_failed: Zaisma didn't answer (no connection, or a problem on Zaisma's side); try again.
|
|
1553
1633
|
* - no_game: {@link Items.buy} named a game that isn't open to play here.
|
|
1634
|
+
* - chat_stopped, chat_not_allowed, chat_empty, chat_too_long, chat_too_fast, chat_not_here: {@link Room.chat} didn't send ({@link ChatErrorCode}).
|
|
1554
1635
|
* - And a request's ({@link RequestErrorCode}).
|
|
1555
1636
|
*
|
|
1556
1637
|
* More values may come; treat an unknown one as "request_failed".
|
|
@@ -1558,6 +1639,12 @@ declare namespace Zaisma {
|
|
|
1558
1639
|
type ErrorCode =
|
|
1559
1640
|
| RoomErrorCode
|
|
1560
1641
|
| RequestErrorCode
|
|
1642
|
+
| "chat_stopped"
|
|
1643
|
+
| "chat_not_allowed"
|
|
1644
|
+
| "chat_empty"
|
|
1645
|
+
| "chat_too_long"
|
|
1646
|
+
| "chat_too_fast"
|
|
1647
|
+
| "chat_not_here"
|
|
1561
1648
|
| "unchecked"
|
|
1562
1649
|
| "unknown_option"
|
|
1563
1650
|
| "unknown_event"
|
|
@@ -1636,6 +1723,8 @@ declare namespace Zaisma {
|
|
|
1636
1723
|
message: string | null;
|
|
1637
1724
|
/** Whether it swears. Zaisma allows swearing; block it yourself if your game doesn't. */
|
|
1638
1725
|
hasSwearing: boolean;
|
|
1726
|
+
/** Whether it has a slur. Zaisma allows it too; your game and its moderators decide. */
|
|
1727
|
+
hasSlur: boolean;
|
|
1639
1728
|
/** The text tidied up: hidden characters and extra spaces gone, line breaks kept (empty when it's too_long). */
|
|
1640
1729
|
text: string;
|
|
1641
1730
|
}
|
|
@@ -1643,10 +1732,11 @@ declare namespace Zaisma {
|
|
|
1643
1732
|
/**
|
|
1644
1733
|
* Why {@link checkChat} stopped a message: "contact_details" (a link, handle, email, phone number or
|
|
1645
1734
|
* contact app in any disguise, or moving to another app or meeting up), "personal_details" (where
|
|
1646
|
-
* someone lives, their school, their full name, photos), "
|
|
1647
|
-
*
|
|
1648
|
-
* ChatCheckReason has two more, "chat_paused" and
|
|
1649
|
-
* More values may come; treat an unknown one as a
|
|
1735
|
+
* someone lives, their school, their full name, photos), "sexual" or "too_long" (over 2000 characters).
|
|
1736
|
+
* "slur" and "unchecked" are v1's and no longer given: slurs are flagged (hasSlur), and what the AI can't
|
|
1737
|
+
* look at goes after the rules. The game server's ChatCheckReason has two more, "chat_paused" and
|
|
1738
|
+
* "not_allowed_to_chat", which only a room can give. More values may come; treat an unknown one as a
|
|
1739
|
+
* stop: show message.
|
|
1650
1740
|
*/
|
|
1651
1741
|
type ChatCheckReason = "contact_details" | "personal_details" | "slur" | "sexual" | "unchecked" | "too_long";
|
|
1652
1742
|
|
package/sdk/zaisma.js
CHANGED
|
@@ -29,7 +29,11 @@
|
|
|
29
29
|
var SMOOTHING_OPTIONS = ["delayMs"];
|
|
30
30
|
var DELIVERIES = ["reliable", "latest_only"];
|
|
31
31
|
var ROOM_INFO_FIELDS = ["name", "labels"];
|
|
32
|
-
var ROOM_EVENTS = ["message", "bytes", "state", "players", "connectionStatus", "ended", "ending", "kicked", "settings", "move", "moved", "idle", "away", "back", "muted", "error", "versionUpdate"];
|
|
32
|
+
var ROOM_EVENTS = ["message", "bytes", "state", "players", "connectionStatus", "ended", "ending", "kicked", "settings", "move", "moved", "idle", "away", "back", "muted", "error", "versionUpdate", "chat"];
|
|
33
|
+
/** Chat (room.chat): the longest message, the lines a room keeps in memory for reports, and how many of them a report carries. */
|
|
34
|
+
var MAXIMUM_CHAT_CHARACTER_COUNT = 2000;
|
|
35
|
+
var KEPT_CHAT_LINE_COUNT = 50;
|
|
36
|
+
var REPORTED_CHAT_LINE_COUNT = 20;
|
|
33
37
|
var SAVES_EVENTS = ["conflict", "developerChange"];
|
|
34
38
|
var ITEMS_EVENTS = ["purchase", "refund", "sheetOpen", "sheetClose"];
|
|
35
39
|
var VERSIONS_EVENTS = ["update"];
|
|
@@ -186,9 +190,11 @@
|
|
|
186
190
|
if (!config.rooms) return Promise.reject(notOnZaisma("Player passes come from Zaisma, so they work only in games played on Zaisma."));
|
|
187
191
|
if (!passPromise) {
|
|
188
192
|
passPromise = roomKey().then(function (given) {
|
|
189
|
-
//
|
|
193
|
+
// A guest keeps their pass, so a reload is the same guest. An account gets a new one with each page's key: the key is for
|
|
194
|
+
// whoever is signed in now (another account than the kept pass's, maybe), and its pass brings their chat settings, friends
|
|
195
|
+
// and blocks as they are now; the account's player ID is the same either way.
|
|
190
196
|
var kept = keptPass();
|
|
191
|
-
if (kept && kept.kind === given.playerKind) return kept;
|
|
197
|
+
if (kept && kept.kind === "guest" && given.playerKind === "guest") return kept;
|
|
192
198
|
return post("/v1/pass", { gameSlug: config.game, roomKey: given.key }).then(function (answer) {
|
|
193
199
|
var mine = { id: answer.player.id, name: answer.player.name, kind: answer.player.kind, pass: answer.pass, expiresAt: answer.expiresAt };
|
|
194
200
|
try { sessionStorage.setItem(passKey, JSON.stringify(mine)); } catch (error) { /* fine */ }
|
|
@@ -457,6 +463,12 @@
|
|
|
457
463
|
this._flushTimer = null;
|
|
458
464
|
this._isQueueFullSaid = false;
|
|
459
465
|
this._isWelcomed = false;
|
|
466
|
+
// Chat (room.chat): whether this player can chat at all (from welcome), the messages waiting for the
|
|
467
|
+
// room's answer by their chatNumber, and the room's last chat lines, in memory only, for reports.
|
|
468
|
+
this.canChat = false;
|
|
469
|
+
this._chatNumber = 0;
|
|
470
|
+
this._chats = new Map();
|
|
471
|
+
this._chatLines = [];
|
|
460
472
|
}
|
|
461
473
|
Room.prototype = Object.create(Emitter.prototype);
|
|
462
474
|
|
|
@@ -557,6 +569,7 @@
|
|
|
557
569
|
room._checks = message.awayChecks || { idleTimeoutSec: null, hiddenTimeoutSec: null };
|
|
558
570
|
room._probeIntervalSec = typeof message.probeIntervalSec === "number" ? message.probeIntervalSec : null;
|
|
559
571
|
room.spectatorCount = typeof message.spectatorCount === "number" ? message.spectatorCount : 0;
|
|
572
|
+
room.canChat = message.canChat === true && !room.isSpectator;
|
|
560
573
|
room._away = room._gone = room._idleSent = room._coming = room._offered = room._isReloadingForVersion = false;
|
|
561
574
|
room.awayReason = null;
|
|
562
575
|
room._isQueueFullSaid = false;
|
|
@@ -606,6 +619,8 @@
|
|
|
606
619
|
room._mutedChanged();
|
|
607
620
|
} else if (message.type === "message") {
|
|
608
621
|
room._emit("message", message.data, typeof message.fromPlayerId === "string" ? message.fromPlayerId : null);
|
|
622
|
+
} else if (message.type === "chat" || message.type === "chat_sent" || message.type === "chat_stopped" || (message.type === "error" && typeof message.chatNumber === "number")) {
|
|
623
|
+
room._chatAnswer(message);
|
|
609
624
|
} else if (message.type === "state") {
|
|
610
625
|
room._takeState(message);
|
|
611
626
|
} else if (message.type === "room") {
|
|
@@ -661,6 +676,7 @@
|
|
|
661
676
|
if (room._done || event.code === 4000 || event.code === 4001) {
|
|
662
677
|
room._done = true;
|
|
663
678
|
room._queue = [];
|
|
679
|
+
room._failChats();
|
|
664
680
|
room._setConnectionStatus("closed");
|
|
665
681
|
if (room._ready) {
|
|
666
682
|
room._ready.reject(codedError("connection_closed", "The connection closed before the room let the player in" + (event.reason ? ": " + event.reason : "") + "."));
|
|
@@ -1253,16 +1269,85 @@
|
|
|
1253
1269
|
});
|
|
1254
1270
|
return blockAsk;
|
|
1255
1271
|
}
|
|
1256
|
-
/**
|
|
1272
|
+
/**
|
|
1273
|
+
* Report a player to Zaisma. reason: real_life_threat, personal_details, following_after_block or sexual (Zaisma looks), or harassment, cheating or other (passed to the game's developer).
|
|
1274
|
+
* The last chat lines they sent you, or you sent them, go with it (up to 20); nothing else about you is sent.
|
|
1275
|
+
*/
|
|
1257
1276
|
Room.prototype.report = function (playerId, reason, details) {
|
|
1258
1277
|
var reported = this.players.filter(function (each) { return each.id === playerId; })[0];
|
|
1259
|
-
|
|
1278
|
+
var me = this.me ? this.me.id : null;
|
|
1279
|
+
var chatMessages = this._chatLines.filter(function (line) {
|
|
1280
|
+
return line.fromPlayerId === playerId || (line.fromPlayerId === me && (!line.toPlayerIds || line.toPlayerIds.indexOf(playerId) !== -1));
|
|
1281
|
+
}).slice(-REPORTED_CHAT_LINE_COUNT).map(function (line) { return { fromPlayerId: line.fromPlayerId, text: line.text, sentAt: line.sentAt }; });
|
|
1282
|
+
return post("/v1/report", { gameSlug: config.game, roomCode: this.roomCode, playerId: playerId, playerName: reported ? reported.name : "", reason: reason, details: details || "", chatMessages: chatMessages });
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Send a chat message (zaisma.d.ts: room.chat): through the room, to the players who may chat with this
|
|
1287
|
+
* one, after the game server's onChat and Zaisma's chat filter. Resolves { sentAt } once it went, even to
|
|
1288
|
+
* nobody; rejects with a coded error when it didn't.
|
|
1289
|
+
*/
|
|
1290
|
+
Room.prototype.chat = function (text, chatOptions) {
|
|
1291
|
+
var toPlayerIds = chatOptions && chatOptions.toPlayerIds;
|
|
1292
|
+
if (!this.canChat) return Promise.reject(codedError("chat_not_allowed", "You can't chat here."));
|
|
1293
|
+
if (typeof text !== "string" || !text.trim()) return Promise.reject(codedError("chat_empty", "Type a message first."));
|
|
1294
|
+
if (text.length > MAXIMUM_CHAT_CHARACTER_COUNT) return Promise.reject(codedError("chat_too_long", "That message is too long to send."));
|
|
1295
|
+
if (toPlayerIds !== undefined && !(Array.isArray(toPlayerIds) && toPlayerIds.every(function (id) { return typeof id === "string"; }))) {
|
|
1296
|
+
return Promise.reject(codedError("bad_player_id", "chatOptions.toPlayerIds takes a list of player IDs from room.players."));
|
|
1297
|
+
}
|
|
1298
|
+
if (this._done) return Promise.reject(codedError("connection_closed", "This room is closed."));
|
|
1299
|
+
var room = this;
|
|
1300
|
+
var chatNumber = ++this._chatNumber;
|
|
1301
|
+
var payload = { type: "chat", chatNumber: chatNumber, text: text };
|
|
1302
|
+
if (toPlayerIds) payload.toPlayerIds = toPlayerIds.slice();
|
|
1303
|
+
return new Promise(function (resolve, reject) {
|
|
1304
|
+
room._chats.set(chatNumber, { text: text, toPlayerIds: payload.toPlayerIds || null, resolve: resolve, reject: reject });
|
|
1305
|
+
// Waits in order with the other sends while the connection is back.
|
|
1306
|
+
room._sendReliable(JSON.stringify(payload));
|
|
1307
|
+
});
|
|
1308
|
+
};
|
|
1309
|
+
/** A chat line, kept in memory for reports: the last KEPT_CHAT_LINE_COUNT. */
|
|
1310
|
+
Room.prototype._keepChatLine = function (line) {
|
|
1311
|
+
this._chatLines.push(line);
|
|
1312
|
+
if (this._chatLines.length > KEPT_CHAT_LINE_COUNT) this._chatLines.shift();
|
|
1313
|
+
};
|
|
1314
|
+
/** What the room said about chat: someone's message, or its answer on one of this player's. */
|
|
1315
|
+
Room.prototype._chatAnswer = function (message) {
|
|
1316
|
+
if (message.type === "chat") {
|
|
1317
|
+
if (typeof message.fromPlayerId !== "string" || typeof message.text !== "string") return;
|
|
1318
|
+
// Muted and blocked players' chat never reaches the game.
|
|
1319
|
+
if (this.isMuted(message.fromPlayerId)) return;
|
|
1320
|
+
var chat = { fromPlayerId: message.fromPlayerId, text: message.text, hasSwearing: message.hasSwearing === true, hasSlur: message.hasSlur === true, sentAt: typeof message.sentAt === "string" ? message.sentAt : new Date().toISOString() };
|
|
1321
|
+
this._keepChatLine({ fromPlayerId: chat.fromPlayerId, text: chat.text, sentAt: chat.sentAt, toPlayerIds: null });
|
|
1322
|
+
this._emit("chat", chat);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
var waiting = this._chats.get(message.chatNumber);
|
|
1326
|
+
if (!waiting) return;
|
|
1327
|
+
this._chats.delete(message.chatNumber);
|
|
1328
|
+
if (message.type === "chat_sent") {
|
|
1329
|
+
this._keepChatLine({ fromPlayerId: this.me ? this.me.id : null, text: waiting.text, sentAt: message.sentAt, toPlayerIds: waiting.toPlayerIds });
|
|
1330
|
+
waiting.resolve({ sentAt: message.sentAt });
|
|
1331
|
+
} else if (message.type === "chat_stopped") {
|
|
1332
|
+
var stopped = codedError("chat_stopped", typeof message.message === "string" && message.message ? message.message : "Your message wasn't sent.");
|
|
1333
|
+
stopped.reason = typeof message.reason === "string" ? message.reason : null;
|
|
1334
|
+
waiting.reject(stopped);
|
|
1335
|
+
} else {
|
|
1336
|
+
waiting.reject(codedError(message.code, message.message));
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
/** Chat messages still waiting when the room closes don't go. */
|
|
1340
|
+
Room.prototype._failChats = function () {
|
|
1341
|
+
var waiting = Array.from(this._chats.values());
|
|
1342
|
+
this._chats.clear();
|
|
1343
|
+
waiting.forEach(function (chat) { chat.reject(codedError("connection_closed", "The room closed before the message went.")); });
|
|
1260
1344
|
};
|
|
1261
1345
|
|
|
1262
1346
|
Room.prototype.leave = function () {
|
|
1263
1347
|
if (!this._done) left(this);
|
|
1264
1348
|
this._done = true;
|
|
1265
1349
|
this._queue = [];
|
|
1350
|
+
this._failChats();
|
|
1266
1351
|
if (this._ws) this._ws.close(1000, "Left");
|
|
1267
1352
|
};
|
|
1268
1353
|
|
package/src/chat.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zaisma's chat filter for your own server (docs/DECISIONS.md, "The text filter catches what matters and
|
|
3
3
|
* leaves the rest alone"). Every chat message to or from an under-18 account is checked on Zaisma's side:
|
|
4
|
-
* contact details in any disguise, moving to another app or meeting up, personal details
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* contact details in any disguise, moving to another app or meeting up, personal details and sexual
|
|
5
|
+
* content are stopped. Swearing and slurs are only flagged (hasSwearing, hasSlur): blocking them is your
|
|
6
|
+
* call. Between adults it only flags, unless you turn the filter on for them too (isAdultFilterEnabled).
|
|
7
|
+
* The answer is one verdict for the whole message, never per receiver, so it never says who's under 18.
|
|
7
8
|
*
|
|
8
9
|
* checkChat sends the players' passes (as your server received them) and the sender's last few messages to
|
|
9
10
|
* Zaisma (POST <issuer>/v1/chat/check), so a phone number sent a few digits at a time is caught too.
|
|
@@ -11,14 +12,17 @@
|
|
|
11
12
|
* It's for your own server only. In Zaisma rooms the room filters the chat it delivers itself, so a game
|
|
12
13
|
* server running there never sees passes, and checkChat throws a ChatCheckError (own_server_only).
|
|
13
14
|
*/
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Why a message was stopped. The same reasons as Zaisma.checkChat, plus chat_paused, not_allowed_to_chat and
|
|
17
|
+
* unchecked (Zaisma couldn't be reached). slur is v1's and no longer given: slurs are flagged (hasSlur), never stopped.
|
|
18
|
+
*/
|
|
15
19
|
export type ChatCheckReason = "contact_details" | "personal_details" | "slur" | "sexual" | "chat_paused" | "unchecked" | "too_long" | "not_allowed_to_chat";
|
|
16
20
|
export interface ChatCheck {
|
|
17
21
|
/** Whether to deliver it. */
|
|
18
22
|
isAllowed: boolean;
|
|
19
23
|
/**
|
|
20
|
-
* Why not; null when it's allowed. chat_paused: the sender's chat is paused or removed. unchecked:
|
|
21
|
-
* couldn't
|
|
24
|
+
* Why not; null when it's allowed. chat_paused: the sender's chat is paused or removed. unchecked: your
|
|
25
|
+
* server couldn't reach Zaisma, so don't deliver it (try again in a moment). too_long: over 2000 characters.
|
|
22
26
|
* not_allowed_to_chat: canChat says the sender and a receiver may not chat (Zaisma checks it too).
|
|
23
27
|
*/
|
|
24
28
|
reason: ChatCheckReason | null;
|
|
@@ -26,6 +30,8 @@ export interface ChatCheck {
|
|
|
26
30
|
message: string | null;
|
|
27
31
|
/** Whether it swears. Zaisma allows it; block it yourself if your game doesn't. */
|
|
28
32
|
hasSwearing: boolean;
|
|
33
|
+
/** Whether it has a slur. Zaisma allows it too, so your game and its moderators decide. */
|
|
34
|
+
hasSlur: boolean;
|
|
29
35
|
}
|
|
30
36
|
export interface ChatCheckOptions {
|
|
31
37
|
/** The message. */
|
|
@@ -34,7 +40,15 @@ export interface ChatCheckOptions {
|
|
|
34
40
|
senderPass: string;
|
|
35
41
|
/** The passes of everyone it would reach, up to 50. */
|
|
36
42
|
receiverPasses: string[];
|
|
43
|
+
/** Check chat between adults too, as for under-18s; each message Zaisma's AI looks at counts on your hosting meter. False by default. */
|
|
44
|
+
isAdultFilterEnabled?: boolean;
|
|
37
45
|
}
|
|
46
|
+
/** Chat in rooms (room.chat), for both Zaisma's rooms and your own server's runner: past any human's typing, so it only stops floods. */
|
|
47
|
+
export declare const CHAT_ROOM_LIMITS: {
|
|
48
|
+
/** Chat messages one player may send in any chatWindowDurationMs; past it, chat_too_fast. */
|
|
49
|
+
readonly maximumChatMessageCount: 20;
|
|
50
|
+
readonly chatWindowDurationMs: 10000;
|
|
51
|
+
};
|
|
38
52
|
export declare const CHAT_CHECK_LIMITS: {
|
|
39
53
|
/** The longest message checked, in characters: the whole text is checked, and a longer one is refused (too_long), never cut. */
|
|
40
54
|
readonly maximumMessageCharacterCount: 2000;
|
|
@@ -56,8 +70,9 @@ export declare class ChatCheckError extends Error {
|
|
|
56
70
|
/**
|
|
57
71
|
* Check one chat message with Zaisma's filter. Call it on every chat message your server delivers, after
|
|
58
72
|
* canChat says the players may chat, and deliver it only when isAllowed (Zaisma applies canChat's rule
|
|
59
|
-
* too, so a message to anyone who may not chat is stopped). It's free
|
|
60
|
-
* reached it answers unchecked: don't deliver the message.
|
|
73
|
+
* too, so a message to anyone who may not chat is stopped). It's free, except the checks of chat between
|
|
74
|
+
* adults when isAdultFilterEnabled. When Zaisma can't be reached it answers unchecked: don't deliver the message.
|
|
75
|
+
* The runner (zaisma-game-server) does all this for room.chat in its rooms.
|
|
61
76
|
*
|
|
62
77
|
* const check = await checkChat({ text, senderPass, receiverPasses: [receiverPass] });
|
|
63
78
|
* if (!check.isAllowed || check.hasSwearing) return reply(sender, check.message ?? "No swearing here.");
|
package/src/chat.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zaisma's chat filter for your own server (docs/DECISIONS.md, "The text filter catches what matters and
|
|
3
3
|
* leaves the rest alone"). Every chat message to or from an under-18 account is checked on Zaisma's side:
|
|
4
|
-
* contact details in any disguise, moving to another app or meeting up, personal details
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* contact details in any disguise, moving to another app or meeting up, personal details and sexual
|
|
5
|
+
* content are stopped. Swearing and slurs are only flagged (hasSwearing, hasSlur): blocking them is your
|
|
6
|
+
* call. Between adults it only flags, unless you turn the filter on for them too (isAdultFilterEnabled).
|
|
7
|
+
* The answer is one verdict for the whole message, never per receiver, so it never says who's under 18.
|
|
7
8
|
*
|
|
8
9
|
* checkChat sends the players' passes (as your server received them) and the sender's last few messages to
|
|
9
10
|
* Zaisma (POST <issuer>/v1/chat/check), so a phone number sent a few digits at a time is caught too.
|
|
@@ -12,6 +13,12 @@
|
|
|
12
13
|
* server running there never sees passes, and checkChat throws a ChatCheckError (own_server_only).
|
|
13
14
|
*/
|
|
14
15
|
import { OutsideCallError } from "./outside-calls.js";
|
|
16
|
+
/** Chat in rooms (room.chat), for both Zaisma's rooms and your own server's runner: past any human's typing, so it only stops floods. */
|
|
17
|
+
export const CHAT_ROOM_LIMITS = {
|
|
18
|
+
/** Chat messages one player may send in any chatWindowDurationMs; past it, chat_too_fast. */
|
|
19
|
+
maximumChatMessageCount: 20,
|
|
20
|
+
chatWindowDurationMs: 10_000,
|
|
21
|
+
};
|
|
15
22
|
export const CHAT_CHECK_LIMITS = {
|
|
16
23
|
/** The longest message checked, in characters: the whole text is checked, and a longer one is refused (too_long), never cut. */
|
|
17
24
|
maximumMessageCharacterCount: 2000,
|
|
@@ -34,9 +41,9 @@ export class ChatCheckError extends Error {
|
|
|
34
41
|
this.fix = fix;
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
|
-
const UNCHECKED = { isAllowed: false, reason: "unchecked", message: "This message couldn't be checked. Try again in a moment.", hasSwearing: false };
|
|
38
|
-
const TOO_LONG = { isAllowed: false, reason: "too_long", message: "That message is too long to send.", hasSwearing: false };
|
|
39
|
-
const REASONS = ["contact_details", "personal_details", "
|
|
44
|
+
const UNCHECKED = { isAllowed: false, reason: "unchecked", message: "This message couldn't be checked. Try again in a moment.", hasSwearing: false, hasSlur: false };
|
|
45
|
+
const TOO_LONG = { isAllowed: false, reason: "too_long", message: "That message is too long to send.", hasSwearing: false, hasSlur: false };
|
|
46
|
+
const REASONS = ["contact_details", "personal_details", "sexual", "chat_paused", "unchecked", "too_long", "not_allowed_to_chat"];
|
|
40
47
|
/** Each sender's last messages, by the sender's pass's player ID: only in this process's memory. */
|
|
41
48
|
const recentBySender = new Map();
|
|
42
49
|
/** The claims of a pass, unchecked: Zaisma checks the signature. Only to find whose it is and where it's from. */
|
|
@@ -67,13 +74,16 @@ function rememberText(senderPlayerId, text, nowMs) {
|
|
|
67
74
|
/**
|
|
68
75
|
* Check one chat message with Zaisma's filter. Call it on every chat message your server delivers, after
|
|
69
76
|
* canChat says the players may chat, and deliver it only when isAllowed (Zaisma applies canChat's rule
|
|
70
|
-
* too, so a message to anyone who may not chat is stopped). It's free
|
|
71
|
-
* reached it answers unchecked: don't deliver the message.
|
|
77
|
+
* too, so a message to anyone who may not chat is stopped). It's free, except the checks of chat between
|
|
78
|
+
* adults when isAdultFilterEnabled. When Zaisma can't be reached it answers unchecked: don't deliver the message.
|
|
79
|
+
* The runner (zaisma-game-server) does all this for room.chat in its rooms.
|
|
72
80
|
*
|
|
73
81
|
* const check = await checkChat({ text, senderPass, receiverPasses: [receiverPass] });
|
|
74
82
|
* if (!check.isAllowed || check.hasSwearing) return reply(sender, check.message ?? "No swearing here.");
|
|
75
83
|
*/
|
|
76
84
|
export async function checkChat({ text, senderPass, receiverPasses }) {
|
|
85
|
+
// Read apart from the destructuring, which is v1's signature as it's locked (test/contracts).
|
|
86
|
+
const isAdultFilterEnabled = arguments[0]?.isAdultFilterEnabled === true;
|
|
77
87
|
const claims = readClaims(String(senderPass ?? ""));
|
|
78
88
|
if (typeof claims.sub !== "string" || !isZaismaIssuer(claims.iss) || !Array.isArray(receiverPasses))
|
|
79
89
|
return UNCHECKED;
|
|
@@ -87,7 +97,7 @@ export async function checkChat({ text, senderPass, receiverPasses }) {
|
|
|
87
97
|
const response = await fetch(`${claims.iss}/v1/chat/check`, {
|
|
88
98
|
method: "POST",
|
|
89
99
|
headers: { "Content-Type": "application/json" },
|
|
90
|
-
body: JSON.stringify({ gameSlug: claims.aud, senderPass, receiverPasses: receiverPasses.slice(0, CHAT_CHECK_LIMITS.maximumReceiverCount), text: message, recentTexts }),
|
|
100
|
+
body: JSON.stringify({ gameSlug: claims.aud, senderPass, receiverPasses: receiverPasses.slice(0, CHAT_CHECK_LIMITS.maximumReceiverCount), text: message, recentTexts, isAdultFilterEnabled }),
|
|
91
101
|
signal: AbortSignal.timeout(CHAT_CHECK_LIMITS.checkTimeoutMs),
|
|
92
102
|
});
|
|
93
103
|
if (!response.ok)
|
|
@@ -96,12 +106,12 @@ export async function checkChat({ text, senderPass, receiverPasses }) {
|
|
|
96
106
|
if (typeof answer.isAllowed !== "boolean")
|
|
97
107
|
return UNCHECKED;
|
|
98
108
|
const reason = typeof answer.reason === "string" && REASONS.includes(answer.reason) ? answer.reason : null;
|
|
99
|
-
return { isAllowed: answer.isAllowed, reason, message: typeof answer.message === "string" ? answer.message : null, hasSwearing: answer.hasSwearing === true };
|
|
109
|
+
return { isAllowed: answer.isAllowed, reason, message: typeof answer.message === "string" ? answer.message : null, hasSwearing: answer.hasSwearing === true, hasSlur: answer.hasSlur === true };
|
|
100
110
|
}
|
|
101
111
|
catch (error) {
|
|
102
112
|
// In Zaisma rooms the gateway refuses the call to Zaisma's own address: checkChat is for a game server on your own server.
|
|
103
113
|
if (error instanceof OutsideCallError && ["unreachable_host", "not_https", "host_not_allowed"].includes(error.code)) {
|
|
104
|
-
throw new ChatCheckError("checkChat is for a game server on your own server. In Zaisma rooms the room runs Zaisma's chat filter on the chat it delivers.", "
|
|
114
|
+
throw new ChatCheckError("checkChat is for a game server on your own server. In Zaisma rooms the room runs Zaisma's chat filter on the chat it delivers.", "In Zaisma rooms, send chat with room.chat in the game; the room filters it.");
|
|
105
115
|
}
|
|
106
116
|
return UNCHECKED;
|
|
107
117
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, GAME_SERVER_OPTIONS, GameServerError, NO_ANSWER_CLOSE_CODE, REMOVED_CLOSE_CODE, ROOM_LIMITS, SEAT_GONE_CLOSE_CODE, VERSION_UPDATE_CLOSE_CODE, defineGameServer, type Audience, type AwayInfo, type AwayMode, type AwayReason, type BroadcastOptions, type Delivery, type GameServer, type GameServerErrorCode, type HandlerResult, type IdleCheck, type JoinAnswer, type JoinInfo, type KickOptions, type LeaveInfo, type LeaveReason, type MoveOptions, type MoveResult, type Player, type Purchase, type Refusal, type ReserveOptions, type RestoreInfo, type Room, type RoomErrorCode, type RoomInfo, type SendOptions, type VersionUpdate, } from "./room.ts";
|
|
1
|
+
export { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, GAME_SERVER_OPTIONS, GameServerError, NO_ANSWER_CLOSE_CODE, REMOVED_CLOSE_CODE, ROOM_LIMITS, SEAT_GONE_CLOSE_CODE, VERSION_UPDATE_CLOSE_CODE, defineGameServer, type Audience, type AwayInfo, type AwayMode, type AwayReason, type BroadcastOptions, type Delivery, type GameServer, type GameServerErrorCode, type HandlerResult, type IdleCheck, type JoinAnswer, type ChatAnswer, type ChatInfo, type JoinInfo, type KickOptions, type LeaveInfo, type LeaveReason, type MoveOptions, type MoveResult, type Player, type Purchase, type Refusal, type ReserveOptions, type RestoreInfo, type Room, type RoomErrorCode, type RoomInfo, type SendOptions, type VersionUpdate, } from "./room.ts";
|
|
2
2
|
export { REQUEST_LIMITS, type RequestErrorCode, type Server } from "./requests.ts";
|
|
3
3
|
export { SCHEDULE_LIMITS, type Schedule, type ScheduledRun } from "./schedules.ts";
|
|
4
4
|
export { WEBHOOK_LIMITS, verifyWebhook, type WebhookError, type WebhookErrorCode, type WebhookEvent, type WebhookEventKind, type WebhookHeaders } from "./webhooks.ts";
|
|
5
5
|
export { DATABASE_LIMITS, DatabaseError, type DatabaseErrorCode, type DatabaseRow, type DatabaseStatement, type DatabaseValue, type GameDatabase } from "./database.ts";
|
|
6
6
|
export { OUTSIDE_CALL_LIMITS, OutsideCallError, type OutsideCallErrorCode } from "./outside-calls.ts";
|
|
7
7
|
export { PASS_ISSUER_URL, PASS_TOKEN_TYPE, PASS_VERSION_NUMBER, PassError, RECEIPT_ISSUER_URL, RECEIPT_TOKEN_TYPE, RECEIPT_VERSION_NUMBER, ReceiptError, canChat, passesAreFriends, passesHaveBlockBetween, verifyPass, verifyReceipt, type PassClaims, type PassErrorCode, type PassExpectations, type PlayerKind, type PublicKey, type ReceiptClaims, type ReceiptErrorCode, type ReceiptExpectations, type WhoCanChat } from "./pass.ts";
|
|
8
|
-
export { CHAT_CHECK_LIMITS, ChatCheckError, checkChat, type ChatCheck, type ChatCheckOptions, type ChatCheckReason } from "./chat.ts";
|
|
8
|
+
export { CHAT_CHECK_LIMITS, CHAT_ROOM_LIMITS, ChatCheckError, checkChat, type ChatCheck, type ChatCheckOptions, type ChatCheckReason } from "./chat.ts";
|
|
9
9
|
export { guestName, isRoomCode } from "./names.ts";
|
|
10
10
|
export { relay } from "./relay.ts";
|