commands/ward-group.ts
18 KB · sha256:81436014297ce2de1d7391eedb3d0949aa68e669d963c8fac349418e3f5ae0b9
import {
addNode,
addParent,
addPermission,
createGroup,
deleteGroup,
denyPermission,
findGroup,
getGroupPrefix,
getGroupSuffix,
getParents,
groupExists,
listGroups,
removeNode,
removeParent,
removePermission,
resolveGroupPermissions,
setDefaultGroup,
setParent,
setPrefix,
setSuffix,
setWeight,
} from "../models/Group";
import { isActive, nodeKey } from "../models/Node";
import {
groupOrNoneSuggester,
groupSuggester,
refreshGroups,
} from "../lib/cache";
import { describeExpiry } from "../lib/duration";
import { refreshAll, refreshGroupMembers } from "../lib/attachments";
import { err, escapeMM, ok, raw, requirePerm, warn } from "../lib/format";
import { PERM } from "./ward";
@Command("ward group list")
export class WardGroupList {
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupInfo)) return;
const all = await listGroups();
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
raw(
ctx.sender,
`<gradient:#9b87f5:#5b3df5>Groups</gradient> <gray>(${all.length})`,
);
if (all.length === 0) raw(ctx.sender, "<dark_gray>(none)");
for (const g of all) {
const tag = g.isDefault ? " <gold>[default]</gold>" : "";
const parents = g.nodes
.filter((n) => n.type === "inheritance" && n.value)
.map((n) => n.key.slice("group.".length));
const parentTag = parents.length
? ` <dark_gray>parents=[${parents.map(escapeMM).join(", ")}]`
: "";
raw(
ctx.sender,
` <white>${escapeMM(g.name)}</white> <gray>w=${g.weight} nodes=${g.nodes.length}${tag}${parentTag}`,
);
}
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
}
}
@Command("ward group create")
export class WardGroupCreate {
@Arg("name", { type: "string" })
name!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
if (!/^[a-z0-9_\-]{1,32}$/i.test(this.name)) {
err(ctx.sender, "group name must be 1-32 chars of [a-zA-Z0-9_-]");
return;
}
if (await groupExists(this.name)) {
warn(
ctx.sender,
`group <white>${escapeMM(this.name)}</white> already exists`,
);
return;
}
const created = await createGroup(this.name);
await refreshGroups();
ok(ctx.sender, `created group <white>${escapeMM(created.name)}`);
}
}
@Command("ward group delete")
export class WardGroupDelete {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
const g = await findGroup(this.group);
if (!g) {
err(ctx.sender, `unknown group: <white>${escapeMM(this.group)}`);
return;
}
if (g.isDefault) {
err(
ctx.sender,
`cannot delete the default group; set another default first`,
);
return;
}
await deleteGroup(g.name);
await refreshGroups();
await refreshAll();
ok(ctx.sender, `deleted group <white>${escapeMM(g.name)}`);
}
}
@Command("ward group setdefault")
export class WardGroupSetDefault {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
if (!(await findGroup(this.group))) {
err(ctx.sender, `unknown group: <white>${escapeMM(this.group)}`);
return;
}
await setDefaultGroup(this.group);
await refreshAll();
ok(ctx.sender, `set default group to <white>${escapeMM(this.group)}`);
}
}
// ---------------------------------------------------------------------------
// /ward group <name> -- info card (re-declares the <group> arg
// so it lives at the same level as `list`)
// /ward group <name> info -- alias
// ---------------------------------------------------------------------------
@Command("ward group")
export class WardGroup {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupInfo)) return;
await renderGroupCard(ctx, this.group);
}
}
@Command("ward group info")
export class WardGroupInfo {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupInfo)) return;
await renderGroupCard(ctx, this.group);
}
}
async function renderGroupCard(ctx: CommandCtx, name: string) {
const g = await findGroup(name);
if (!g) {
err(ctx.sender, `unknown group: <white>${escapeMM(name)}`);
return;
}
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
raw(
ctx.sender,
`<gradient:#9b87f5:#5b3df5>${escapeMM(g.name)}</gradient>${g.isDefault ? " <gold>[default]" : ""}`,
);
raw(
ctx.sender,
`<gray>Display: <white>${escapeMM(g.displayName || g.name)}`,
);
raw(ctx.sender, `<gray>Weight: <white>${g.weight}`);
const parents = await getParents(g.name);
raw(
ctx.sender,
`<gray>Parents: <white>${parents.length ? parents.map(escapeMM).join(", ") : "<dark_gray>(none)"}`,
);
raw(
ctx.sender,
`<gray>Prefix: <white>${getGroupPrefix(g) ?? "<dark_gray>(none)"}`,
);
raw(
ctx.sender,
`<gray>Suffix: <white>${getGroupSuffix(g) ?? "<dark_gray>(none)"}`,
);
raw(ctx.sender, `<gray>Nodes (${g.nodes.length}):`);
if (g.nodes.length === 0) raw(ctx.sender, " <dark_gray>(none)");
for (const n of g.nodes) {
const sign = n.value ? "<green>+" : "<red>-";
const exp = n.expiry ? ` <dark_gray>${describeExpiry(n.expiry)}` : "";
raw(
ctx.sender,
` ${sign}<white>${escapeMM(n.key)}</white> <dark_gray>[${n.type}]</dark_gray>${exp}`,
);
}
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
}
// ---------------------------------------------------------------------------
// /ward group <name> permission add/remove <perm> [value]
// ---------------------------------------------------------------------------
@Command("ward group permission add")
export class WardGroupPermissionAdd {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Arg("perm", { type: "string" })
perm!: string;
@Arg("value", { type: "string", optional: true })
value?: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
if (!(await findGroup(this.group))) {
err(ctx.sender, `unknown group: <white>${escapeMM(this.group)}`);
return;
}
const deny = this.value === "false";
await (deny
? denyPermission(this.group, this.perm)
: addPermission(this.group, this.perm));
await refreshGroupMembers(this.group);
ok(
ctx.sender,
`${deny ? "denied" : "granted"} <white>${escapeMM(this.perm)}</white> on <white>${escapeMM(this.group)}`,
);
}
}
@Command("ward group permission remove")
export class WardGroupPermissionRemove {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Arg("perm", { type: "string" })
perm!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
if (!(await findGroup(this.group))) {
err(ctx.sender, `unknown group: <white>${escapeMM(this.group)}`);
return;
}
await removePermission(this.group, this.perm);
await refreshGroupMembers(this.group);
ok(
ctx.sender,
`removed <white>${escapeMM(this.perm)}</white> from <white>${escapeMM(this.group)}`,
);
}
}
@Command("ward group permission info")
export class WardGroupPermissionInfo {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupInfo)) return;
const g = await findGroup(this.group);
if (!g) {
err(ctx.sender, `unknown group: <white>${escapeMM(this.group)}`);
return;
}
const perms = await resolveGroupPermissions(g.name);
const entries = [...perms.entries()].sort();
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
raw(
ctx.sender,
`<gray>Resolved permissions for <white>${escapeMM(g.name)}</white> (${entries.length})`,
);
if (entries.length === 0) raw(ctx.sender, " <dark_gray>(none)");
for (const [k, v] of entries) {
raw(ctx.sender, ` ${v ? "<green>+" : "<red>-"}<white>${escapeMM(k)}`);
}
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
}
}
// ---------------------------------------------------------------------------
// /ward group <name> parent add/remove/set <parent>
// /ward group <name> parent info
// ---------------------------------------------------------------------------
@Command("ward group parent add")
export class WardGroupParentAdd {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Arg("parent", { type: "string", suggest: groupSuggester })
parent!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
if (!(await findGroup(this.group))) {
err(ctx.sender, `unknown group: <white>${escapeMM(this.group)}`);
return;
}
if (!(await findGroup(this.parent))) {
err(ctx.sender, `unknown parent: <white>${escapeMM(this.parent)}`);
return;
}
if (this.parent.toLowerCase() === this.group.toLowerCase()) {
err(ctx.sender, "a group cannot be its own parent");
return;
}
await addParent(this.group, this.parent);
await refreshGroupMembers(this.group);
ok(
ctx.sender,
`<white>${escapeMM(this.group)}</white> now inherits from <white>${escapeMM(this.parent)}`,
);
}
}
@Command("ward group parent remove")
export class WardGroupParentRemove {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Arg("parent", { type: "string", suggest: groupSuggester })
parent!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
await removeParent(this.group, this.parent);
await refreshGroupMembers(this.group);
ok(
ctx.sender,
`removed parent <white>${escapeMM(this.parent)}</white> from <white>${escapeMM(this.group)}`,
);
}
}
@Command("ward group parent set")
export class WardGroupParentSet {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Arg("parent", { type: "string", suggest: groupOrNoneSuggester })
parent!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
if (this.parent !== "none" && !(await findGroup(this.parent))) {
err(ctx.sender, `unknown parent: <white>${escapeMM(this.parent)}`);
return;
}
if (this.parent.toLowerCase() === this.group.toLowerCase()) {
err(ctx.sender, "a group cannot be its own parent");
return;
}
await setParent(this.group, this.parent as any);
await refreshGroupMembers(this.group);
if (this.parent === "none") {
ok(ctx.sender, `cleared parents of <white>${escapeMM(this.group)}`);
} else {
ok(
ctx.sender,
`set parent of <white>${escapeMM(this.group)}</white> -> <white>${escapeMM(this.parent)}`,
);
}
}
}
@Command("ward group parent info")
export class WardGroupParentInfo {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupInfo)) return;
const parents = await getParents(this.group);
ok(
ctx.sender,
`<white>${escapeMM(this.group)}</white> parents: <white>${
parents.length ? parents.map(escapeMM).join(", ") : "<dark_gray>(none)"
}`,
);
}
}
// ---------------------------------------------------------------------------
// /ward group <name> prefix|suffix set <text>
// /ward group <name> prefix|suffix clear
// /ward group <name> weight <n>
// ---------------------------------------------------------------------------
@Command("ward group prefix set")
export class WardGroupPrefixSet {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Arg("text", { type: "greedy", greedy: true })
text!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
const parsed = splitPriorityText(this.text, 100);
await setPrefix(this.group, parsed.text, parsed.priority);
await refreshGroupMembers(this.group);
ok(
ctx.sender,
`set prefix of <white>${escapeMM(this.group)}</white> -> ${parsed.text} <dark_gray>(p=${parsed.priority})`,
);
}
}
@Command("ward group prefix clear")
export class WardGroupPrefixClear {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
await setPrefix(this.group, "");
await refreshGroupMembers(this.group);
ok(ctx.sender, `cleared prefix of <white>${escapeMM(this.group)}`);
}
}
@Command("ward group suffix set")
export class WardGroupSuffixSet {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Arg("text", { type: "greedy", greedy: true })
text!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
const parsed = splitPriorityText(this.text, 100);
await setSuffix(this.group, parsed.text, parsed.priority);
await refreshGroupMembers(this.group);
ok(
ctx.sender,
`set suffix of <white>${escapeMM(this.group)}</white> -> ${parsed.text} <dark_gray>(p=${parsed.priority})`,
);
}
}
@Command("ward group suffix clear")
export class WardGroupSuffixClear {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
await setSuffix(this.group, "");
await refreshGroupMembers(this.group);
ok(ctx.sender, `cleared suffix of <white>${escapeMM(this.group)}`);
}
}
@Command("ward group weight")
export class WardGroupWeight {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Arg("weight", { type: "int" })
weight!: number;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
if (!(await findGroup(this.group))) {
err(ctx.sender, `unknown group: <white>${escapeMM(this.group)}`);
return;
}
await setWeight(this.group, this.weight);
await refreshGroupMembers(this.group);
ok(
ctx.sender,
`set weight of <white>${escapeMM(this.group)}</white> -> <white>${this.weight}`,
);
}
}
// ---------------------------------------------------------------------------
// /ward group <name> meta set <key> <value>
// /ward group <name> meta unset <key>
// ---------------------------------------------------------------------------
@Command("ward group meta set")
export class WardGroupMetaSet {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Arg("key", { type: "string" })
key!: string;
@Arg("value", { type: "greedy", greedy: true })
value!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
const g = await findGroup(this.group);
if (!g) {
err(ctx.sender, `unknown group: <white>${escapeMM(this.group)}`);
return;
}
g.nodes = g.nodes.filter(
(n) => !(n.type === "meta" && n.key.startsWith(`meta.${this.key}.`)),
);
await g.save();
await addNode(g.name, nodeKey.meta(this.key, this.value));
await refreshGroupMembers(g.name);
ok(
ctx.sender,
`set meta <white>${escapeMM(this.key)}=${escapeMM(this.value)}</white> on <white>${escapeMM(g.name)}`,
);
}
}
@Command("ward group meta unset")
export class WardGroupMetaUnset {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Arg("key", { type: "string" })
key!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
const g = await findGroup(this.group);
if (!g) {
err(ctx.sender, `unknown group: <white>${escapeMM(this.group)}`);
return;
}
const before = g.nodes.length;
g.nodes = g.nodes.filter(
(n) => !(n.type === "meta" && n.key.startsWith(`meta.${this.key}.`)),
);
if (g.nodes.length === before) {
warn(
ctx.sender,
`<white>${escapeMM(g.name)}</white> had no meta key <white>${escapeMM(this.key)}`,
);
return;
}
await g.save();
await refreshGroupMembers(g.name);
ok(
ctx.sender,
`cleared meta <white>${escapeMM(this.key)}</white> from <white>${escapeMM(g.name)}`,
);
}
}
// ---------------------------------------------------------------------------
// /ward group <name> clear
// ---------------------------------------------------------------------------
@Command("ward group clear")
export class WardGroupClear {
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.groupEdit)) return;
const g = await findGroup(this.group);
if (!g) {
err(ctx.sender, `unknown group: <white>${escapeMM(this.group)}`);
return;
}
g.nodes = [];
g.weight = 0;
await g.save();
await refreshGroupMembers(g.name);
ok(ctx.sender, `cleared all nodes on <white>${escapeMM(g.name)}`);
}
}
function splitPriorityText(
input: string,
dflt: number,
): { priority: number; text: string } {
const m = /^(-?\d+)\s+(.*)$/s.exec(input);
if (m) return { priority: Number(m[1]), text: m[2] };
return { priority: dflt, text: input };
}