diff --git a/exp_groups/controller.ts b/exp_groups/controller.ts deleted file mode 100644 index dde5515c..00000000 --- a/exp_groups/controller.ts +++ /dev/null @@ -1,282 +0,0 @@ -import * as lib from "@clusterio/lib"; -import { BaseControllerPlugin, InstanceInfo } from "@clusterio/controller"; - -import { - PermissionStrings, PermissionStringsUpdate, - PermissionGroup, PermissionGroupUpdate, - InstancePermissionGroups, - PermissionInstanceId, - PermissionGroupEditEvent, -} from "./messages"; - -import path from "path"; -import fs from "fs-extra"; - -export class ControllerPlugin extends BaseControllerPlugin { - static permissionGroupsPath = "exp_groups.json"; - static userGroupsPath = "exp_user_groups.json"; - - userToGroup: Map = new Map(); // TODO this needs to be per instance - permissionStrings!: Map; - permissionGroups!: Map; - - async init() { - this.controller.handle(PermissionStringsUpdate, this.handlePermissionStringsUpdate.bind(this)); - this.controller.handle(PermissionGroupUpdate, this.handlePermissionGroupUpdate.bind(this)); - this.controller.handle(PermissionGroupEditEvent, this.handlePermissionGroupEditEvent.bind(this)); - this.controller.subscriptions.handle(PermissionStringsUpdate, this.handlePermissionStringsSubscription.bind(this)); - this.controller.subscriptions.handle(PermissionGroupUpdate, this.handlePermissionGroupSubscription.bind(this)); - this.controller.subscriptions.handle(PermissionGroupEditEvent); - this.permissionStrings = new Map([["Global", new PermissionStrings("Global", new Set())]]); - this.permissionGroups = new Map([["Global", new InstancePermissionGroups("Global")]]); - await this.loadData(); - - // Add the default group if missing and add any missing cluster roles - const clusterRoles = [...this.controller.userManager.roles.values()] - for (const instanceGroups of this.permissionGroups.values()) { - const groups = instanceGroups.groups; - const instanceRoles = [...groups.values()].flatMap(group => [...group.roleIds.values()]); - const missingRoles = clusterRoles.filter(role => instanceRoles.includes(role.id)); - const defaultGroup = groups.get("Default"); - if (defaultGroup) { - for (const role of missingRoles) { - defaultGroup.roleIds.add(role.id) - } - } else { - groups.set("Default", new PermissionGroup( - instanceGroups.instanceId, - "Default", - groups.size, - new Set(missingRoles.map(role => role.id)) - )); - } - } - } - - async onControllerConfigFieldChanged(field: string, curr: unknown, prev: unknown) { - if (field === "exp_groups.allow_role_inconsistency") { - // Do something with this.userToGroup - } - } - - async onInstanceConfigFieldChanged(instance: InstanceInfo, field: string, curr: unknown, prev: unknown) { - this.logger.info(`controller::onInstanceConfigFieldChanged ${instance.id} ${field}`); - if (field === "exp_groups.sync_permission_groups") { - const updates = [] - const now = Date.now(); - if (curr) { - // Global sync enabled, we dont need the instance config - const instanceGroups = this.permissionGroups.get(instance.id); - if (instanceGroups) { - this.permissionGroups.delete(instance.id); - for (const group of instanceGroups.groups.values()) { - group.updatedAtMs = now; - group.isDeleted = true; - updates.push(group); - } - } - } else { - // Global sync disabled, make a copy of the global config as a base - const global = this.permissionGroups.get("Global")!; - const oldInstanceGroups = this.permissionGroups.get(instance.id); - const instanceGroups = new InstancePermissionGroups( - instance.id, new Map([...global.groups.values()].map(group => [group.name, group.copy(instance.id)])) - ) - this.permissionGroups.set(instance.id, instanceGroups); - for (const group of instanceGroups.groups.values()) { - group.updatedAtMs = now; - updates.push(group); - } - // If it has an old config (unexpected) then deal with it - if (oldInstanceGroups) { - for (const group of oldInstanceGroups.groups.values()) { - if (!instanceGroups.groups.has(group.name)) { - group.updatedAtMs = now; - group.isDeleted = true; - updates.push(group); - } - } - } - } - // Send the updates to all instances and controls - if (updates.length) { - this.controller.subscriptions.broadcast(new PermissionGroupUpdate(updates)); - } - } - } - - async loadPermissionGroups() { - const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.permissionGroupsPath); - this.logger.verbose(`Loading ${file}`); - try { - const content = await fs.readFile(file, { encoding: "utf8" }); - for (const groupRaw of JSON.parse(content)) { - const group = PermissionGroup.fromJSON(groupRaw); - const instanceGroups = this.permissionGroups.get(group.instanceId); - if (instanceGroups) { - instanceGroups.groups.set(group.name, group); - } else { - this.permissionGroups.set(group.instanceId, - new InstancePermissionGroups(group.instanceId, new Map([[group.name, group]])) - ); - } - }; - - } catch (err: any) { - if (err.code === "ENOENT") { - this.logger.verbose("Creating new permission group database"); - return; - } - throw err; - } - } - - async savePermissionGroups() { - const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.permissionGroupsPath); - this.logger.verbose(`Writing ${file}`); - await lib.safeOutputFile(file, JSON.stringify( - [...this.permissionGroups.values()].flatMap(instanceGroups => [...instanceGroups.groups.values()]) - )); - } - - async loadUserGroups() { - if (!this.controller.config.get("exp_groups.allow_role_inconsistency")) return; - const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.userGroupsPath); - this.logger.verbose(`Loading ${file}`); - try { - const content = await fs.readFile(file, { encoding: "utf8" }); - this.userToGroup = new Map(JSON.parse(content)); - - } catch (err: any) { - if (err.code === "ENOENT") { - this.logger.verbose("Creating new user group database"); - return; - } - throw err; - } - } - - async saveUserGroups() { - if (!this.controller.config.get("exp_groups.allow_role_inconsistency")) return; - const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.userGroupsPath); - this.logger.verbose(`Writing ${file}`); - await lib.safeOutputFile(file, JSON.stringify([...this.permissionGroups.entries()])); - } - - async loadData() { - await Promise.all([ - this.loadPermissionGroups(), - this.loadUserGroups(), - ]) - } - - async onSaveData() { - await Promise.all([ - this.savePermissionGroups(), - this.saveUserGroups(), - ]) - } - - addPermisisonGroup(instanceId: PermissionInstanceId, name: string, permissions = new Set(), silent = false) { - const instanceGroups = this.permissionGroups.get(instanceId); - if (!instanceGroups) { - throw new Error("Instance ID does not exist"); - } - if (instanceGroups.groups.has(name)) { - return instanceGroups.groups.get(name)!; - } - for (const group of instanceGroups.groups.values()) { - group.order += 1; - } - const group = new PermissionGroup(instanceId, name, 0, new Set(), permissions, Date.now(), false); - instanceGroups.groups.set(group.id, group); - if (!silent) { - this.controller.subscriptions.broadcast(new PermissionGroupUpdate([group])); - } - return group; - } - - removePermissionGroup(instanceId: PermissionInstanceId, name: string, silent = false) { - const instanceGroups = this.permissionGroups.get(instanceId); - if (!instanceGroups) { - throw new Error("Instance ID does not exist"); - } - const group = instanceGroups.groups.get(name) - if (!group) { - return null; - } - for (const nextGroup of instanceGroups.groups.values()) { - if (nextGroup.order > group.order) { - nextGroup.order -= 1; - } - } - instanceGroups.groups.delete(group.id); - group.updatedAtMs = Date.now(); - group.isDeleted = true; - if (!silent) { - this.controller.subscriptions.broadcast(new PermissionGroupUpdate([group])); - } - return group; - } - - async handlePermissionGroupEditEvent(event: PermissionGroupEditEvent) { - // TODO - } - - async handlePermissionStringsUpdate(event: PermissionStringsUpdate) { - for (const update of event.updates) { - const global = this.permissionStrings.get("Global")! - this.permissionStrings.set(update.instanceId as number, update) - global.updatedAtMs = Math.max(global.updatedAtMs, update.updatedAtMs) - for (const permission of update.permissions) { - global.permissions.add(permission) - } - // TODO maybe check if changes have happened rather than always pushing updates - this.controller.subscriptions.broadcast(new PermissionStringsUpdate([global, update])) - } - } - - async handlePermissionGroupUpdate(event: PermissionGroupUpdate) { - const updates = []; - for (const group of event.updates) { - const groups = this.permissionGroups.get(group.instanceId); - if (!groups) continue; - const existingGroup = groups.groups.get(group.id); - let update - if (!existingGroup) { - update = this.addPermisisonGroup(group.instanceId, group.name, group.permissions, true); - } else if (group.isDeleted) { - update = this.removePermissionGroup(group.instanceId, group.name, true); - } else { - existingGroup.permissions = group.permissions; - existingGroup.updatedAtMs = Date.now(); - update = existingGroup; - } - if (update) updates.push(update); - } - this.controller.subscriptions.broadcast(new PermissionGroupUpdate(updates)); - } - - async handlePermissionStringsSubscription(request: lib.SubscriptionRequest, src: lib.Address) { - const updates = [ ...this.permissionStrings.values() ] - .filter( - value => value.updatedAtMs > request.lastRequestTimeMs, - ) - return updates.length ? new PermissionStringsUpdate(updates) : null; - } - - async handlePermissionGroupSubscription(request: lib.SubscriptionRequest, src: lib.Address) { - const updates = [ ...this.permissionGroups.values() ] - .flatMap(instanceGroups => [...instanceGroups.groups.values()]) - .filter( - value => value.updatedAtMs > request.lastRequestTimeMs, - ) - if (src.type === lib.Address.instance) { - const instanceUpdates = updates.filter(group => group.instanceId === src.id || group.instanceId === "Global"); - this.logger.info(JSON.stringify(updates)) - this.logger.info(JSON.stringify(instanceUpdates)) - return instanceUpdates.length ? new PermissionGroupUpdate(instanceUpdates) : null; - } - return updates.length ? new PermissionGroupUpdate(updates) : null; - } -} diff --git a/exp_groups/index.ts b/exp_groups/index.ts deleted file mode 100644 index f5e272b3..00000000 --- a/exp_groups/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -import * as lib from "@clusterio/lib"; -import * as Messages from "./messages"; - -lib.definePermission({ - name: "exp_groups.create_delete_groups", - title: "Create and delete permission groups", - description: "Create and delete permission groups.", -}); - -lib.definePermission({ - name: "exp_groups.reorder_groups", - title: "Reorder permission groups", - description: "Reorder groups and link them to user roles.", -}); - -lib.definePermission({ - name: "exp_groups.modify_permissions", - title: "Modify permission groups", - description: "Modify game permissions for groups.", -}); - -lib.definePermission({ - name: "exp_groups.assign_players", - title: "Change player group", - description: "Change the permission group of a player", -}); - -lib.definePermission({ - name: "exp_groups.list", - title: "View permission groups", - description: "View permission groups.", -}); - -lib.definePermission({ - name: "exp_groups.list.subscribe", - title: "Subscribe to permission group updates", - description: "Subscribe to permission group updates.", -}); - -declare module "@clusterio/lib" { - export interface ControllerConfigFields { - "exp_groups.allow_role_inconsistency": boolean; - } - export interface InstanceConfigFields { - "exp_groups.sync_permission_groups": boolean; - } -} - -export const plugin: lib.PluginDeclaration = { - name: "exp_groups", - title: "exp_groups", - description: "Create, modify, and link factorio permission groups to clusterio user roles.", - - controllerEntrypoint: "./dist/node/controller", - controllerConfigFields: { - "exp_groups.allow_role_inconsistency": { - title: "Allow User Role Inconsistency", - description: "When true, users can be assgined to any group regardless of their roles", - type: "boolean", - initialValue: false, - }, - }, - - instanceEntrypoint: "./dist/node/instance", - instanceConfigFields: { - "exp_groups.sync_permission_groups": { - title: "Sync Permission Groups", - description: "When true, the instance cannot deviate from the global group settings and will be hidden from the sellection dropdown.", - type: "boolean", - initialValue: true, - }, - }, - - messages: [ - Messages.PermissionGroupEditEvent, - Messages.PermissionStringsUpdate, - Messages.PermissionGroupUpdate, - ], - - webEntrypoint: "./web", - routes: [ - "/exp_groups", - ], -}; diff --git a/exp_groups/instance.ts b/exp_groups/instance.ts deleted file mode 100644 index 5142d933..00000000 --- a/exp_groups/instance.ts +++ /dev/null @@ -1,129 +0,0 @@ -import * as lib from "@clusterio/lib"; -import { BaseInstancePlugin } from "@clusterio/host"; -import { - PermissionGroup, PermissionGroupEditEvent, PermissionGroupEditType, - PermissionGroupUpdate, PermissionInstanceId, PermissionStrings, PermissionStringsUpdate -} from "./messages"; - -const rconBase = "/sc local Groups = package.loaded['modules/exp_groups/module_exports'];" - -type EditIPC = { - type: PermissionGroupEditType, - changes: string[], - group: string, -}; - -type CreateIPC = { - group: string, - defiantion: [boolean, string[] | {}] -} - -type DeleteIPC = { - group: string, -} - -export class InstancePlugin extends BaseInstancePlugin { - permissions: Set = new Set(); - permissionGroups = new lib.EventSubscriber(PermissionGroupUpdate, this.instance); - permissionGroupUpdates = new lib.EventSubscriber(PermissionGroupEditEvent, this.instance); - syncId: PermissionInstanceId = this.instance.config.get("exp_groups.sync_permission_groups") ? "Global" : this.instance.id; - - async init() { - this.instance.server.handle("exp_groups-permission_group_edit", this.handleEditIPC.bind(this)); - this.instance.server.handle("exp_groups-permission_group_create", this.handleCreateIPC.bind(this)); - this.instance.server.handle("exp_groups-permission_group_delete", this.handleDeleteIPC.bind(this)); - } - - async onStart() { - // Send the most recent version of the permission string - const permissionsString = await this.sendRcon(rconBase + "rcon.print(Groups.get_actions_json())"); - this.permissions = new Set(JSON.parse(permissionsString)); - this.instance.sendTo("controller", new PermissionStringsUpdate([ - new PermissionStrings(this.instance.id, this.permissions, Date.now()) - ])); - - // Subscribe to get updates for permission groups - this.permissionGroups.subscribe(this.onPermissionGroupsUpdate.bind(this)); - this.permissionGroupUpdates.subscribe(this.onPermissionGroupUpdate.bind(this)); - } - - async onControllerConnectionEvent(event: any) { - this.permissionGroups.handleConnectionEvent(event); - } - - async onInstanceConfigFieldChanged(field: string, curr: unknown, prev: unknown) { - if (field === "exp_groups.sync_permission_groups") { - this.syncId = curr ? "Global" : this.instance.id; - const [snapshot, synced] = this.permissionGroups.getSnapshot(); - if (synced && this.instance.status !== "running") await this.syncPermissionGroups(snapshot.values()); - } - } - - async onPermissionGroupsUpdate(event: PermissionGroupUpdate | null, synced: boolean) { - if (!synced || this.instance.status !== "running" || !event?.updates.length) return; - await this.syncPermissionGroups(event.updates); - } - - async syncPermissionGroups(groups: Iterable) { - const updateCommands = [rconBase]; - for (const group of groups) { - if (group.instanceId === this.syncId && group.updatedAtMs > (this.permissionGroups.values.get(group.id)?.updatedAtMs ?? 0)) { - if (group.isDeleted) { - updateCommands.push(`Groups.destroy_group('${group.name}')`); - } else if (group.permissions.size < this.permissions.size / 2) { - updateCommands.push(`Groups.get_or_create('${group.name}'):from_json('${JSON.stringify([false, [...this.permissions.values()]])}')`); - } else { - const inverted = [...this.permissions.values()].filter(permission => !group.permissions.has(permission)); - updateCommands.push(`Groups.get_or_create('${group.name}'):from_json('${JSON.stringify([true, inverted])}')`); - } - } - } - await this.sendRcon(updateCommands.join(";"), true); - } - - async onPermissionGroupUpdate(event: PermissionGroupEditEvent | null, synced: boolean) { - if (!synced || this.instance.status !== "running" || !event) return; - if (event.src.equals(lib.Address.fromShorthand({ instanceId: this.instance.id }))) return; - const getCmd = `Groups.get_or_create('${event.group}')`; - if (event.type === "add_permissions") { - await this.sendRcon(rconBase + getCmd + `:allow_actions(Groups.json_to_actions('${JSON.stringify(event.changes)}'))`); - } else if (event.type === "remove_permissions") { - await this.sendRcon(rconBase + getCmd + `:disallow_actions(Groups.json_to_actions('${JSON.stringify(event.changes)}'))`); - } else if (event.type === "assign_players") { - await this.sendRcon(rconBase + getCmd + `:add_players(game.json_to_table('${JSON.stringify(event.changes)}'))`); - } - } - - async handleEditIPC(event: EditIPC) { - this.logger.info(JSON.stringify(event)) - this.instance.sendTo("controller", new PermissionGroupEditEvent( - lib.Address.fromShorthand({ instanceId: this.instance.id }), - event.type, event.group, event.changes - )) - } - - async handleCreateIPC(event: CreateIPC) { - this.logger.info(JSON.stringify(event)) - if (!this.permissionGroups.synced) return; - let [defaultAllow, permissionsRaw] = event.defiantion; - if (!Array.isArray(permissionsRaw)) { - permissionsRaw = [] // lua outputs {} for empty arrays - } - const permissions = [...this.permissions.values()] - .filter(permission => defaultAllow !== (permissionsRaw as String[]).includes(permission)); - this.instance.sendTo("controller", new PermissionGroupUpdate([ new PermissionGroup( - this.syncId, event.group, 0, new Set(), new Set(permissions) - ) ])); - } - - async handleDeleteIPC(event: DeleteIPC) { - if (!this.permissionGroups.synced) return; - const group = [...this.permissionGroups.values.values()] - .find(group => group.instanceId === this.syncId && group.name === event.group); - if (group) { - group.updatedAtMs = Date.now(); - group.isDeleted = true; - this.instance.sendTo("controller", new PermissionGroupUpdate([ group ])); - } - } -} diff --git a/exp_groups/messages.ts b/exp_groups/messages.ts deleted file mode 100644 index 9b7337a8..00000000 --- a/exp_groups/messages.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { User, InstanceDetails, IControllerUser, Link, MessageRequest, StringEnum, PermissionError, Address } from "@clusterio/lib"; -import { Type, Static } from "@sinclair/typebox"; - -export const PermissionInstanceIdSchema = Type.Union([InstanceDetails.jsonSchema.properties.id, Type.Literal("Global")]) -export type PermissionInstanceId = InstanceDetails["id"] | "Global" -export type GamePermission = string; // todo: maybe enum this? - -/** - * Data class for permission groups - */ -export class PermissionGroup { - constructor( - public instanceId: PermissionInstanceId, - public name: string, - /** A lower order assumes a lower permission group */ - public order: number = 0, - /** A role will use the highest order group it is apart of */ - public roleIds: User["roleIds"] = new Set(), - public permissions: Set = new Set(), - public updatedAtMs: number = 0, - public isDeleted: boolean = false, - ) { - } - - static jsonSchema = Type.Object({ - instanceId: PermissionInstanceIdSchema, - name: Type.String(), - order: Type.Number(), - roleIds: Type.Array(Type.Number()), - permissions: Type.Array(Type.String()), - updatedAtMs: Type.Optional(Type.Number()), - isDeleted: Type.Optional(Type.Boolean()), - }); - - static fromJSON(json: Static) { - return new this( - json.instanceId, - json.name, - json.order, - new Set(json.roleIds), - new Set(json.permissions), - json.updatedAtMs, - json.isDeleted - ); - } - - toJSON(): Static { - return { - instanceId: this.instanceId, - name: this.name, - order: this.order, - roleIds: [...this.roleIds.values()], - permissions: [...this.permissions.values()], - updatedAtMs: this.updatedAtMs > 0 ? this.updatedAtMs : undefined, - isDeleted: this.isDeleted ? this.isDeleted : undefined, - } - } - - get id() { - return `${this.instanceId}:${this.name}`; - } - - copy(newInstanceId: PermissionInstanceId) { - return new PermissionGroup( - newInstanceId, - this.name, - this.order, - new Set(this.roleIds), - new Set(this.permissions), - Date.now(), - false - ) - } -} - -export class InstancePermissionGroups { - constructor( - public instanceId: PermissionInstanceId, - public groups: Map = new Map(), - ) { - } - - static jsonSchema = Type.Object({ - instanceId: PermissionInstanceIdSchema, - permissionsGroups: Type.Array(PermissionGroup.jsonSchema), - }); - - static fromJSON(json: Static) { - return new InstancePermissionGroups( - json.instanceId, - new Map(json.permissionsGroups.map(group => [group.name, PermissionGroup.fromJSON(group)])), - ); - } - - toJSON() { - return { - instanceId: this.instanceId, - permissionsGroups: [...this.groups.values()], - } - } - - getUserGroup(user: User) { - const groups = [...user.roleIds.values()].map(roleId => - // There will always be one and only one group for each role - [...this.groups.values()].find(group => group.roleIds.has(roleId))! - ); - return groups.reduce((highest, group) => highest.order > group.order ? highest : group); - } - - get id() { - return this.instanceId; - } -} - -export class PermissionGroupUpdate { - declare ["constructor"]: typeof PermissionGroupUpdate; - static type = "event" as const; - static src = ["controller", "instance"] as const; - static dst = ["control", "instance", "controller"] as const; - static plugin = "exp_groups" as const; - static permission = "exp_groups.list.subscribe"; - - constructor( - public updates: PermissionGroup[], - ) { } - - static jsonSchema = Type.Object({ - "updates": Type.Array(PermissionGroup.jsonSchema), - }); - - static fromJSON(json: Static) { - return new this( - json.updates.map(update => PermissionGroup.fromJSON(update)) - ); - } -} - -export type PermissionGroupEditType = "assign_players" | "add_permissions" | "remove_permissions"; - -export class PermissionGroupEditEvent { - declare ["constructor"]: typeof PermissionGroupEditEvent; - static type = "event" as const; - static src = ["instance", "controller"] as const; - static dst = ["control", "instance", "controller"] as const; - static plugin = "exp_groups" as const; - - static permission(user: IControllerUser, message: MessageRequest) { - if (typeof message.data === "object" && message.data !== null) { - const data = message.data as Static; - if (data.type === "add_permissions" || data.type === "remove_permissions") { - user.checkPermission("exp_groups.modify_permissions") - } else if (data.type === "assign_players") { - user.checkPermission("exp_groups.assign_players") - } else { - throw new PermissionError("Permission denied"); - } - }; - } - - constructor( - public src: Address, - public type: PermissionGroupEditType, - public group: string, - public changes: String[], - ) { } - - static jsonSchema = Type.Object({ - "src": Address.jsonSchema, - "type": StringEnum(["assign_players", "add_permissions", "remove_permissions"]), - "group": Type.String(), - "changes": Type.Array(Type.String()), - }); - - static fromJSON(json: Static) { - return new this(Address.fromJSON(json.src), json.type, json.group, json.changes); - } -} - -export class PermissionStrings { - constructor( - public instanceId: PermissionInstanceId, - public permissions: Set, - public updatedAtMs: number = 0, - public isDeleted: boolean = false, - ) { - } - - static jsonSchema = Type.Object({ - instanceId: PermissionInstanceIdSchema, - permissions: Type.Array(Type.String()), - updatedAtMs: Type.Optional(Type.Number()), - isDeleted: Type.Optional(Type.Boolean()), - }); - - static fromJSON(json: Static) { - return new PermissionStrings( - json.instanceId, - new Set(json.permissions), - json.updatedAtMs, - json.isDeleted - ); - } - - toJSON() { - return { - instanceId: this.instanceId, - permissions: [...this.permissions.values()], - updatedAtMs: this.updatedAtMs > 0 ? this.updatedAtMs : undefined, - isDeleted: this.isDeleted ? this.isDeleted : undefined, - } - } - - get id() { - return this.instanceId - } -} - -export class PermissionStringsUpdate { - declare ["constructor"]: typeof PermissionStringsUpdate; - static type = "event" as const; - static src = ["instance", "controller"] as const; - static dst = ["controller", "control"] as const; - static plugin = "exp_groups" as const; - static permission = "exp_groups.list.subscribe"; - - constructor( - public updates: PermissionStrings[], - ) { } - - static jsonSchema = Type.Object({ - "updates": Type.Array(PermissionStrings.jsonSchema), - }); - - static fromJSON(json: Static) { - return new this( - json.updates.map(update => PermissionStrings.fromJSON(update)) - ); - } -} diff --git a/exp_groups/module/control.lua b/exp_groups/module/control.lua deleted file mode 100644 index af956afc..00000000 --- a/exp_groups/module/control.lua +++ /dev/null @@ -1,127 +0,0 @@ -local clusterio_api = require("modules/clusterio/api") -local Global = require("modules/exp_util/global") -local Groups = require("modules/exp_groups") - -local pending_updates = {} -Global.register(pending_updates, function(tbl) - pending_updates = tbl -end) - -local function on_permission_group_added(event) - if not event.player_index then return end - pending_updates[event.group.name] = { - created = true, - sync_all = true, - tick = event.tick, - permissions = {}, - players = {}, - } -end - -local function on_permission_group_deleted(event) - if not event.player_index then return end - local existing = pending_updates[event.group_name] - pending_updates[event.group_name] = nil - if not existing or not existing.created then - clusterio_api.send_json("exp_groups-permission_group_delete", { - group = event.group_name, - }) - end -end - -local function on_permission_group_edited(event) - if not event.player_index then return end - local pending = pending_updates[event.group.name] - if not pending then - pending = { - tick = event.tick, - permissions = {}, - players = {}, - } - pending_updates[event.group.name] = pending - end - pending.tick = event.tick - - if event.type == "add-permission" then - if not pending.sync_all then - pending.permissions[event.action] = true - end - elseif event.type == "remove-permission" then - if not pending.sync_all then - pending.permissions[event.action] = false - end - elseif event.type == "enable-all" then - pending.sync_all = true - elseif event.type == "disable-all" then - pending.sync_all = true - elseif event.type == "add-player" then - local player = game.get_player(event.other_player_index) --- @cast player -nil - pending.players[player.name] = true - elseif event.type == "remove-player" then - local player = game.get_player(event.other_player_index) --- @cast player -nil - pending.players[player.name] = nil - elseif event.type == "rename" then - pending.created = true - pending.sync_all = true - local old = pending_updates[event.old_name] - if old then pending.players = old.players end - on_permission_group_deleted{ - tick = event.tick, player_index = event.player_index, group_name = event.old_name, - } - end -end - -local function send_updates() - local tick = game.tick - 600 -- 10 Seconds - local done = {} - for group_name, pending in pairs(pending_updates) do - if pending.tick < tick then - done[group_name] = true - if pending.sync_all then - clusterio_api.send_json("exp_groups-permission_group_create", { - group = group_name, defiantion = Groups.get_group(group_name):to_json(true), - }) - else - if next(pending.players) then - clusterio_api.send_json("exp_groups-permission_group_edit", { - type = "assign_players", group = group_name, changes = table.get_keys(pending.players), - }) - end - local add, remove = {}, {} - for permission, state in pairs(pending.permissions) do - if state then - add[#add + 1] = permission - else - remove[#remove + 1] = permission - end - end - - if next(add) then - clusterio_api.send_json("exp_groups-permission_group_edit", { - type = "add_permissions", group = group_name, changes = Groups.actions_to_names(add), - }) - end - if next(remove) then - clusterio_api.send_json("exp_groups-permission_group_edit", { - type = "remove_permissions", group = group_name, changes = Groups.actions_to_names(remove), - }) - end - end - end - end - - for group_name in pairs(done) do - pending_updates[group_name] = nil - end -end - -return { - events = { - [defines.events.on_permission_group_added] = on_permission_group_added, - [defines.events.on_permission_group_deleted] = on_permission_group_deleted, - [defines.events.on_permission_group_edited] = on_permission_group_edited, - }, - on_nth_tick = { - [300] = send_updates, - }, -} diff --git a/exp_groups/module/module.json b/exp_groups/module/module.json deleted file mode 100644 index 391b1a1d..00000000 --- a/exp_groups/module/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "exp_groups", - "load": [ - "control.lua" - ], - "require": [ - ], - "dependencies": { - "clusterio": "*", - "exp_util": "*" - } -} diff --git a/exp_groups/module/module_exports.lua b/exp_groups/module/module_exports.lua deleted file mode 100644 index 6f84f4da..00000000 --- a/exp_groups/module/module_exports.lua +++ /dev/null @@ -1,305 +0,0 @@ -local Async = require("modules/exp_util/async") - -local table_to_json = helpers.table_to_json -local json_to_table = helpers.json_to_table - ---- Top level module table, contains event handlers and public methods -local Groups = {} - ---- @class ExpGroup : LuaPermissionGroup ---- @field group LuaPermissionGroup The permission group for this group proxy -Groups._prototype = {} - -Groups._metatable = { - __index = setmetatable(Groups._prototype, { - --- @type any Annotation required because otherwise it is typed as 'table' - __index = function(self, key) - return self.group[key] - end, - }), - __class = "ExpGroup", -} - -local action_to_name = {} -for name, action in pairs(defines.input_action) do - action_to_name[action] = name -end - ---- Async Functions --- These are required to allow bypassing edit_permission_group - ---- Add a player to a permission group, requires edit_permission_group ---- @param player LuaPlayer Player to add to the group ---- @param group LuaPermissionGroup Group to add the player to ---- @return boolean # True if successful -local function add_player_to_group(player, group) - return group.add_player(player) -end - ---- Add a players to a permission group, requires edit_permission_group ---- @param players LuaPlayer[] Players to add to the group ---- @param group LuaPermissionGroup Group to add the players to ---- @return boolean # True if successful -local function add_players_to_group(players, group) - local add_player = group.add_player - if not add_player(players[1]) then - return false - end - for i = 2, #players do - add_player(players[i]) - end - - return true -end - --- Async will bypass edit_permission_group but takes at least one tick -local add_player_to_group_async = Async.register(add_player_to_group) -local add_players_to_group_async = Async.register(add_players_to_group) - ---- Static methods for gettings, creating and removing permission groups - ---- Gets the permission group proxy with the given name or group ID. ---- @param group_name string|uint32 The name or id of the permission group ---- @return ExpGroup? -function Groups.get_group(group_name) - local group = game.permissions.get_group(group_name) - if group == nil then return nil end - return setmetatable({ - group = group, - }, Groups._metatable) -end - ---- Gets the permission group proxy for a players group ---- @param player LuaPlayer The player to get the group of ---- @return ExpGroup? -function Groups.get_player_group(player) - local group = player.permission_group - if group == nil then return nil end - return setmetatable({ - group = group, - }, Groups._metatable) -end - ---- Creates a new permission group, requires add_permission_group ---- @param group_name string Name of the group to create ---- @return ExpGroup -function Groups.new_group(group_name) - local group = game.permissions.get_group(group_name) - assert(group == nil, "Group already exists with name: " .. group_name) - group = game.permissions.create_group(group_name) - assert(group ~= nil, "Requires permission add_permission_group") - return setmetatable({ - group = group, - }, Groups._metatable) -end - ---- Get or create a permisison group, must use the group name not the group id ---- @param group_name string Name of the group to create ---- @return ExpGroup -function Groups.get_or_create(group_name) - local group = game.permissions.get_group(group_name) - if group then - return setmetatable({ - group = group, - }, Groups._metatable) - else - group = game.permissions.create_group(group_name) - assert(group ~= nil, "Requires permission add_permission_group") - return setmetatable({ - group = group, - }, Groups._metatable) - end -end - ---- Destory a permission group, moves all players to default group ---- @param group_name string|uint32 The name or id of the permission group to destroy ---- @param move_to_name string|uint32? The name or id of the permission group to move players to -function Groups.destroy_group(group_name, move_to_name) - local group = game.permissions.get_group(group_name) - if group == nil then return end - - local players = group.players - if #players > 0 then - local move_to = game.permissions.get_group(move_to_name or "Default") - for _, player in ipairs(players) do - player.permission_group = move_to - end - end - - local success = group.destroy() - assert(success, "Requires permission delete_permission_group") -end - ---- Prototype methods for modifying and working with permission groups - ---- Add a player to the permission group ---- @param player LuaPlayer The player to add to the group -function Groups._prototype:add_player(player) - if not add_player_to_group(player, self.group) then - add_player_to_group_async(player, self.group) - end -end - ---- Add players to the permission group ---- @param players LuaPlayer[] The player to add to the group -function Groups._prototype:add_players(players) - if not add_players_to_group(players, self.group) then - add_players_to_group_async(players, self.group) - end -end - ---- Move all players to another group ---- @param other_group ExpGroup The group to move players to, default is the Default group -function Groups._prototype:move_players(other_group) - if not add_players_to_group(self.group.players, other_group.group) then - add_players_to_group_async(self.group.players, other_group.group) - end -end - ---- Allow a set of actions for this group ---- @param actions defines.input_action[] Actions to allow ---- @return ExpGroup -function Groups._prototype:allow_actions(actions) - local set_allow = self.group.set_allows_action - for _, action in ipairs(actions) do - set_allow(action, true) - end - - return self -end - ---- Disallow a set of actions for this group ---- @param actions defines.input_action[] Actions to disallow ---- @return ExpGroup -function Groups._prototype:disallow_actions(actions) - local set_allow = self.group.set_allows_action - for _, action in ipairs(actions) do - set_allow(action, false) - end - - return self -end - ---- Reset the allowed state of all actions ---- @param allowed boolean? default true for allow all actions, false to disallow all actions ---- @return ExpGroup -function Groups._prototype:reset(allowed) - local set_allow = self.group.set_allows_action - if allowed == nil then allowed = true end - for _, action in pairs(defines.input_action) do - set_allow(action, allowed) - end - - return self -end - ---- Returns if the group is allowed a given action ---- @param action string|defines.input_action Actions to test ---- @return boolean # True if successful -function Groups._prototype:allows(action) - if type(action) == "string" then - return self.group.allows_action(defines.input_action[action]) - end - return self.group.allows_action(action) -end - ---- Print a message to all players in the group -function Groups._prototype:print(...) - for _, player in ipairs(self.group.players) do - player.print(...) - end -end - ---- Static and Prototype methods for use with IPC - ---- Convert an array of strings into an array of action names ---- @param actions_names string[] An array of action names ---- @return defines.input_action[] -local function names_to_actions(actions_names) - local actions, invalid, invalid_i = {}, {}, 1 - for i, action_name in ipairs(actions_names) do - local action = defines.input_action[action_name] - if action then - actions[i] = action - else - invalid[invalid_i] = i - invalid_i = invalid_i + 1 - end - end - - local last = #actions - for _, i in ipairs(invalid) do - actions[i] = actions[last] - last = last - 1 - end - - return actions -end - ---- Get the action names from the action numbers -function Groups.actions_to_names(actions) - local names = {} - for i, action in ipairs(actions) do - names[i] = action_to_name[action] - end - - return names -end - ---- Get all input actions that are defined -function Groups.get_actions_json() - local rtn, rtn_i = {}, 1 - for name in pairs(defines.input_action) do - rtn[rtn_i] = name - rtn_i = rtn_i + 1 - end - - return table_to_json(rtn) -end - ---- Convert a json string array into an array of input actions ---- @param json string A json string representing a string array of actions ---- @return defines.input_action[] -function Groups.json_to_actions(json) - local tbl = json_to_table(json) - assert(tbl, "Invalid Json String") - --- @cast tbl string[] - return names_to_actions(tbl) -end - ---- Returns the shortest defination of the allowed actions --- The first value of the return can be passed to :reset -function Groups._prototype:to_json(raw) - local allow, disallow = {}, {} - local allow_i, disallow_i = 1, 1 - local allows = self.group.allows_action - for name, action in pairs(defines.input_action) do - if allows(action) then - allow[allow_i] = name - allow_i = allow_i + 1 - else - disallow[disallow_i] = name - disallow_i = disallow_i + 1 - end - end - - if allow_i >= disallow_i then - return raw and { true, disallow } or table_to_json{ true, disallow } - end - return raw and { false, allow } or table_to_json{ false, allow } -end - ---- Restores this group to the state given in a json string ---- @param json string The json string to restore from -function Groups._prototype:from_json(json) - local tbl = json_to_table(json) - assert(tbl and type(tbl[1]) == "boolean" and type(tbl[2]) == "table", "Invalid Json String") - - if tbl[1] then - self:reset(true):disallow_actions(names_to_actions(tbl[2])) - return - end - self:reset(false):allow_actions(names_to_actions(tbl[2])) -end - -return Groups diff --git a/exp_groups/package.json b/exp_groups/package.json deleted file mode 100644 index ce171ce0..00000000 --- a/exp_groups/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "@expcluster/permission_groups", - "private": true, - "version": "0.0.0", - "description": "Example Description. Package. Change me in package.json", - "main": "dist/node/index.js", - "scripts": { - "prepare": "tsc --build && webpack-cli --env production" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@clusterio/lib": "^2.0.0-alpha.19" - }, - "devDependencies": { - "@clusterio/lib": "^2.0.0-alpha.19", - "@clusterio/web_ui": "^2.0.0-alpha.19", - "@types/fs-extra": "^11.0.4", - "@types/node": "^20.4.5", - "@types/react": "^18.2.21", - "antd": "^5.13.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "typescript": "^5.5.3", - "webpack": "^5.88.2", - "webpack-cli": "^5.1.4", - "webpack-merge": "^5.9.0" - }, - "dependencies": { - "@sinclair/typebox": "^0.30.4", - "fs-extra": "^11.2.0" - }, - "publishConfig": { - "access": "public" - }, - "keywords": [ - "clusterio", - "factorio" - ] -} diff --git a/exp_groups/tsconfig.browser.json b/exp_groups/tsconfig.browser.json deleted file mode 100644 index 1e3889e7..00000000 --- a/exp_groups/tsconfig.browser.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../tsconfig.browser.json", - "include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ], -} diff --git a/exp_groups/tsconfig.json b/exp_groups/tsconfig.json deleted file mode 100644 index be6d4e98..00000000 --- a/exp_groups/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.browser.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/exp_groups/tsconfig.node.json b/exp_groups/tsconfig.node.json deleted file mode 100644 index 3218f2e7..00000000 --- a/exp_groups/tsconfig.node.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../tsconfig.node.json", - "include": ["./**/*.ts"], - "exclude": ["test/*", "./dist/*"], -} diff --git a/exp_groups/web/components/groupTree.tsx b/exp_groups/web/components/groupTree.tsx deleted file mode 100644 index a8bd6718..00000000 --- a/exp_groups/web/components/groupTree.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import React, { useState } from 'react'; -import { Tree } from 'antd'; -import type { TreeDataNode, TreeProps } from 'antd'; - -const defaultData: TreeDataNode[] = [ - { - title: "Group 1", - key: "G-1", - icon: false, - children: [ - { - title: "Role 1", - key: "R-1" - }, - { - title: "Role 2", - key: "R-2" - }, - { - title: "Role 3", - key: "R-3" - } - ] - }, - { - title: "Group 2", - key: "G-2", - icon: false, - children: [ - { - title: "Role 4", - key: "R-4" - }, - { - title: "Role 5", - key: "R-5" - } - ] - }, - { - title: "Default", - key: "G-3", - icon: false, - children: [ - { - title: "Role 6", - key: "R-6" - } - ] - } -]; - -export function GroupTree() { - const [gData, setGData] = useState(defaultData); - - const onDrop: TreeProps['onDrop'] = (info) => { - const dropKey = info.node.key; - const dragKey = info.dragNode.key; - const dropPos = info.node.pos.split('-'); - const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // the drop position relative to the drop node, inside 0, top -1, bottom 1 - - const findKey = ( - data: TreeDataNode[], - key: React.Key, - callback: (node: TreeDataNode, i: number, data: TreeDataNode[]) => void, - ) => { - for (let i = 0; i < data.length; i++) { - if (data[i].key === key) { - return callback(data[i], i, data); - } - if (data[i].children) { - findKey(data[i].children!, key, callback); - } - } - }; - - const data = [...gData] - - // Find dragObject - let dragObj: TreeDataNode; - findKey(data, dragKey, (item, index, arr) => { - arr.splice(index, 1); - dragObj = item; - }); - - if (!info.dropToGap) { - // Drop on the content - findKey(data, dropKey, (item) => { - item.children = item.children || []; - // where to insert. New item was inserted to the start of the array in this example, but can be anywhere - item.children.unshift(dragObj); - }); - } else { - let ar: TreeDataNode[] = []; - let i: number; - findKey(data, dropKey, (_item, index, arr) => { - ar = arr; - i = index; - }); - if (dropPosition === -1) { - // Drop on the top of the drop node - ar.splice(i!, 0, dragObj!); - } else { - // Drop on the bottom of the drop node - ar.splice(i! + 1, 0, dragObj!); - } - } - - setGData(data) - }; - - const allowDrop: TreeProps['allowDrop'] = ({dragNode, dropNode, dropPosition}) => { - const dragType = (dragNode.key as string).charAt(0); - const dropType = (dropNode.key as string).charAt(0); - return dropType === dragType && dropPosition != 0 || dragType === "R" && dropType === "G" && dropPosition == 0 - } - - return ( - - ); -}; diff --git a/exp_groups/web/index.tsx b/exp_groups/web/index.tsx deleted file mode 100644 index 0f85661b..00000000 --- a/exp_groups/web/index.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { - useContext, useEffect, useState, - useCallback, useSyncExternalStore, -} from "react"; - -// import { -// -// } from "antd"; - -import { - BaseWebPlugin, PageLayout, PageHeader, Control, ControlContext, notifyErrorHandler, - useInstances, -} from "@clusterio/web_ui"; - -import { PermissionGroupUpdate, PermissionInstanceId, PermissionStringsUpdate } from "../messages"; - -import * as lib from "@clusterio/lib"; - -import { GroupTree } from "./components/groupTree"; - -function MyTemplatePage() { - const control = useContext(ControlContext); - const plugin = control.plugins.get("exp_groups") as WebPlugin; - const [permissionStrings, permissionStringsSynced] = plugin.usePermissionStrings(); - const [permissionGroups, permissionGroupsSynced] = plugin.usePermissionGroups(); - const [instances, instancesSync] = useInstances(); - - let [roles, setRoles] = useState([]); - - useEffect(() => { - control.send(new lib.RoleListRequest()).then(newRoles => { - setRoles(newRoles); - }).catch(notifyErrorHandler("Error fetching role list")); - }, []); - - return - - Permission Strings: {String(permissionStringsSynced)} {JSON.stringify([...permissionStrings.values()])}
- Permission Groups: {String(permissionGroupsSynced)} {JSON.stringify([...permissionGroups.values()])}
- Instances: {String(instancesSync)} {JSON.stringify([...instances.values()].map(instance => [instance.id, instance.name]))}
- Roles: {JSON.stringify([...roles.values()].map(role => [role.id, role.name]))}
- -
; -} - -export class WebPlugin extends BaseWebPlugin { - permissionStrings = new lib.EventSubscriber(PermissionStringsUpdate, this.control); - permissionGroups = new lib.EventSubscriber(PermissionGroupUpdate, this.control); - - async init() { - this.pages = [ - { - path: "/exp_groups", - sidebarName: "exp_groups", - permission: "exp_groups.list", - content: , - }, - ]; - } - - useInstancePermissionStrings(instanceId?: PermissionInstanceId) { - const [permissionStrings, synced] = this.usePermissionStrings(); - return [instanceId !== undefined ? permissionStrings.get(instanceId) : undefined, synced] as const; - } - - usePermissionStrings() { - const control = useContext(ControlContext); - const subscribe = useCallback((callback: () => void) => this.permissionStrings.subscribe(callback), [control]); - return useSyncExternalStore(subscribe, () => this.permissionStrings.getSnapshot()); - } - - useInstancePermissionGroups(instanceId?: PermissionInstanceId) { - const [permissionGroups, synced] = this.usePermissionGroups(); - return [instanceId !== undefined ? [...permissionGroups.values()].filter(group => group.instanceId === instanceId) : undefined, synced] as const; - } - - usePermissionGroups() { - const control = useContext(ControlContext); - const subscribe = useCallback((callback: () => void) => this.permissionGroups.subscribe(callback), [control]); - return useSyncExternalStore(subscribe, () => this.permissionGroups.getSnapshot()); - } -} diff --git a/exp_groups/webpack.config.js b/exp_groups/webpack.config.js deleted file mode 100644 index 6c269a95..00000000 --- a/exp_groups/webpack.config.js +++ /dev/null @@ -1,32 +0,0 @@ -"use strict"; -const path = require("path"); -const webpack = require("webpack"); -const { merge } = require("webpack-merge"); - -const common = require("@clusterio/web_ui/webpack.common"); - -module.exports = (env = {}) => merge(common(env), { - context: __dirname, - entry: "./web/index.tsx", - output: { - path: path.resolve(__dirname, "dist", "web"), - }, - plugins: [ - new webpack.container.ModuleFederationPlugin({ - name: "exp_groups", - library: { type: "window", name: "plugin_exp_groups" }, - exposes: { - "./": "./index.ts", - "./package.json": "./package.json", - "./web": "./web/index.tsx", - }, - shared: { - "@clusterio/lib": { import: false }, - "@clusterio/web_ui": { import: false }, - "antd": { import: false }, - "react": { import: false }, - "react-dom": { import: false }, - }, - }), - ], -});