mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-11-15 00:24:52 +09:00
Players log in to the web interface with Discord through OAuth2 with the identify scope, so no bot token is needed. After logging in they get a code from the login page and type /link-discord <code> in game to link that Factorio account. One Discord account can hold up to exp_discord.max_accounts Factorio accounts (default 3), and linking a name moves it off any other Discord account. Sessions and link codes live in memory on the controller plugin, the links are stored in database/exp_discord/accounts.json. The user page shows the linked Discord account and lets the user, or anyone with exp_discord.link.delete, unlink it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
2.9 KiB
TypeScript
83 lines
2.9 KiB
TypeScript
import type { Instance, InstancePluginContext } from "@clusterio/host";
|
|
import type * as lib from "@clusterio/lib";
|
|
import * as messages from "./messages.js";
|
|
|
|
/** Sent by the lua side when a player uses a link code. */
|
|
export type IpcLink = {
|
|
player_name: string,
|
|
code: string,
|
|
};
|
|
|
|
/** Sent by the lua side when a player unlinks their Discord account, or asks which one they are linked to. */
|
|
export type IpcPlayer = {
|
|
player_name: string,
|
|
};
|
|
|
|
/** Bridges the lua side and the controller, the answer is printed to the player once it arrives. */
|
|
export class InstancePlugin {
|
|
private constructor(
|
|
public instance: Instance,
|
|
public logger: lib.Logger,
|
|
public name: string,
|
|
) {}
|
|
|
|
static async fromContext(context: InstancePluginContext) {
|
|
const instance = context.instance;
|
|
const plugin = new InstancePlugin(instance, context.logger, context.plugin.name);
|
|
|
|
instance.server.handle("exp_discord:link", plugin.handleLinkIPC.bind(plugin));
|
|
instance.server.handle("exp_discord:unlink", plugin.handleUnlinkIPC.bind(plugin));
|
|
instance.server.handle("exp_discord:get", plugin.handleGetIPC.bind(plugin));
|
|
return plugin;
|
|
}
|
|
|
|
async handleLinkIPC(event: IpcLink) {
|
|
try {
|
|
const discord = await this.instance.sendTo("controller", new messages.DiscordLinkRequest(
|
|
event.player_name, event.code,
|
|
));
|
|
await this.luaSend("receive_linked", { player_name: event.player_name, discord_name: discord!.name });
|
|
} catch (err: any) {
|
|
await this.luaSend("receive_error", { player_name: event.player_name, message: err.message });
|
|
}
|
|
}
|
|
|
|
async handleUnlinkIPC(event: IpcPlayer) {
|
|
try {
|
|
const discord = await this.instance.sendTo("controller", new messages.DiscordUnlinkRequest(event.player_name));
|
|
await this.luaSend("receive_unlinked", { player_name: event.player_name, discord_name: discord?.name });
|
|
} catch (err: any) {
|
|
await this.luaSend("receive_error", { player_name: event.player_name, message: err.message });
|
|
}
|
|
}
|
|
|
|
async handleGetIPC(event: IpcPlayer) {
|
|
try {
|
|
const discord = await this.instance.sendTo("controller", new messages.DiscordLinkGetRequest(event.player_name));
|
|
await this.luaSend("receive_get", { player_name: event.player_name, discord_name: discord?.name });
|
|
} catch (err: any) {
|
|
await this.luaSend("receive_error", { player_name: event.player_name, 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;
|
|
}
|
|
// Discord names can contain anything, so pick a long bracket level the json does not close
|
|
const text = JSON.stringify(json);
|
|
let level = "=";
|
|
while (text.includes(`]${level}]`)) {
|
|
level += "=";
|
|
}
|
|
await this.instance.sendRcon(
|
|
`/sc exp_discord.${receiver}(helpers.json_to_table[${level}[${text}]${level}])`, true
|
|
);
|
|
}
|
|
}
|
|
|
|
export default async function (context: InstancePluginContext) {
|
|
await InstancePlugin.fromContext(context);
|
|
}
|