Files
factorio-expcluster/exp_reports/instance.ts
T
bbassie bad98c5013 Add the exp_reports plugin and move the scenario onto it
Reports were the last legacy control module, kept in the lua state of
each instance. They are now an exp_reports plugin which stores them on
the controller: a report is immutable and holds the reported player, who
made it, the reason, the instance name and when it was made, and the only
operations are get, list, create and delete.

In game nothing is stored. The lua module sends each request over ipc,
the instance plugin asks the controller and hands the answer back with
rcon, and the module prints it to whoever asked if they are still online.
Answers are dropped if the instance stopped meanwhile. The module raises
on_player_reported with the count and reporters of every report against
the player, which report_jail and discord_alerts use, and the commands
and player list button call into it.

The web ui gets a /reports page: a table with search on the player,
reporter and reason, a filter on the instance, delete per row and a
create form which reports in the name of the web user.

New reports are posted to two optional webhooks configured on the
controller, a Discord channel hook as an embed and a generic JSON hook.
Failures are logged rather than failing the report.

Tests cover the messages, the controller against a real Controller, the
instance plugin against a real Instance, and the lua module through the
shared harness.
2026-09-06 16:44:23 +00:00

96 lines
3.2 KiB
TypeScript

import { BaseInstancePlugin } from "@clusterio/host";
import * as messages from "./messages";
/** Sent by the lua side when a player reports another. */
export type IpcReportCreate = {
player_name: string,
by_player_name: string,
reason: string,
};
/** Sent by the lua side to list the reports for a player, or for everyone. */
export type IpcReportList = {
caller: string,
player_name: string | undefined,
};
/** Sent by the lua side to delete the reports against a player, optionally only those from one player. */
export type IpcReportDelete = {
caller: string,
player_name: string,
by_player_name: string | undefined,
};
/**
* Bridges the lua side and the controller.
*
* Nothing is kept in the lua state, every request goes to the controller and
* the answer is handed back to lua once it arrives. The instance may have
* stopped by then, in which case the answer is dropped.
*/
export class InstancePlugin extends BaseInstancePlugin {
async init() {
this.instance.server.handle("exp_reports:create", this.handleCreateIPC.bind(this));
this.instance.server.handle("exp_reports:list", this.handleListIPC.bind(this));
this.instance.server.handle("exp_reports:delete", this.handleDeleteIPC.bind(this));
}
async handleCreateIPC(event: IpcReportCreate) {
try {
const report = await this.instance.sendTo("controller", new messages.ReportCreateRequest(
event.player_name, event.reason, event.by_player_name,
));
const reports = await this.instance.sendTo("controller", new messages.ReportListRequest(event.player_name));
await this.luaSend("receive_created", {
report: report.toJSON(),
reports: reports.map(other => other.toJSON()),
});
} catch (err: any) {
await this.luaSend("receive_error", { caller: event.by_player_name, message: err.message });
}
}
async handleListIPC(event: IpcReportList) {
try {
const reports = await this.instance.sendTo("controller", new messages.ReportListRequest(event.player_name));
await this.luaSend("receive_list", {
caller: event.caller,
player_name: event.player_name,
reports: reports.map(report => report.toJSON()),
});
} catch (err: any) {
await this.luaSend("receive_error", { caller: event.caller, message: err.message });
}
}
async handleDeleteIPC(event: IpcReportDelete) {
try {
let reports = await this.instance.sendTo("controller", new messages.ReportListRequest(event.player_name));
if (event.by_player_name !== undefined) {
reports = reports.filter(report => report.byPlayerName === event.by_player_name);
}
for (const report of reports) {
await this.instance.sendTo("controller", new messages.ReportDeleteRequest(report.id));
}
await this.luaSend("receive_deleted", {
caller: event.caller,
player_name: event.player_name,
by_player_name: event.by_player_name,
count: reports.length,
});
} catch (err: any) {
await this.luaSend("receive_error", { caller: event.caller, message: err.message });
}
}
/** Hand an answer to lua, unless the instance stopped while it was being fetched. */
async luaSend(receiver: string, json: any) {
if (this.instance.status !== "running") {
return;
}
await this.instance.sendRcon(
`/sc exp_reports.${receiver}(helpers.json_to_table[=[${JSON.stringify(json)}]=])`, true
);
}
}