Add account based multiplayer hosting and bug report retention

This commit is contained in:
2026-10-10 16:24:34 +05:00
parent 9af71c85c2
commit fc6da6756d
33 changed files with 1538 additions and 102 deletions

View File

@@ -3,7 +3,14 @@ import {
type BugReportRecord,
} from "../shared/bug-reports";
type Issue = { id: string; identifier: string; team: { id: string } };
type Issue = {
id: string;
identifier: string;
team: { id: string };
state?: { id: string; name: string; type: string };
completedAt?: string | null;
};
export class LinearPermissionError extends Error {}
export class LinearSupportClient {
constructor(
private key: string,
@@ -34,13 +41,26 @@ export class LinearSupportClient {
)
)
return null;
if (
response.status === 401 ||
response.status === 403 ||
result.errors?.some(
(error: { message?: string; extensions?: { code?: string } }) =>
/forbidden|permission|authentication|unauthorized/i.test(
`${error.extensions?.code ?? ""} ${error.message ?? ""}`,
),
)
)
throw new LinearPermissionError(
"Support delivery is temporarily unavailable. Check Linear Write permission.",
);
if (!response.ok || result.errors?.length || !result.data)
throw new Error("Support delivery is temporarily unavailable");
return result.data;
}
async issue(id: string): Promise<Issue | null> {
const data = await this.graphql<{ issue: Issue }>(
"query SupportIssue($id: String!) { issue(id: $id) { id identifier team { id } } }",
"query SupportIssue($id: String!) { issue(id: $id) { id identifier team { id } state { id name type } completedAt } }",
{ id },
true,
);
@@ -49,6 +69,47 @@ export class LinearSupportClient {
throw new Error("Support issue team mismatch");
return issue;
}
async done(id: string) {
const issue = await this.issue(id);
if (!issue) throw new Error("Support issue was not found");
if (
issue.state?.type === "completed" &&
issue.state.name.toLowerCase() === "done"
)
return;
const data = await this.graphql<{
team: { states: { nodes: { id: string; name: string; type: string }[] } };
}>(
"query SupportDoneState($team: String!) { team(id:$team) { states { nodes { id name type } } } }",
{ team: this.team },
);
const state = data?.team.states.nodes.find(
(s) => s.type === "completed" && s.name.toLowerCase() === "done",
);
if (!state)
throw new Error("The support team needs a Done workflow status");
const result = await this.graphql<{
issueUpdate: { success: boolean; issue: Issue };
}>(
"mutation SupportDone($id: String!, $input: IssueUpdateInput!) { issueUpdate(id:$id,input:$input) { success issue { id state { id name type } } } }",
{ id, input: { stateId: state.id } },
);
if (
!result?.issueUpdate.success ||
result.issueUpdate.issue.id !== id ||
result.issueUpdate.issue.state?.id !== state.id
)
throw new Error("Support issue status was not confirmed");
}
async remove(id: string) {
if (!(await this.issue(id))) return;
const result = await this.graphql<{ issueDelete: { success: boolean } }>(
"mutation SupportDelete($id: String!) { issueDelete(id:$id) { success } }",
{ id },
);
if (!result?.issueDelete.success)
throw new Error("Support issue deletion was not confirmed");
}
async createIssue(report: BugReportRecord): Promise<Issue> {
const existing = await this.issue(report.id);
if (existing) return existing;
@@ -65,6 +126,7 @@ export class LinearSupportClient {
"",
"Reply to the player with #player-report-send followed by your message.",
"Grant or remove the username reward with #player-report-glow grant or #player-report-glow revoke.",
"Close this report with #player-report-close or move the issue to Done. Closed reports and issues are deleted after three days.",
`Report reference: ${report.id}`,
].join("\n");
try {