mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-11-11 15:54:52 +09:00
The plugin is now exp_discord_auth, so later Discord plugins can sit next to it. Config keys, permissions, routes and the Lua module moved with it, and the Discord redirect is now api/exp_discord_auth/callback. The controller registers its handlers and routes in the constructor and uses the express types, the instance plugin is a closure, and the Lua commands file returns its commands. The get and unlink responses carry the controller url so the in game messages can name the website. The login form asks the new status route whether Discord is configured and shows the error straight away instead of a button that fails. The button is full width like the link button, and a "Not you?" link next to the Discord name replaces the separate switch account button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
201 lines
5.1 KiB
TypeScript
201 lines
5.1 KiB
TypeScript
import * as lib from "@clusterio/lib";
|
|
import { Type, Static } from "@sinclair/typebox";
|
|
|
|
/*
|
|
Data records
|
|
*/
|
|
|
|
/** A Discord account and the Factorio accounts linked to it. */
|
|
export class DiscordAccountRecord {
|
|
constructor(
|
|
/** Discord user id */
|
|
public id: string,
|
|
/** Discord display name at the time of the last login */
|
|
public name: string,
|
|
public factorioNames: string[],
|
|
public updatedAtMs: number = 0,
|
|
public isDeleted: boolean = false,
|
|
) {}
|
|
|
|
static jsonSchema = Type.Object({
|
|
id: Type.String(),
|
|
name: Type.String(),
|
|
factorio_names: Type.Array(Type.String()),
|
|
updated_at_ms: Type.Optional(Type.Number()),
|
|
is_deleted: Type.Optional(Type.Boolean()),
|
|
});
|
|
|
|
toJSON() {
|
|
const json: Static<typeof DiscordAccountRecord.jsonSchema> = {
|
|
id: this.id,
|
|
name: this.name,
|
|
factorio_names: this.factorioNames,
|
|
};
|
|
|
|
if (this.updatedAtMs) {
|
|
json.updated_at_ms = this.updatedAtMs;
|
|
}
|
|
|
|
if (this.isDeleted) {
|
|
json.is_deleted = true;
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
return new this(
|
|
json.id,
|
|
json.name,
|
|
json.factorio_names,
|
|
json.updated_at_ms ?? 0,
|
|
json.is_deleted ?? false,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Users may always act on their own link, otherwise the permission is needed. */
|
|
function selfOrPermission(permission: lib.PermissionName) {
|
|
return (user: lib.IUser, message: lib.MessageRequest) => {
|
|
const playerName = (message.data as { player_name: string }).player_name;
|
|
if (user.name.toLowerCase() !== playerName.toLowerCase()) {
|
|
user.checkPermission(permission);
|
|
}
|
|
};
|
|
}
|
|
|
|
/** Only the user themselves, no permission lets someone link another user to their Discord. */
|
|
function selfOnly(user: lib.IUser, message: lib.MessageRequest) {
|
|
const playerName = (message.data as { player_name: string }).player_name;
|
|
if (user.name.toLowerCase() !== playerName.toLowerCase()) {
|
|
throw new lib.PermissionError("You can only link your own account");
|
|
}
|
|
}
|
|
|
|
const Link = Type.Union([
|
|
Type.Null(),
|
|
Type.Object({
|
|
id: Type.String(),
|
|
name: Type.String(),
|
|
}),
|
|
]);
|
|
const LinkResponse = lib.plainJson(Link);
|
|
/** The website is included so the player can be told where to link their account. */
|
|
const LinkStatusResponse = lib.plainJson(Type.Object({
|
|
link: Link,
|
|
url: Type.String(),
|
|
}));
|
|
|
|
/*
|
|
Link requests
|
|
*/
|
|
|
|
/** Link a player to the Discord account which generated the code, sent when the player uses the code in game. */
|
|
export class DiscordLinkRequest {
|
|
declare ["constructor"]: typeof DiscordLinkRequest;
|
|
static plugin = "exp_discord_auth" as const;
|
|
static type = "request" as const;
|
|
static src = "instance" as const;
|
|
static dst = "controller" as const;
|
|
static Response = LinkResponse;
|
|
|
|
constructor(
|
|
public playerName: string,
|
|
public code: string,
|
|
) {}
|
|
|
|
static jsonSchema = Type.Object({
|
|
player_name: Type.String(),
|
|
code: Type.String(),
|
|
});
|
|
|
|
toJSON() {
|
|
return { player_name: this.playerName, code: this.code };
|
|
}
|
|
|
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
return new this(json.player_name, json.code);
|
|
}
|
|
}
|
|
|
|
/** Remove the link of a player, responds with the account it was linked to and the website url. */
|
|
export class DiscordUnlinkRequest {
|
|
declare ["constructor"]: typeof DiscordUnlinkRequest;
|
|
static plugin = "exp_discord_auth" as const;
|
|
static type = "request" as const;
|
|
static src = ["control", "instance"] as const;
|
|
static dst = "controller" as const;
|
|
static permission = selfOrPermission("exp_discord_auth.link.delete");
|
|
static Response = LinkStatusResponse;
|
|
|
|
constructor(
|
|
public playerName: string,
|
|
) {}
|
|
|
|
static jsonSchema = Type.Object({
|
|
player_name: Type.String(),
|
|
});
|
|
|
|
toJSON() {
|
|
return { player_name: this.playerName };
|
|
}
|
|
|
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
return new this(json.player_name);
|
|
}
|
|
}
|
|
|
|
/** Get the Discord account a player is linked to and the website url. */
|
|
export class DiscordLinkGetRequest {
|
|
declare ["constructor"]: typeof DiscordLinkGetRequest;
|
|
static plugin = "exp_discord_auth" as const;
|
|
static type = "request" as const;
|
|
static src = ["control", "instance"] as const;
|
|
static dst = "controller" as const;
|
|
static permission = selfOrPermission("exp_discord_auth.link.get");
|
|
static Response = LinkStatusResponse;
|
|
|
|
constructor(
|
|
public playerName: string,
|
|
) {}
|
|
|
|
static jsonSchema = Type.Object({
|
|
player_name: Type.String(),
|
|
});
|
|
|
|
toJSON() {
|
|
return { player_name: this.playerName };
|
|
}
|
|
|
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
return new this(json.player_name);
|
|
}
|
|
}
|
|
|
|
/** Start linking the Discord account from the user page, responds with a ticket for the authorize route. */
|
|
export class DiscordLinkStartRequest {
|
|
declare ["constructor"]: typeof DiscordLinkStartRequest;
|
|
static plugin = "exp_discord_auth" as const;
|
|
static type = "request" as const;
|
|
static src = "control" as const;
|
|
static dst = "controller" as const;
|
|
static permission = selfOnly;
|
|
static Response = lib.plainJson(Type.String());
|
|
|
|
constructor(
|
|
public playerName: string,
|
|
) {}
|
|
|
|
static jsonSchema = Type.Object({
|
|
player_name: Type.String(),
|
|
});
|
|
|
|
toJSON() {
|
|
return { player_name: this.playerName };
|
|
}
|
|
|
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
return new this(json.player_name);
|
|
}
|
|
}
|