Files
factorio-clusterio/packages/lib/src/data/UserDetails.ts
T
bbassieandClaude Opus 5.5 54e9340e61 Add UnavailableError and keep the PermissionError name
Host not connected, host shutting down and instance not running are
now UnavailableError (503) instead of ConflictError. The request error
migration notes move to a new 2.1 guide, since this ships in 2.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:03:13 +00:00

169 lines
4.8 KiB
TypeScript

import { Static, Type } from "@sinclair/typebox";
import type { PermissionName } from "../permissions.js";
import PlayerStats from "./PlayerStats.js";
/** Represents a user in the cluster */
export default class UserDetails {
public playerStats: PlayerStats;
constructor(
/** Factorio user name. */
public name: string,
/** Roles this user has */
public roleIds = new Set<number>(),
/** Instances this user is online on. */
public instances = new Set<number>(),
/** True if the user is promoted to admin on the Factorio instances. */
public isAdmin = false,
/** True if the user is banned from Factorio instances. */
public isBanned = false,
/** True if the user is whitelisted on the Factorio instances. */
public isWhitelisted = false,
/** Reason for being banned. Ignored if isBanned is false. */
public banReason = "",
/** Millisecond Unix timestamp this entry was last updated at */
public updatedAtMs = 0,
/** True if this user object has been removed from the cluster. */
public isDeleted = false,
/** Per instance statistics for the player this user account is tied to. */
public instanceStats = new Map<number, PlayerStats>(),
) {
this.playerStats = UserDetails._calculatePlayerStats(this.instanceStats);
}
/** Name of the user, used by event subscriptions and to ensure case insensitivity */
get id() {
return this.name.toLowerCase();
}
static jsonSchema = Type.Object({
name: Type.String(),
roles: Type.Optional(Type.Array(Type.Integer())),
instances: Type.Optional(Type.Array(Type.Integer())),
is_admin: Type.Optional(Type.Boolean()),
is_banned: Type.Optional(Type.Boolean()),
is_whitelisted: Type.Optional(Type.Boolean()),
ban_reason: Type.Optional(Type.String()),
updated_at_ms: Type.Optional(Type.Number()),
is_deleted: Type.Optional(Type.Boolean()),
instance_stats: Type.Optional(
Type.Array(
Type.Tuple([Type.Integer(), PlayerStats.jsonSchema]),
)
),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
const instanceStats = new Map(
(json.instance_stats ?? []).map(([id, stats]) => [id, PlayerStats.fromJSON(stats)])
);
return new this(
json.name,
new Set<number>(json.roles),
new Set(json.instances),
json.is_admin,
json.is_banned,
json.is_whitelisted,
json.ban_reason,
json.updated_at_ms,
json.is_deleted,
instanceStats,
);
}
toJSON() {
let json: Static<typeof UserDetails.jsonSchema> = {
name: this.name,
};
if (this.roleIds.size) {
json.roles = [...this.roleIds];
}
if (this.instances.size) {
json.instances = [...this.instances];
}
if (this.isAdmin) {
json.is_admin = true;
}
if (this.isWhitelisted) {
json.is_whitelisted = true;
}
if (this.isBanned) {
json.is_banned = true;
}
if (this.banReason) {
json.ban_reason = this.banReason;
}
if (this.updatedAtMs) {
json.updated_at_ms = this.updatedAtMs;
}
if (this.isDeleted) {
json.is_deleted = this.isDeleted;
}
if (this.instanceStats.size) {
json.instance_stats = [...this.instanceStats].map(([id, stats]) => [id, stats.toJSON()]);
}
return json;
}
recalculatePlayerStats() {
this.playerStats = UserDetails._calculatePlayerStats(this.instanceStats);
}
static _calculatePlayerStats(instanceStatsMap: Map<number, PlayerStats>) {
let playerStats = new PlayerStats();
for (let instanceStats of instanceStatsMap.values()) {
if (
instanceStats.firstJoinAt
&& (!playerStats.firstJoinAt || instanceStats.firstJoinAt < playerStats.firstJoinAt)
) {
playerStats.firstJoinAt = instanceStats.firstJoinAt;
}
if (
instanceStats.lastJoinAt
&& (!playerStats.lastJoinAt || instanceStats.lastJoinAt > playerStats.lastJoinAt)
) {
playerStats.lastJoinAt = instanceStats.lastJoinAt;
}
if (
instanceStats.lastLeaveAt
&& (!playerStats.lastLeaveAt || instanceStats.lastLeaveAt > playerStats.lastLeaveAt)
) {
playerStats.lastLeaveAt = instanceStats.lastLeaveAt;
playerStats.lastLeaveReason = instanceStats.lastLeaveReason;
}
playerStats.joinCount += instanceStats.joinCount;
playerStats.onlineTimeMs += instanceStats.onlineTimeMs;
}
return playerStats;
}
}
/**
* This interface is implemented by User on the controller.
* It is exposed here to allow message definitions to create permission methods.
*/
export interface IUser extends UserDetails {
/**
* Check if a given permission is granted
*
* Checks the roles the user is member of for one that grants the given
* permission. If the permission is not granted for the user a
* "Permission denied" error is thrown.
*
* @param permission - The permission to check for.
* @throws {Error} If the given permission does not exist.
* @throws {libErrors.PermissionError} if the user does noh have the given permission.
*/
checkPermission(permission: PermissionName): void
}