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(), /** Instances this user is online on. */ public instances = new Set(), /** 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(), ) { 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) { const instanceStats = new Map( (json.instance_stats ?? []).map(([id, stats]) => [id, PlayerStats.fromJSON(stats)]) ); return new this( json.name, new Set(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 = { 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) { 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 }