@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/package.json
CHANGED
package/src/bot/botPanel.test.ts
CHANGED
|
@@ -213,4 +213,34 @@ describe("the bot modules", () => {
|
|
|
213
213
|
|
|
214
214
|
await expect(module.actions[BotDeployAction.Redeploy]?.(context, {})).rejects.toThrow(BridgeUserError);
|
|
215
215
|
});
|
|
216
|
+
|
|
217
|
+
test("greys out both deploy buttons with the refusal's words while a running bot has not taken a deploy", async () => {
|
|
218
|
+
const module = createBotDeployModule();
|
|
219
|
+
const pending = deployContext(true, [
|
|
220
|
+
DEPLOY_MARKER,
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
const unavailable = await module.unavailable?.(pending.context);
|
|
224
|
+
|
|
225
|
+
expect(Object.keys(unavailable ?? {}).sort()).toEqual([
|
|
226
|
+
BotDeployAction.Redeploy,
|
|
227
|
+
BotDeployAction.Reinstall,
|
|
228
|
+
]);
|
|
229
|
+
await expect(module.actions[BotDeployAction.Redeploy]?.(pending.context, {})).rejects.toThrow(
|
|
230
|
+
unavailable?.[BotDeployAction.Redeploy]?.en,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("offers both deploy buttons when nothing is pending or the bot is stopped", async () => {
|
|
235
|
+
const module = createBotDeployModule();
|
|
236
|
+
|
|
237
|
+
expect(await module.unavailable?.(deployContext(true).context)).toEqual({});
|
|
238
|
+
expect(
|
|
239
|
+
await module.unavailable?.(
|
|
240
|
+
deployContext(false, [
|
|
241
|
+
DEPLOY_MARKER,
|
|
242
|
+
]).context,
|
|
243
|
+
),
|
|
244
|
+
).toEqual({});
|
|
245
|
+
});
|
|
216
246
|
});
|
package/src/bot/botPanel.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BridgeContext } from "../context";
|
|
2
|
-
import { BridgeKind, BridgeUserError, type BridgeValues, type LocalizedText } from "../core";
|
|
2
|
+
import { BridgeKind, type BridgeUnavailable, BridgeUserError, type BridgeValues, type LocalizedText } from "../core";
|
|
3
3
|
import type { BridgeActionsModule, BridgeDetailModule } from "../modules";
|
|
4
4
|
import { BridgeIcon, BridgeLayout, BridgePlace, type BridgeTab } from "../panel";
|
|
5
5
|
import { type DeployRequest, deployPending, requestDeploy } from "./botDeploy";
|
|
@@ -117,5 +117,13 @@ export const createBotDeployModule = (): BridgeActionsModule => {
|
|
|
117
117
|
[BotDeployAction.Redeploy]: deploy(false),
|
|
118
118
|
[BotDeployAction.Reinstall]: deploy(true),
|
|
119
119
|
},
|
|
120
|
+
async unavailable(context): Promise<BridgeUnavailable> {
|
|
121
|
+
return (await deployPending(context))
|
|
122
|
+
? {
|
|
123
|
+
[BotDeployAction.Redeploy]: DEPLOY_PENDING,
|
|
124
|
+
[BotDeployAction.Reinstall]: DEPLOY_PENDING,
|
|
125
|
+
}
|
|
126
|
+
: {};
|
|
127
|
+
},
|
|
120
128
|
};
|
|
121
129
|
};
|
package/src/cli/validate.test.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type BridgeTab,
|
|
17
17
|
type BridgeTableSection,
|
|
18
18
|
} from "../panel";
|
|
19
|
-
import { placementReportLines, runValidate, setupReportLines } from "./validate";
|
|
19
|
+
import { availabilityReportLines, placementReportLines, runValidate, setupReportLines } from "./validate";
|
|
20
20
|
|
|
21
21
|
const text = (value: string) => ({
|
|
22
22
|
ar: value,
|
|
@@ -602,3 +602,75 @@ describe("validating a bot package", () => {
|
|
|
602
602
|
);
|
|
603
603
|
});
|
|
604
604
|
});
|
|
605
|
+
|
|
606
|
+
describe("availability report", () => {
|
|
607
|
+
const driverWithModules = (modules: BridgeDriver["modules"]): BridgeDriver => ({
|
|
608
|
+
install: {
|
|
609
|
+
kind: BridgeKind.Install,
|
|
610
|
+
run: async () => {
|
|
611
|
+
return;
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
lifecycle: {
|
|
615
|
+
kind: BridgeKind.Lifecycle,
|
|
616
|
+
command: async () => [
|
|
617
|
+
"run",
|
|
618
|
+
],
|
|
619
|
+
},
|
|
620
|
+
modules,
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test("warns about a protected module that cannot say when its action is unavailable", () => {
|
|
624
|
+
expect(
|
|
625
|
+
availabilityReportLines(
|
|
626
|
+
driverWithModules({
|
|
627
|
+
worlds: {
|
|
628
|
+
kind: BridgeKind.Collection,
|
|
629
|
+
protectedActions: [
|
|
630
|
+
"delete",
|
|
631
|
+
],
|
|
632
|
+
list: async () => [],
|
|
633
|
+
actions: {
|
|
634
|
+
delete: async () => {
|
|
635
|
+
return;
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
}),
|
|
640
|
+
),
|
|
641
|
+
).toEqual([
|
|
642
|
+
' warning: module "worlds" protects delete but declares no `unavailable` — the panel offers the action everywhere and the platform stops the game before the driver can refuse it',
|
|
643
|
+
]);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test("says nothing when the protected module declares the hook or protects nothing", () => {
|
|
647
|
+
expect(
|
|
648
|
+
availabilityReportLines(
|
|
649
|
+
driverWithModules({
|
|
650
|
+
worlds: {
|
|
651
|
+
kind: BridgeKind.Collection,
|
|
652
|
+
protectedActions: [
|
|
653
|
+
"delete",
|
|
654
|
+
],
|
|
655
|
+
list: async () => [],
|
|
656
|
+
actions: {
|
|
657
|
+
delete: async () => {
|
|
658
|
+
return;
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
unavailable: async () => ({}),
|
|
662
|
+
},
|
|
663
|
+
players: {
|
|
664
|
+
kind: BridgeKind.Collection,
|
|
665
|
+
list: async () => [],
|
|
666
|
+
actions: {
|
|
667
|
+
kick: async () => {
|
|
668
|
+
return;
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
}),
|
|
673
|
+
),
|
|
674
|
+
).toEqual([]);
|
|
675
|
+
});
|
|
676
|
+
});
|
package/src/cli/validate.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { type ArticlePack, compileArticles } from "../blog";
|
|
7
7
|
import { DETECT_TABLE_FILE, renderDetectTable } from "../bot/detect";
|
|
8
|
-
import { PortProtocol } from "../core";
|
|
8
|
+
import { BridgeKind, PortProtocol } from "../core";
|
|
9
9
|
import { GuideOpenTab, type GuideOpenTarget, type GuidePack, readGuidePack, readGuidePackage } from "../guides";
|
|
10
10
|
import {
|
|
11
11
|
declaredFiles,
|
|
@@ -279,6 +279,39 @@ const reportPlacement = (driver: BridgeDriver) => {
|
|
|
279
279
|
}
|
|
280
280
|
};
|
|
281
281
|
|
|
282
|
+
export const availabilityReportLines = (driver: BridgeDriver): string[] => {
|
|
283
|
+
const lines: string[] = [];
|
|
284
|
+
|
|
285
|
+
for (const [id, module] of Object.entries(driver.modules ?? {})) {
|
|
286
|
+
if (
|
|
287
|
+
module.kind !== BridgeKind.Actions
|
|
288
|
+
&& module.kind !== BridgeKind.Catalog
|
|
289
|
+
&& module.kind !== BridgeKind.Collection
|
|
290
|
+
&& module.kind !== BridgeKind.Settings
|
|
291
|
+
) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const protectedActions = module.protectedActions ?? [];
|
|
296
|
+
|
|
297
|
+
if (protectedActions.length === 0 || module.unavailable) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
lines.push(
|
|
302
|
+
` warning: module "${id}" protects ${protectedActions.join(", ")} but declares no \`unavailable\` — the panel offers the action everywhere and the platform stops the game before the driver can refuse it`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return lines;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const reportAvailability = (driver: BridgeDriver) => {
|
|
310
|
+
for (const line of availabilityReportLines(driver)) {
|
|
311
|
+
console.log(line);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
282
315
|
const openTargetOf = (target: GuideOpenTarget) => {
|
|
283
316
|
if (target.tab === GuideOpenTab.Panel) {
|
|
284
317
|
return `panel tab "${target.tabId}"`;
|
|
@@ -432,6 +465,7 @@ const validateRunnablePackage = async (
|
|
|
432
465
|
reportEvents(driver);
|
|
433
466
|
reportActionTargets(driver);
|
|
434
467
|
reportPlacement(driver);
|
|
468
|
+
reportAvailability(driver);
|
|
435
469
|
};
|
|
436
470
|
|
|
437
471
|
export const runValidate = async (args: string[]) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BridgeStream, BridgeValues } from "../core";
|
|
1
|
+
import type { BridgeActivity, BridgeStream, BridgeValues } from "../core";
|
|
2
2
|
import type { BridgeHttpMethod, BridgeSetupRuntime } from "../protocol";
|
|
3
3
|
|
|
4
4
|
export interface BridgeServer {
|
|
@@ -189,6 +189,10 @@ export interface BridgeSetupReporter {
|
|
|
189
189
|
clear(): void;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
export interface BridgeActivityReporter {
|
|
193
|
+
report(entry: BridgeActivity): void;
|
|
194
|
+
}
|
|
195
|
+
|
|
192
196
|
export interface BridgeLog {
|
|
193
197
|
(message: string, fields?: BridgeValues): void;
|
|
194
198
|
warn(message: string, fields?: BridgeValues): void;
|
|
@@ -212,6 +216,7 @@ export interface BridgeContext {
|
|
|
212
216
|
exec(command: string[], options?: BridgeExecOptions): Promise<BridgeExecResult>;
|
|
213
217
|
logs: BridgeLogs;
|
|
214
218
|
setup: BridgeSetupReporter;
|
|
219
|
+
activity: BridgeActivityReporter;
|
|
215
220
|
emit(event: string, payload?: BridgeValues): void;
|
|
216
221
|
restart(): Promise<void>;
|
|
217
222
|
log: BridgeLog;
|
package/src/core/bridgeAction.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export enum BridgeAction {
|
|
2
2
|
Add = "add",
|
|
3
|
+
Availability = "availability",
|
|
3
4
|
Install = "install",
|
|
4
5
|
Installed = "installed",
|
|
5
6
|
List = "list",
|
|
@@ -16,6 +17,7 @@ export enum BridgeAction {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export const BRIDGE_READ_ACTIONS: readonly string[] = [
|
|
20
|
+
BridgeAction.Availability,
|
|
19
21
|
BridgeAction.Releases,
|
|
20
22
|
BridgeAction.Installed,
|
|
21
23
|
BridgeAction.List,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BridgeEventName } from "../protocol/bridgeEvents";
|
|
2
|
+
import type { BridgeValues, LocalizedText } from "./bridgeValue";
|
|
3
|
+
|
|
4
|
+
export interface BridgeActivity {
|
|
5
|
+
name: BridgeEventName;
|
|
6
|
+
text: LocalizedText;
|
|
7
|
+
subject?: string;
|
|
8
|
+
details?: BridgeValues;
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LocalizedText } from "./bridgeValue";
|
|
2
|
+
|
|
3
|
+
export const BRIDGE_AVAILABILITY_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
4
|
+
|
|
5
|
+
export type BridgeUnavailable = Record<string, LocalizedText>;
|
|
6
|
+
|
|
7
|
+
export interface BridgeAvailability {
|
|
8
|
+
module: BridgeUnavailable;
|
|
9
|
+
subjects: Record<string, BridgeUnavailable>;
|
|
10
|
+
}
|
|
@@ -17,6 +17,16 @@ describe("BridgeUserError", () => {
|
|
|
17
17
|
expect(error.text).toEqual(text);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
test("keeps the cause a driver attaches for the staff log", () => {
|
|
21
|
+
const cause = new Error("cdn.modrinth.com answered 404");
|
|
22
|
+
const error = new BridgeUserError(text, {
|
|
23
|
+
cause,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(error.cause).toBe(cause);
|
|
27
|
+
expect(error.message).toBe(text.en);
|
|
28
|
+
});
|
|
29
|
+
|
|
20
30
|
test("is the class a driver reaches through the published namespace", () => {
|
|
21
31
|
expect(new Bridge.UserError(text)).toBeInstanceOf(BridgeUserError);
|
|
22
32
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { LocalizedText } from "./bridgeValue";
|
|
2
2
|
|
|
3
|
+
export interface BridgeUserErrorOptions {
|
|
4
|
+
cause?: unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
3
7
|
export class BridgeUserError extends Error {
|
|
4
8
|
readonly text: LocalizedText;
|
|
5
9
|
|
|
6
|
-
constructor(text: LocalizedText) {
|
|
7
|
-
super(text.en);
|
|
10
|
+
constructor(text: LocalizedText, options: BridgeUserErrorOptions = {}) {
|
|
11
|
+
super(text.en, options);
|
|
8
12
|
|
|
9
13
|
this.name = "BridgeUserError";
|
|
10
14
|
this.text = text;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { BridgeDownloadError } from "./bridgeDownloadError";
|
|
3
|
+
import { BridgeNetError } from "./bridgeNetError";
|
|
4
|
+
import { describeDownloadFailure } from "./describeDownloadFailure";
|
|
5
|
+
|
|
6
|
+
const SUBJECT = "All the Mods 9";
|
|
7
|
+
|
|
8
|
+
describe("describeDownloadFailure", () => {
|
|
9
|
+
test("says the file is gone for 404 and 410", () => {
|
|
10
|
+
for (const status of [
|
|
11
|
+
404,
|
|
12
|
+
410,
|
|
13
|
+
]) {
|
|
14
|
+
expect(describeDownloadFailure(new BridgeDownloadError(status, "gone"), SUBJECT).en).toBe(
|
|
15
|
+
"All the Mods 9 is no longer available from its source.",
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("says the source refused for 401 and 403", () => {
|
|
21
|
+
expect(describeDownloadFailure(new BridgeNetError(403, "forbidden"), SUBJECT).en).toBe(
|
|
22
|
+
"The source refused the download of All the Mods 9.",
|
|
23
|
+
);
|
|
24
|
+
expect(describeDownloadFailure(new BridgeDownloadError(401, "unauthorized"), SUBJECT).ar).toBe(
|
|
25
|
+
"المصدر رفض تنزيل All the Mods 9.",
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("says the source is busy for 429", () => {
|
|
30
|
+
expect(describeDownloadFailure(new BridgeDownloadError(429, "slow down"), SUBJECT).en).toContain("busy");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("says the source is down for a 5xx", () => {
|
|
34
|
+
expect(describeDownloadFailure(new BridgeNetError(503, "unavailable"), SUBJECT).en).toBe(
|
|
35
|
+
"The source of All the Mods 9 is down right now. Try again in a bit.",
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("says the source could not be reached when there is no status", () => {
|
|
40
|
+
expect(describeDownloadFailure(new BridgeDownloadError(null, "ECONNRESET"), SUBJECT).en).toBe(
|
|
41
|
+
"We could not reach the source to download All the Mods 9. Try again.",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("falls back to a plain download failure for anything else", () => {
|
|
46
|
+
const generic = "We could not download All the Mods 9. Try again.";
|
|
47
|
+
|
|
48
|
+
expect(describeDownloadFailure(new BridgeDownloadError(400, "bad request"), SUBJECT).en).toBe(generic);
|
|
49
|
+
expect(describeDownloadFailure(new Error("digest mismatch"), SUBJECT).en).toBe(generic);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("always writes both languages", () => {
|
|
53
|
+
const text = describeDownloadFailure(new BridgeDownloadError(500, "boom"), SUBJECT);
|
|
54
|
+
|
|
55
|
+
expect(text.ar.length).toBeGreaterThan(0);
|
|
56
|
+
expect(text.en.length).toBeGreaterThan(0);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { BridgeDownloadError } from "./bridgeDownloadError";
|
|
2
|
+
import { BridgeNetError } from "./bridgeNetError";
|
|
3
|
+
import type { LocalizedText } from "./bridgeValue";
|
|
4
|
+
|
|
5
|
+
const REMOVED_STATUSES: readonly number[] = [
|
|
6
|
+
404,
|
|
7
|
+
410,
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const REFUSED_STATUSES: readonly number[] = [
|
|
11
|
+
401,
|
|
12
|
+
403,
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const BUSY_STATUS = 429;
|
|
16
|
+
|
|
17
|
+
const SERVER_ERROR_STATUS = 500;
|
|
18
|
+
|
|
19
|
+
const statusOf = (error: unknown): number | null | undefined => {
|
|
20
|
+
if (error instanceof BridgeDownloadError || error instanceof BridgeNetError) {
|
|
21
|
+
return error.status;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return undefined;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const describeDownloadFailure = (error: unknown, subject: string): LocalizedText => {
|
|
28
|
+
const status = statusOf(error);
|
|
29
|
+
|
|
30
|
+
if (status === null) {
|
|
31
|
+
return {
|
|
32
|
+
ar: `ما قدرنا نوصل للمصدر عشان ننزّل ${subject}. جرّب مرة ثانية.`,
|
|
33
|
+
en: `We could not reach the source to download ${subject}. Try again.`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (status !== undefined && REMOVED_STATUSES.includes(status)) {
|
|
38
|
+
return {
|
|
39
|
+
ar: `${subject} ما عاد موجود عند المصدر.`,
|
|
40
|
+
en: `${subject} is no longer available from its source.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (status !== undefined && REFUSED_STATUSES.includes(status)) {
|
|
45
|
+
return {
|
|
46
|
+
ar: `المصدر رفض تنزيل ${subject}.`,
|
|
47
|
+
en: `The source refused the download of ${subject}.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (status === BUSY_STATUS) {
|
|
52
|
+
return {
|
|
53
|
+
ar: `المصدر مشغول الحين وما قدرنا ننزّل ${subject}. جرّب بعد شوي.`,
|
|
54
|
+
en: `The source is busy right now, so we could not download ${subject}. Try again in a bit.`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (status !== undefined && status >= SERVER_ERROR_STATUS) {
|
|
59
|
+
return {
|
|
60
|
+
ar: `مصدر ${subject} فيه عطل الحين. جرّب بعد شوي.`,
|
|
61
|
+
en: `The source of ${subject} is down right now. Try again in a bit.`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
ar: `ما قدرنا ننزّل ${subject}. جرّب مرة ثانية.`,
|
|
67
|
+
en: `We could not download ${subject}. Try again.`,
|
|
68
|
+
};
|
|
69
|
+
};
|
package/src/core/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from "./bridgeAction";
|
|
2
|
+
export * from "./bridgeActivity";
|
|
3
|
+
export * from "./bridgeAvailability";
|
|
2
4
|
export * from "./bridgeDownloadError";
|
|
3
5
|
export * from "./bridgeFailure";
|
|
4
6
|
export * from "./bridgeHold";
|
|
@@ -14,3 +16,4 @@ export * from "./bridgeSetup";
|
|
|
14
16
|
export * from "./bridgeStream";
|
|
15
17
|
export * from "./bridgeUserError";
|
|
16
18
|
export * from "./bridgeValue";
|
|
19
|
+
export * from "./describeDownloadFailure";
|
package/src/modules/bridge.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { BridgeContext, BridgeExecResult, BridgeFileEntry, BridgeReply } from "../context";
|
|
1
|
+
import type { BridgeActivityReporter, BridgeContext, BridgeExecResult, BridgeFileEntry, BridgeReply } from "../context";
|
|
2
2
|
import type {
|
|
3
|
+
BridgeActivity,
|
|
4
|
+
BridgeAvailability,
|
|
3
5
|
BridgeCell,
|
|
4
6
|
BridgeCells,
|
|
5
7
|
BridgeCopy,
|
|
@@ -7,6 +9,7 @@ import type {
|
|
|
7
9
|
BridgeInstallOutcome,
|
|
8
10
|
BridgeOption,
|
|
9
11
|
BridgeRow,
|
|
12
|
+
BridgeUnavailable,
|
|
10
13
|
BridgeValue,
|
|
11
14
|
BridgeValues,
|
|
12
15
|
LocalizedText,
|
|
@@ -75,8 +78,11 @@ import type {
|
|
|
75
78
|
|
|
76
79
|
export namespace Bridge {
|
|
77
80
|
export type Actions = BridgeActionsModule;
|
|
81
|
+
export type Activity = BridgeActivity;
|
|
82
|
+
export type ActivityReporter = BridgeActivityReporter;
|
|
78
83
|
export type AddOutcome = BridgeAddOutcome;
|
|
79
84
|
export type Announce = BridgeAnnounceModule;
|
|
85
|
+
export type Availability = BridgeAvailability;
|
|
80
86
|
export type Backup = BridgeBackupModule;
|
|
81
87
|
export type Catalog = BridgeCatalogModule;
|
|
82
88
|
export type CatalogEntry = BridgeCatalogEntry;
|
|
@@ -138,6 +144,7 @@ export namespace Bridge {
|
|
|
138
144
|
export type TerminalRule = BridgeTerminalRule;
|
|
139
145
|
export type Tab = BridgeTab;
|
|
140
146
|
export type TabRef = BridgeTabRef;
|
|
147
|
+
export type Unavailable = BridgeUnavailable;
|
|
141
148
|
export type UploadMode = BridgeUploadMode;
|
|
142
149
|
export const UserError = BridgeUserError;
|
|
143
150
|
export type UserError = BridgeUserError;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { BridgeContext, BridgeReply } from "../context";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
BridgeInstallOutcome,
|
|
4
|
+
BridgeKind,
|
|
5
|
+
BridgeOption,
|
|
6
|
+
BridgeRow,
|
|
7
|
+
BridgeStream,
|
|
8
|
+
BridgeUnavailable,
|
|
9
|
+
BridgeValues,
|
|
10
|
+
} from "../core";
|
|
3
11
|
import type { BridgeAddOutcome, BridgeDetail, BridgeDetailOutcome } from "../panel";
|
|
4
12
|
import type {
|
|
5
13
|
BridgeCatalogEntry,
|
|
@@ -115,6 +123,7 @@ export interface BridgeSettingsModule {
|
|
|
115
123
|
protectedActions?: string[];
|
|
116
124
|
read(context: BridgeContext): Promise<BridgeValues>;
|
|
117
125
|
write?(context: BridgeContext, values: BridgeValues): Promise<void>;
|
|
126
|
+
unavailable?(context: BridgeContext): Promise<BridgeUnavailable>;
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
export type BridgeRowAction<Row extends BridgeRow = BridgeRow> = (
|
|
@@ -130,6 +139,7 @@ export interface BridgeActionsModule {
|
|
|
130
139
|
protectedActions?: string[];
|
|
131
140
|
requiresRunning?: boolean;
|
|
132
141
|
actions: Record<string, BridgeServerAction>;
|
|
142
|
+
unavailable?(context: BridgeContext): Promise<BridgeUnavailable>;
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
export interface BridgeCollectionModule<Row extends BridgeRow = BridgeRow> {
|
|
@@ -140,6 +150,7 @@ export interface BridgeCollectionModule<Row extends BridgeRow = BridgeRow> {
|
|
|
140
150
|
list(context: BridgeContext): Promise<Row[]>;
|
|
141
151
|
add?(context: BridgeContext, input: string): Promise<BridgeAddOutcome | undefined>;
|
|
142
152
|
actions?: Record<string, BridgeRowAction<Row>>;
|
|
153
|
+
unavailable?(context: BridgeContext, row: Row): Promise<BridgeUnavailable>;
|
|
143
154
|
}
|
|
144
155
|
|
|
145
156
|
export type BridgeDetailAction = (context: BridgeContext, args: BridgeValues) => Promise<BridgeDetailOutcome | null>;
|
|
@@ -164,6 +175,7 @@ export interface BridgeCatalogModule {
|
|
|
164
175
|
install(context: BridgeContext, id: string, releaseId?: string): Promise<BridgeCatalogEntry>;
|
|
165
176
|
remove(context: BridgeContext, id: string): Promise<void>;
|
|
166
177
|
toggle?(context: BridgeContext, id: string, enabled: boolean): Promise<void>;
|
|
178
|
+
unavailable?(context: BridgeContext, entry: BridgeCatalogEntry): Promise<BridgeUnavailable>;
|
|
167
179
|
}
|
|
168
180
|
|
|
169
181
|
export type BridgePanelModule =
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BridgeCopy, BridgeValue, LocalizedText } from "../core";
|
|
1
|
+
import type { BridgeCopy, BridgeUnavailable, BridgeValue, LocalizedText } from "../core";
|
|
2
2
|
import type { BridgeIcon } from "./bridgeIcon";
|
|
3
3
|
|
|
4
4
|
export enum BridgeDetailTone {
|
|
@@ -45,6 +45,7 @@ export interface BridgeDetail {
|
|
|
45
45
|
links: BridgeDetailLink[];
|
|
46
46
|
stale: boolean;
|
|
47
47
|
actions: string[];
|
|
48
|
+
unavailable?: BridgeUnavailable;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export interface BridgeDetailOutcome {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BridgeActivity, BridgeValues } from "../core";
|
|
2
|
+
|
|
3
|
+
export const BRIDGE_ACTIVITY_KEYS = {
|
|
4
|
+
ar: "activityAr",
|
|
5
|
+
en: "activityEn",
|
|
6
|
+
subject: "subject",
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export const BRIDGE_ACTIVITY_LIMIT = 8;
|
|
10
|
+
|
|
11
|
+
export const BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT = 256;
|
|
12
|
+
|
|
13
|
+
export const BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT = 64;
|
|
14
|
+
|
|
15
|
+
export const BRIDGE_ACTIVITY_DETAILS_LIMIT = 16;
|
|
16
|
+
|
|
17
|
+
export const BRIDGE_ACTIVITY_DETAIL_KEY_LENGTH_LIMIT = 64;
|
|
18
|
+
|
|
19
|
+
export const BRIDGE_ACTIVITY_EXCERPT_LENGTH_LIMIT = 80;
|
|
20
|
+
|
|
21
|
+
export const BRIDGE_ACTIVITY_NAME_LENGTH_LIMIT = 64;
|
|
22
|
+
|
|
23
|
+
export const BRIDGE_ACTIVITY_NAME_PATTERN = /^[A-Z][A-Za-z0-9]*$/;
|
|
24
|
+
|
|
25
|
+
export const BRIDGE_ACTIVITY_DETAIL_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
26
|
+
|
|
27
|
+
const ELLIPSIS = "…";
|
|
28
|
+
|
|
29
|
+
export const isBridgeActivity = (payload: BridgeValues | null): boolean => {
|
|
30
|
+
return typeof payload?.[BRIDGE_ACTIVITY_KEYS.ar] === "string" && typeof payload[BRIDGE_ACTIVITY_KEYS.en] === "string";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const activityPayload = (entry: BridgeActivity): BridgeValues => {
|
|
34
|
+
return {
|
|
35
|
+
...entry.details,
|
|
36
|
+
[BRIDGE_ACTIVITY_KEYS.ar]: entry.text.ar,
|
|
37
|
+
[BRIDGE_ACTIVITY_KEYS.en]: entry.text.en,
|
|
38
|
+
...(entry.subject === undefined
|
|
39
|
+
? {}
|
|
40
|
+
: {
|
|
41
|
+
[BRIDGE_ACTIVITY_KEYS.subject]: entry.subject,
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const activityExcerpt = (text: string): string => {
|
|
47
|
+
const line = text.trim();
|
|
48
|
+
|
|
49
|
+
if (line.length <= BRIDGE_ACTIVITY_EXCERPT_LENGTH_LIMIT) {
|
|
50
|
+
return line;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return `${line.slice(0, BRIDGE_ACTIVITY_EXCERPT_LENGTH_LIMIT - ELLIPSIS.length).trimEnd()}${ELLIPSIS}`;
|
|
54
|
+
};
|
|
@@ -1,23 +1,53 @@
|
|
|
1
1
|
export enum BridgeEventName {
|
|
2
|
+
AccessGranted = "AccessGranted",
|
|
3
|
+
AccessRevoked = "AccessRevoked",
|
|
4
|
+
BroadcastSent = "BroadcastSent",
|
|
5
|
+
GameInstalled = "GameInstalled",
|
|
2
6
|
GameModeChanged = "GameModeChanged",
|
|
7
|
+
GameplayChanged = "GameplayChanged",
|
|
8
|
+
InstallFailed = "InstallFailed",
|
|
3
9
|
MemoryUndersized = "MemoryUndersized",
|
|
4
10
|
MissingDependency = "MissingDependency",
|
|
5
11
|
ModCrashed = "ModCrashed",
|
|
12
|
+
ModDisabled = "ModDisabled",
|
|
13
|
+
ModEnabled = "ModEnabled",
|
|
14
|
+
ModInstalled = "ModInstalled",
|
|
6
15
|
ModLoaded = "ModLoaded",
|
|
16
|
+
ModLoaderInstalled = "ModLoaderInstalled",
|
|
17
|
+
ModLoaderRemoved = "ModLoaderRemoved",
|
|
18
|
+
ModRemoved = "ModRemoved",
|
|
19
|
+
ModpackInstalled = "ModpackInstalled",
|
|
20
|
+
ModpackRemoved = "ModpackRemoved",
|
|
21
|
+
ModpackSelected = "ModpackSelected",
|
|
7
22
|
PlayerAdvanced = "PlayerAdvanced",
|
|
8
23
|
PlayerBanned = "PlayerBanned",
|
|
24
|
+
PlayerChanged = "PlayerChanged",
|
|
9
25
|
PlayerDied = "PlayerDied",
|
|
10
26
|
PlayerJoined = "PlayerJoined",
|
|
11
27
|
PlayerKicked = "PlayerKicked",
|
|
12
28
|
PlayerLeft = "PlayerLeft",
|
|
29
|
+
PlayerUnbanned = "PlayerUnbanned",
|
|
13
30
|
PortBindFailed = "PortBindFailed",
|
|
31
|
+
SecretRotated = "SecretRotated",
|
|
14
32
|
ServerCrashed = "ServerCrashed",
|
|
15
33
|
ServerStarted = "ServerStarted",
|
|
16
34
|
ServerStopping = "ServerStopping",
|
|
17
35
|
ServerUpdated = "ServerUpdated",
|
|
36
|
+
SettingsApplied = "SettingsApplied",
|
|
37
|
+
SettingsChanged = "SettingsChanged",
|
|
38
|
+
SetupAction = "SetupAction",
|
|
39
|
+
SetupCompleted = "SetupCompleted",
|
|
40
|
+
SetupRejected = "SetupRejected",
|
|
41
|
+
SignInExpired = "SignInExpired",
|
|
18
42
|
TickLagging = "TickLagging",
|
|
43
|
+
WorldActivated = "WorldActivated",
|
|
44
|
+
WorldCloned = "WorldCloned",
|
|
19
45
|
WorldCorrupt = "WorldCorrupt",
|
|
46
|
+
WorldCreated = "WorldCreated",
|
|
47
|
+
WorldDeleted = "WorldDeleted",
|
|
48
|
+
WorldImported = "WorldImported",
|
|
20
49
|
WorldSaved = "WorldSaved",
|
|
50
|
+
WorldWiped = "WorldWiped",
|
|
21
51
|
WrongJavaVersion = "WrongJavaVersion",
|
|
22
52
|
}
|
|
23
53
|
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BridgeActivity,
|
|
3
|
+
BridgeDesired,
|
|
4
|
+
BridgeInstallHold,
|
|
5
|
+
BridgeRuntime,
|
|
6
|
+
BridgeValues,
|
|
7
|
+
LocalizedText,
|
|
8
|
+
} from "../core";
|
|
2
9
|
import type { BridgeSetupSummary } from "./bridgeSetup";
|
|
3
10
|
|
|
4
11
|
export const BRIDGE_SOCKET_NAME = "bridge.sock";
|
|
@@ -59,6 +66,7 @@ export interface BridgeSuccess {
|
|
|
59
66
|
id: number | null;
|
|
60
67
|
ok: true;
|
|
61
68
|
result: unknown;
|
|
69
|
+
activity?: BridgeActivity[];
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
export interface BridgeFailure {
|
|
@@ -68,6 +76,7 @@ export interface BridgeFailure {
|
|
|
68
76
|
secret?: string;
|
|
69
77
|
failure?: string;
|
|
70
78
|
text?: LocalizedText;
|
|
79
|
+
unchanged?: boolean;
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
export type BridgeResponse = BridgeFailure | BridgeSuccess;
|