mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2025-12-27 03:25:23 +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