mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2025-12-31 13:01:39 +09:00
.
This commit is contained in:
@@ -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<lib.User["id"], PermissionGroup> = new Map(); // TODO this needs to be per instance
|
|
||||||
permissionStrings!: Map<PermissionStrings["id"], PermissionStrings>;
|
|
||||||
permissionGroups!: Map<InstancePermissionGroups["id"], InstancePermissionGroups>;
|
|
||||||
|
|
||||||
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<string>(), 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -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<string> = 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<PermissionGroup>) {
|
|
||||||
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 ]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<GamePermission> = 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<typeof this.jsonSchema>) {
|
|
||||||
return new this(
|
|
||||||
json.instanceId,
|
|
||||||
json.name,
|
|
||||||
json.order,
|
|
||||||
new Set(json.roleIds),
|
|
||||||
new Set(json.permissions),
|
|
||||||
json.updatedAtMs,
|
|
||||||
json.isDeleted
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): Static<typeof PermissionGroup.jsonSchema> {
|
|
||||||
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<PermissionGroup["name"], PermissionGroup> = new Map(),
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
static jsonSchema = Type.Object({
|
|
||||||
instanceId: PermissionInstanceIdSchema,
|
|
||||||
permissionsGroups: Type.Array(PermissionGroup.jsonSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
static fromJSON(json: Static<typeof InstancePermissionGroups.jsonSchema>) {
|
|
||||||
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<typeof this.jsonSchema>) {
|
|
||||||
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<typeof PermissionGroupEditEvent.jsonSchema>;
|
|
||||||
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<typeof this.jsonSchema>) {
|
|
||||||
return new this(Address.fromJSON(json.src), json.type, json.group, json.changes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PermissionStrings {
|
|
||||||
constructor(
|
|
||||||
public instanceId: PermissionInstanceId,
|
|
||||||
public permissions: Set<GamePermission>,
|
|
||||||
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<typeof PermissionStrings.jsonSchema>) {
|
|
||||||
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<typeof this.jsonSchema>) {
|
|
||||||
return new this(
|
|
||||||
json.updates.map(update => PermissionStrings.fromJSON(update))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "exp_groups",
|
|
||||||
"load": [
|
|
||||||
"control.lua"
|
|
||||||
],
|
|
||||||
"require": [
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"clusterio": "*",
|
|
||||||
"exp_util": "*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../tsconfig.browser.json",
|
|
||||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"files": [],
|
|
||||||
"references": [
|
|
||||||
{ "path": "./tsconfig.browser.json" },
|
|
||||||
{ "path": "./tsconfig.node.json" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../tsconfig.node.json",
|
|
||||||
"include": ["./**/*.ts"],
|
|
||||||
"exclude": ["test/*", "./dist/*"],
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<Tree
|
|
||||||
className="draggable-tree"
|
|
||||||
defaultExpandAll={true}
|
|
||||||
draggable
|
|
||||||
blockNode
|
|
||||||
onDrop={onDrop}
|
|
||||||
allowDrop={allowDrop}
|
|
||||||
treeData={gData}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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<lib.Role[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
control.send(new lib.RoleListRequest()).then(newRoles => {
|
|
||||||
setRoles(newRoles);
|
|
||||||
}).catch(notifyErrorHandler("Error fetching role list"));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <PageLayout nav={[{ name: "exp_groups" }]}>
|
|
||||||
<PageHeader title="exp_groups" />
|
|
||||||
Permission Strings: {String(permissionStringsSynced)} {JSON.stringify([...permissionStrings.values()])} <br/>
|
|
||||||
Permission Groups: {String(permissionGroupsSynced)} {JSON.stringify([...permissionGroups.values()])} <br/>
|
|
||||||
Instances: {String(instancesSync)} {JSON.stringify([...instances.values()].map(instance => [instance.id, instance.name]))} <br/>
|
|
||||||
Roles: {JSON.stringify([...roles.values()].map(role => [role.id, role.name]))} <br/>
|
|
||||||
<GroupTree/>
|
|
||||||
</PageLayout>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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: <MyTemplatePage/>,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user