@serverkgg/bridge 0.1.1 → 0.1.2
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/package.json +1 -1
- package/src/bot/botPanel.test.ts +30 -0
- package/src/bot/botPanel.ts +9 -1
- package/src/cli/validate.test.ts +73 -1
- package/src/cli/validate.ts +35 -1
- package/src/context/bridgeContext.ts +6 -1
- package/src/core/bridgeAction.ts +2 -0
- package/src/core/bridgeActivity.ts +9 -0
- package/src/core/bridgeAvailability.ts +10 -0
- package/src/core/bridgeUserError.test.ts +10 -0
- package/src/core/bridgeUserError.ts +6 -2
- package/src/core/describeDownloadFailure.test.ts +58 -0
- package/src/core/describeDownloadFailure.ts +69 -0
- package/src/core/index.ts +3 -0
- package/src/modules/bridge.ts +8 -1
- package/src/modules/bridgeModule.ts +13 -1
- package/src/panel/bridgeDetail.ts +2 -1
- package/src/protocol/bridgeActivity.ts +54 -0
- package/src/protocol/bridgeEvents.ts +30 -0
- package/src/protocol/bridgeFrame.ts +10 -1
- package/src/protocol/index.ts +1 -0
- package/src/runtime/bridgeActions.ts +19 -2
- package/src/runtime/bridgeActivity.test.ts +318 -0
- package/src/runtime/bridgeActivity.ts +268 -0
- package/src/runtime/bridgeContext.test.ts +6 -0
- package/src/runtime/bridgeContext.ts +8 -1
- package/src/runtime/bridgeDriver.test.ts +21 -0
- package/src/runtime/bridgeDriver.ts +10 -7
- package/src/runtime/bridgeLog.ts +10 -0
- package/src/runtime/bridgeRefusalError.ts +13 -0
- package/src/runtime/bridgeRegistry.test.ts +1053 -10
- package/src/runtime/bridgeRegistry.ts +464 -65
- package/src/runtime/bridgeScopedContext.ts +55 -0
- package/src/runtime/bridgeServer.test.ts +404 -4
- package/src/runtime/bridgeServer.ts +125 -35
- package/src/runtime/bridgeSetupHandlers.test.ts +69 -6
- package/src/runtime/bridgeSetupHandlers.ts +45 -10
- package/src/runtime/serve.ts +45 -3
- package/src/steam/steamcmd.test.ts +32 -1
- package/src/steam/steamcmd.ts +27 -4
- package/src/utils/changedSettingKeys.test.ts +65 -0
- package/src/utils/changedSettingKeys.ts +6 -0
- package/src/utils/describeExecFailure.test.ts +34 -0
- package/src/utils/describeExecFailure.ts +9 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/sameSettingValue.test.ts +35 -0
- package/src/utils/sameSettingValue.ts +40 -0
- package/src/wire/activity.test.ts +95 -0
- package/src/wire/activity.ts +41 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/panel.test.ts +67 -1
- package/src/wire/panel.ts +11 -0
package/src/steam/steamcmd.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BridgeContext } from "../context";
|
|
1
|
+
import type { BridgeContext, BridgeExecResult } from "../context";
|
|
2
|
+
import { BridgeUserError, type LocalizedText } from "../core";
|
|
2
3
|
import { execDetail } from "../utils";
|
|
3
4
|
import { installedBuildId } from "./steamApp";
|
|
4
5
|
import { type SteamcmdLogin, SteamcmdLoginKind, steamcmdLoginArgs } from "./steamcmdLogin";
|
|
@@ -25,6 +26,8 @@ const ATTEMPTS = 3;
|
|
|
25
26
|
|
|
26
27
|
const RETRY_DELAY_MS = 5000;
|
|
27
28
|
|
|
29
|
+
const NO_SUBSCRIPTION = "No subscription";
|
|
30
|
+
|
|
28
31
|
export interface SteamcmdOptions {
|
|
29
32
|
appId: string;
|
|
30
33
|
label: string;
|
|
@@ -100,6 +103,20 @@ export const steamcmdUpdateArgs = (appId: string, update: SteamcmdUpdateOptions)
|
|
|
100
103
|
return args;
|
|
101
104
|
};
|
|
102
105
|
|
|
106
|
+
export const steamcmdFailureText = (result: BridgeExecResult, label: string): LocalizedText => {
|
|
107
|
+
if (`${result.stdout}\n${result.stderr}`.includes(NO_SUBSCRIPTION)) {
|
|
108
|
+
return {
|
|
109
|
+
ar: `Steam رفض تنزيل ${label} لأن حساب التنزيل ما يملكه. راسل الدعم.`,
|
|
110
|
+
en: `Steam refused to download ${label} because the download account does not own it. Contact support.`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
ar: `ما قدرنا ننزّل ${label} من Steam. جرّب مرة ثانية.`,
|
|
116
|
+
en: `We could not download ${label} from Steam. Try again.`,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
103
120
|
const installRoot = async (context: BridgeContext) => {
|
|
104
121
|
const result = await context.exec([
|
|
105
122
|
"pwd",
|
|
@@ -162,7 +179,7 @@ export const createSteamcmd = (context: BridgeContext, options: SteamcmdOptions)
|
|
|
162
179
|
|
|
163
180
|
const attempts = runOptions.attempts ?? ATTEMPTS;
|
|
164
181
|
|
|
165
|
-
let failure =
|
|
182
|
+
let failure: BridgeExecResult | null = null;
|
|
166
183
|
|
|
167
184
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
168
185
|
const result = await context.exec(argv, {
|
|
@@ -178,7 +195,7 @@ export const createSteamcmd = (context: BridgeContext, options: SteamcmdOptions)
|
|
|
178
195
|
return;
|
|
179
196
|
}
|
|
180
197
|
|
|
181
|
-
failure =
|
|
198
|
+
failure = result;
|
|
182
199
|
|
|
183
200
|
if (attempt < attempts) {
|
|
184
201
|
context.log("steamcmd did not finish, trying again", {
|
|
@@ -191,7 +208,13 @@ export const createSteamcmd = (context: BridgeContext, options: SteamcmdOptions)
|
|
|
191
208
|
}
|
|
192
209
|
}
|
|
193
210
|
|
|
194
|
-
|
|
211
|
+
if (!failure) {
|
|
212
|
+
throw new Error("steamcmd was not run");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
throw new BridgeUserError(steamcmdFailureText(failure, options.label), {
|
|
216
|
+
cause: new Error(`steamcmd exited with code ${failure.code} — ${execDetail(failure)}`),
|
|
217
|
+
});
|
|
195
218
|
};
|
|
196
219
|
|
|
197
220
|
return {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { changedSettingKeys } from "./changedSettingKeys";
|
|
3
|
+
|
|
4
|
+
describe("changedSettingKeys", () => {
|
|
5
|
+
test("keeps only the keys whose value really changed", () => {
|
|
6
|
+
expect(
|
|
7
|
+
changedSettingKeys(
|
|
8
|
+
{
|
|
9
|
+
difficulty: "easy",
|
|
10
|
+
expRate: "1.000000",
|
|
11
|
+
pvp: "True",
|
|
12
|
+
name: "My server",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
difficulty: "hard",
|
|
16
|
+
expRate: 1,
|
|
17
|
+
pvp: true,
|
|
18
|
+
name: "My server",
|
|
19
|
+
},
|
|
20
|
+
),
|
|
21
|
+
).toEqual([
|
|
22
|
+
"difficulty",
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("counts a key the stored values do not hold as changed", () => {
|
|
27
|
+
expect(
|
|
28
|
+
changedSettingKeys(
|
|
29
|
+
{},
|
|
30
|
+
{
|
|
31
|
+
seed: "42",
|
|
32
|
+
},
|
|
33
|
+
),
|
|
34
|
+
).toEqual([
|
|
35
|
+
"seed",
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("counts every written key as changed when nothing could be read first", () => {
|
|
40
|
+
const written = {
|
|
41
|
+
difficulty: "hard",
|
|
42
|
+
pvp: false,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
expect(changedSettingKeys(null, written)).toEqual([
|
|
46
|
+
"difficulty",
|
|
47
|
+
"pvp",
|
|
48
|
+
]);
|
|
49
|
+
expect(changedSettingKeys(undefined, written)).toEqual([
|
|
50
|
+
"difficulty",
|
|
51
|
+
"pvp",
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("answers nothing for an empty write", () => {
|
|
56
|
+
expect(
|
|
57
|
+
changedSettingKeys(
|
|
58
|
+
{
|
|
59
|
+
difficulty: "easy",
|
|
60
|
+
},
|
|
61
|
+
{},
|
|
62
|
+
),
|
|
63
|
+
).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BridgeValues } from "../core";
|
|
2
|
+
import { sameSettingValue } from "./sameSettingValue";
|
|
3
|
+
|
|
4
|
+
export const changedSettingKeys = (before: BridgeValues | null | undefined, written: BridgeValues): string[] => {
|
|
5
|
+
return Object.keys(written).filter((key) => !before || !sameSettingValue(before[key], written[key]));
|
|
6
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { describeExecFailure } from "./describeExecFailure";
|
|
3
|
+
|
|
4
|
+
describe("describeExecFailure", () => {
|
|
5
|
+
test("names the subject and the exit code in both languages", () => {
|
|
6
|
+
const text = describeExecFailure(
|
|
7
|
+
{
|
|
8
|
+
stdout: "",
|
|
9
|
+
stderr: "java.lang.RuntimeException",
|
|
10
|
+
code: 1,
|
|
11
|
+
},
|
|
12
|
+
"Forge installer",
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
expect(text).toEqual({
|
|
16
|
+
ar: "Forge installer فشل وطلع بكود 1.",
|
|
17
|
+
en: "Forge installer failed with exit code 1.",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("keeps the process output out of the customer text", () => {
|
|
22
|
+
const text = describeExecFailure(
|
|
23
|
+
{
|
|
24
|
+
stdout: "token=abc",
|
|
25
|
+
stderr: "at /home/game/secret",
|
|
26
|
+
code: 2,
|
|
27
|
+
},
|
|
28
|
+
"unzip",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(JSON.stringify(text)).not.toContain("secret");
|
|
32
|
+
expect(JSON.stringify(text)).not.toContain("token");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BridgeExecResult } from "../context";
|
|
2
|
+
import type { LocalizedText } from "../core";
|
|
3
|
+
|
|
4
|
+
export const describeExecFailure = (result: BridgeExecResult, subject: string): LocalizedText => {
|
|
5
|
+
return {
|
|
6
|
+
ar: `${subject} فشل وطلع بكود ${result.code}.`,
|
|
7
|
+
en: `${subject} failed with exit code ${result.code}.`,
|
|
8
|
+
};
|
|
9
|
+
};
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { sameSettingValue } from "./sameSettingValue";
|
|
3
|
+
|
|
4
|
+
describe("sameSettingValue", () => {
|
|
5
|
+
test("treats a float the ini codec wrote as equal to the integer the form sends", () => {
|
|
6
|
+
expect(sameSettingValue("1.000000", 1)).toBe(true);
|
|
7
|
+
expect(sameSettingValue("1", "1.000000")).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("treats decimals that differ only in trailing zeros as equal", () => {
|
|
11
|
+
expect(sameSettingValue("0.5", "0.50")).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("treats boolean text as equal whatever its case", () => {
|
|
15
|
+
expect(sameSettingValue("True", "true")).toBe(true);
|
|
16
|
+
expect(sameSettingValue("False", false)).toBe(true);
|
|
17
|
+
expect(sameSettingValue(true, "FALSE")).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("never treats an empty value as zero", () => {
|
|
21
|
+
expect(sameSettingValue("", "0")).toBe(false);
|
|
22
|
+
expect(sameSettingValue(null, 0)).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("keeps a leading-zero string different from its number, since it may be an id", () => {
|
|
26
|
+
expect(sameSettingValue("01", "1")).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("compares the trimmed text of anything else", () => {
|
|
30
|
+
expect(sameSettingValue(" easy ", "easy")).toBe(true);
|
|
31
|
+
expect(sameSettingValue("easy", "hard")).toBe(false);
|
|
32
|
+
expect(sameSettingValue("2", "3")).toBe(false);
|
|
33
|
+
expect(sameSettingValue(undefined, "")).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BridgeValue } from "../core";
|
|
2
|
+
|
|
3
|
+
const PLAIN_NUMBER = /^-?(0|[1-9]\d*)(\.\d+)?$/;
|
|
4
|
+
|
|
5
|
+
const BOOLEAN_TEXT: Readonly<Record<string, boolean>> = {
|
|
6
|
+
true: true,
|
|
7
|
+
false: false,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const settingText = (value: BridgeValue | undefined) => {
|
|
11
|
+
return value === undefined || value === null ? "" : String(value).trim();
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const numberOf = (text: string) => {
|
|
15
|
+
return PLAIN_NUMBER.test(text) ? Number(text) : null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const booleanOf = (text: string) => {
|
|
19
|
+
return BOOLEAN_TEXT[text.toLowerCase()] ?? null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const sameSettingValue = (left: BridgeValue | undefined, right: BridgeValue | undefined): boolean => {
|
|
23
|
+
const a = settingText(left);
|
|
24
|
+
const b = settingText(right);
|
|
25
|
+
|
|
26
|
+
if (a === b) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const leftNumber = numberOf(a);
|
|
31
|
+
const rightNumber = numberOf(b);
|
|
32
|
+
|
|
33
|
+
if (leftNumber !== null && rightNumber !== null) {
|
|
34
|
+
return leftNumber === rightNumber;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const leftBoolean = booleanOf(a);
|
|
38
|
+
|
|
39
|
+
return leftBoolean !== null && leftBoolean === booleanOf(b);
|
|
40
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { BRIDGE_ACTIVITY_DETAILS_LIMIT, BRIDGE_ACTIVITY_LIMIT, BridgeEventName } from "@serverkgg/bridge/protocol";
|
|
3
|
+
import { bridgeActivitiesSchema, bridgeActivitySchema } from "./activity";
|
|
4
|
+
|
|
5
|
+
const entry = {
|
|
6
|
+
name: BridgeEventName.ModpackInstalled,
|
|
7
|
+
text: {
|
|
8
|
+
ar: "ثبّتنا مودباك All the Mods 9",
|
|
9
|
+
en: "Installed modpack All the Mods 9",
|
|
10
|
+
},
|
|
11
|
+
subject: "All the Mods 9",
|
|
12
|
+
details: {
|
|
13
|
+
version: "0.3.2",
|
|
14
|
+
files: 312,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe("bridgeActivitySchema", () => {
|
|
19
|
+
test("accepts a well-formed entry", () => {
|
|
20
|
+
expect(bridgeActivitySchema.parse(entry)).toEqual(entry);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("accepts a name this reader does not know yet, so a newer game still reaches it", () => {
|
|
24
|
+
expect(
|
|
25
|
+
bridgeActivitySchema.safeParse({
|
|
26
|
+
...entry,
|
|
27
|
+
name: "PortalOpened",
|
|
28
|
+
}).success,
|
|
29
|
+
).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("refuses a name that is not an event name", () => {
|
|
33
|
+
expect(
|
|
34
|
+
bridgeActivitySchema.safeParse({
|
|
35
|
+
...entry,
|
|
36
|
+
name: "portal opened",
|
|
37
|
+
}).success,
|
|
38
|
+
).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("refuses an entry missing one language", () => {
|
|
42
|
+
expect(
|
|
43
|
+
bridgeActivitySchema.safeParse({
|
|
44
|
+
...entry,
|
|
45
|
+
text: {
|
|
46
|
+
ar: "",
|
|
47
|
+
en: "Installed",
|
|
48
|
+
},
|
|
49
|
+
}).success,
|
|
50
|
+
).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("refuses more details than an entry carries", () => {
|
|
54
|
+
const details: Record<string, number> = {};
|
|
55
|
+
|
|
56
|
+
for (let index = 0; index <= BRIDGE_ACTIVITY_DETAILS_LIMIT; index++) {
|
|
57
|
+
details[`key${index}`] = index;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
expect(
|
|
61
|
+
bridgeActivitySchema.safeParse({
|
|
62
|
+
...entry,
|
|
63
|
+
details,
|
|
64
|
+
}).success,
|
|
65
|
+
).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("refuses a detail that is not a scalar", () => {
|
|
69
|
+
expect(
|
|
70
|
+
bridgeActivitySchema.safeParse({
|
|
71
|
+
...entry,
|
|
72
|
+
details: {
|
|
73
|
+
nested: {
|
|
74
|
+
deep: true,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}).success,
|
|
78
|
+
).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("bridgeActivitiesSchema", () => {
|
|
83
|
+
test("refuses more entries than one answer carries", () => {
|
|
84
|
+
expect(
|
|
85
|
+
bridgeActivitiesSchema.safeParse(
|
|
86
|
+
Array.from(
|
|
87
|
+
{
|
|
88
|
+
length: BRIDGE_ACTIVITY_LIMIT + 1,
|
|
89
|
+
},
|
|
90
|
+
() => entry,
|
|
91
|
+
),
|
|
92
|
+
).success,
|
|
93
|
+
).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BRIDGE_ACTIVITY_DETAIL_KEY_LENGTH_LIMIT,
|
|
3
|
+
BRIDGE_ACTIVITY_DETAIL_KEY_PATTERN,
|
|
4
|
+
BRIDGE_ACTIVITY_DETAILS_LIMIT,
|
|
5
|
+
BRIDGE_ACTIVITY_LIMIT,
|
|
6
|
+
BRIDGE_ACTIVITY_NAME_LENGTH_LIMIT,
|
|
7
|
+
BRIDGE_ACTIVITY_NAME_PATTERN,
|
|
8
|
+
BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT,
|
|
9
|
+
BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT,
|
|
10
|
+
} from "@serverkgg/bridge/protocol";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
const activityTextSchema = z.object({
|
|
14
|
+
ar: z.string().min(1).max(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT),
|
|
15
|
+
en: z.string().min(1).max(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const activityDetailsSchema = z
|
|
19
|
+
.record(
|
|
20
|
+
z.string().min(1).max(BRIDGE_ACTIVITY_DETAIL_KEY_LENGTH_LIMIT).regex(BRIDGE_ACTIVITY_DETAIL_KEY_PATTERN),
|
|
21
|
+
z.union([
|
|
22
|
+
z.boolean(),
|
|
23
|
+
z.number(),
|
|
24
|
+
z.string().max(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT),
|
|
25
|
+
z.null(),
|
|
26
|
+
]),
|
|
27
|
+
)
|
|
28
|
+
.refine((details) => Object.keys(details).length <= BRIDGE_ACTIVITY_DETAILS_LIMIT);
|
|
29
|
+
|
|
30
|
+
export const bridgeActivitySchema = z.object({
|
|
31
|
+
name: z.string().min(1).max(BRIDGE_ACTIVITY_NAME_LENGTH_LIMIT).regex(BRIDGE_ACTIVITY_NAME_PATTERN),
|
|
32
|
+
text: activityTextSchema,
|
|
33
|
+
subject: z.string().min(1).max(BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT).optional(),
|
|
34
|
+
details: activityDetailsSchema.optional(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export type BridgeActivityInput = z.infer<typeof bridgeActivitySchema>;
|
|
38
|
+
|
|
39
|
+
export const bridgeActivitiesSchema = z.array(bridgeActivitySchema).max(BRIDGE_ACTIVITY_LIMIT);
|
|
40
|
+
|
|
41
|
+
export type BridgeActivitiesInput = z.infer<typeof bridgeActivitiesSchema>;
|
package/src/wire/index.ts
CHANGED
package/src/wire/panel.test.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
BridgeSetupStepKind,
|
|
16
16
|
BridgeUploadMode,
|
|
17
17
|
} from "@serverkgg/bridge/protocol";
|
|
18
|
-
import { bridgeCellsSchema, bridgeManifestSchema } from "./panel";
|
|
18
|
+
import { bridgeAvailabilitySchema, bridgeCellsSchema, bridgeManifestSchema } from "./panel";
|
|
19
19
|
|
|
20
20
|
const text = (value: string) => ({
|
|
21
21
|
ar: value,
|
|
@@ -717,3 +717,69 @@ describe("the cell schema", () => {
|
|
|
717
717
|
).toBe(false);
|
|
718
718
|
});
|
|
719
719
|
});
|
|
720
|
+
|
|
721
|
+
describe("the availability schema", () => {
|
|
722
|
+
test("accepts reasons for the module and per subject", () => {
|
|
723
|
+
const availability = {
|
|
724
|
+
module: {
|
|
725
|
+
wipe: text("Closed right now"),
|
|
726
|
+
},
|
|
727
|
+
subjects: {
|
|
728
|
+
"world 1": {
|
|
729
|
+
delete: text("You cannot delete the active world"),
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
expect(bridgeAvailabilitySchema.parse(availability)).toEqual(availability);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test("accepts an answer with nothing unavailable", () => {
|
|
738
|
+
expect(
|
|
739
|
+
bridgeAvailabilitySchema.parse({
|
|
740
|
+
module: {},
|
|
741
|
+
subjects: {},
|
|
742
|
+
}),
|
|
743
|
+
).toEqual({
|
|
744
|
+
module: {},
|
|
745
|
+
subjects: {},
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test("rejects a reason that is not localized", () => {
|
|
750
|
+
expect(
|
|
751
|
+
bridgeAvailabilitySchema.safeParse({
|
|
752
|
+
module: {
|
|
753
|
+
wipe: "closed",
|
|
754
|
+
},
|
|
755
|
+
subjects: {},
|
|
756
|
+
}).success,
|
|
757
|
+
).toBe(false);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test("keeps a manifest module that advertises availability among its actions", () => {
|
|
761
|
+
const parsed = bridgeManifestSchema.parse({
|
|
762
|
+
...manifest(),
|
|
763
|
+
modules: [
|
|
764
|
+
{
|
|
765
|
+
id: "worlds",
|
|
766
|
+
kind: BridgeKind.Collection,
|
|
767
|
+
requiresRunning: false,
|
|
768
|
+
refreshSeconds: null,
|
|
769
|
+
pageSize: null,
|
|
770
|
+
dependsOn: [],
|
|
771
|
+
actions: [
|
|
772
|
+
BridgeAction.List,
|
|
773
|
+
"delete",
|
|
774
|
+
BridgeAction.Availability,
|
|
775
|
+
],
|
|
776
|
+
protectedActions: [
|
|
777
|
+
"delete",
|
|
778
|
+
],
|
|
779
|
+
},
|
|
780
|
+
],
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
expect(parsed.modules[0]?.actions).toContain(BridgeAction.Availability);
|
|
784
|
+
});
|
|
785
|
+
});
|
package/src/wire/panel.ts
CHANGED
|
@@ -68,6 +68,17 @@ export const panelTextSchema = z.object({
|
|
|
68
68
|
en: z.string().min(1).max(PANEL_TEXT_LENGTH_LIMIT),
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
export const bridgeUnavailableSchema = z.record(zKey, panelTextSchema);
|
|
72
|
+
|
|
73
|
+
export type BridgeUnavailableInput = z.infer<typeof bridgeUnavailableSchema>;
|
|
74
|
+
|
|
75
|
+
export const bridgeAvailabilitySchema = z.object({
|
|
76
|
+
module: bridgeUnavailableSchema,
|
|
77
|
+
subjects: z.record(z.string().min(1), bridgeUnavailableSchema),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export type BridgeAvailabilityInput = z.infer<typeof bridgeAvailabilitySchema>;
|
|
81
|
+
|
|
71
82
|
export const bridgeCellSchema = z.union([
|
|
72
83
|
bridgeValueSchema,
|
|
73
84
|
panelTextSchema,
|