Migrate SelfGrantableRoles to new Plugin structure
This commit is contained in:
parent
140ba84544
commit
763bdd0b19
10 changed files with 418 additions and 0 deletions
|
@ -0,0 +1,95 @@
|
||||||
|
import { PluginOptions } from "knub";
|
||||||
|
import { SelfGrantableRolesPluginType, ConfigSchema, defaultSelfGrantableRoleEntry } from "./types";
|
||||||
|
import { zeppelinPlugin } from "../ZeppelinPluginBlueprint";
|
||||||
|
import { trimPluginDescription } from "src/utils";
|
||||||
|
import { RoleAddCmd } from "./commands/RoleAddCmd";
|
||||||
|
import { RoleRemoveCmd } from "./commands/RoleRemoveCmd";
|
||||||
|
import { RoleHelpCmd } from "./commands/RoleHelpCmd";
|
||||||
|
|
||||||
|
const defaultOptions: PluginOptions<SelfGrantableRolesPluginType> = {
|
||||||
|
config: {
|
||||||
|
entries: {},
|
||||||
|
mention_roles: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelfGrantableRolesPlugin = zeppelinPlugin<SelfGrantableRolesPluginType>()("self_grantable_roles", {
|
||||||
|
configSchema: ConfigSchema,
|
||||||
|
defaultOptions,
|
||||||
|
|
||||||
|
info: {
|
||||||
|
prettyName: "Self-grantable roles",
|
||||||
|
description: trimPluginDescription(`
|
||||||
|
Allows users to grant themselves roles via a command
|
||||||
|
`),
|
||||||
|
configurationGuide: trimPluginDescription(`
|
||||||
|
### Basic configuration
|
||||||
|
In this example, users can add themselves platform roles on the channel 473087035574321152 by using the
|
||||||
|
\`!role\` command. For example, \`!role pc ps4\` to add both the "pc" and "ps4" roles as specified below.
|
||||||
|
|
||||||
|
~~~yml
|
||||||
|
self_grantable_roles:
|
||||||
|
config:
|
||||||
|
entries:
|
||||||
|
basic:
|
||||||
|
roles:
|
||||||
|
"543184300250759188": ["pc", "computer"]
|
||||||
|
"534710505915547658": ["ps4", "ps", "playstation"]
|
||||||
|
"473085927053590538": ["xbox", "xb1", "xb"]
|
||||||
|
overrides:
|
||||||
|
- channel: "473087035574321152"
|
||||||
|
config:
|
||||||
|
entries:
|
||||||
|
basic:
|
||||||
|
roles:
|
||||||
|
can_use: true
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Maximum number of roles
|
||||||
|
This is identical to the basic example above, but users can only choose 1 role.
|
||||||
|
|
||||||
|
~~~yml
|
||||||
|
self_grantable_roles:
|
||||||
|
config:
|
||||||
|
entries:
|
||||||
|
basic:
|
||||||
|
roles:
|
||||||
|
"543184300250759188": ["pc", "computer"]
|
||||||
|
"534710505915547658": ["ps4", "ps", "playstation"]
|
||||||
|
"473085927053590538": ["xbox", "xb1", "xb"]
|
||||||
|
max_roles: 1
|
||||||
|
overrides:
|
||||||
|
- channel: "473087035574321152"
|
||||||
|
config:
|
||||||
|
entries:
|
||||||
|
basic:
|
||||||
|
roles:
|
||||||
|
can_use: true
|
||||||
|
~~~
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
|
||||||
|
configPreprocessor: options => {
|
||||||
|
const config = options.config;
|
||||||
|
for (const [key, entry] of Object.entries(config.entries)) {
|
||||||
|
// Apply default entry config
|
||||||
|
config.entries[key] = { ...defaultSelfGrantableRoleEntry, ...entry };
|
||||||
|
|
||||||
|
// Normalize alias names
|
||||||
|
if (entry.roles) {
|
||||||
|
for (const [roleId, aliases] of Object.entries(entry.roles)) {
|
||||||
|
entry.roles[roleId] = aliases.map(a => a.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...options, config };
|
||||||
|
},
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
commands: [
|
||||||
|
RoleHelpCmd,
|
||||||
|
RoleRemoveCmd,
|
||||||
|
RoleAddCmd,
|
||||||
|
]
|
||||||
|
});
|
124
backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts
Normal file
124
backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import { selfGrantableRolesCmd } from "../types";
|
||||||
|
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||||
|
import { getApplyingEntries } from "../util/getApplyingEntries";
|
||||||
|
import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils";
|
||||||
|
import { splitRoleNames } from "../util/splitRoleNames";
|
||||||
|
import { normalizeRoleNames } from "../util/normalizeRoleNames";
|
||||||
|
import { findMatchingRoles } from "../util/findMatchingRoles";
|
||||||
|
import { Role } from "eris";
|
||||||
|
|
||||||
|
export const RoleAddCmd = selfGrantableRolesCmd({
|
||||||
|
trigger: ["role", "role add"],
|
||||||
|
permission: null,
|
||||||
|
|
||||||
|
signature: {
|
||||||
|
roleNames: ct.string({ rest: true }),
|
||||||
|
},
|
||||||
|
|
||||||
|
async run({ message: msg, args, pluginData }) {
|
||||||
|
const lock = await pluginData.locks.acquire(`grantableRoles:${msg.author.id}`);
|
||||||
|
|
||||||
|
const applyingEntries = getApplyingEntries(pluginData, msg);
|
||||||
|
if (applyingEntries.length === 0) {
|
||||||
|
lock.unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleNames = normalizeRoleNames(splitRoleNames(args.roleNames));
|
||||||
|
const matchedRoleIds = findMatchingRoles(roleNames, applyingEntries);
|
||||||
|
|
||||||
|
const hasUnknownRoles = matchedRoleIds.length !== roleNames.length;
|
||||||
|
|
||||||
|
const rolesToAdd: Map<string, Role> = Array.from(matchedRoleIds.values())
|
||||||
|
.map(id => pluginData.guild.roles.get(id))
|
||||||
|
.filter(Boolean)
|
||||||
|
.reduce((map, role) => {
|
||||||
|
map.set(role.id, role);
|
||||||
|
return map;
|
||||||
|
}, new Map());
|
||||||
|
|
||||||
|
if (!rolesToAdd.size) {
|
||||||
|
sendErrorMessage(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
`<@!${msg.author.id}> Unknown ${args.roleNames.length === 1 ? "role" : "roles"}`,
|
||||||
|
);
|
||||||
|
lock.unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grant the roles
|
||||||
|
const newRoleIds = new Set([...rolesToAdd.keys(), ...msg.member.roles]);
|
||||||
|
|
||||||
|
// Remove extra roles (max_roles) for each entry
|
||||||
|
const skipped: Set<Role> = new Set();
|
||||||
|
const removed: Set<Role> = new Set();
|
||||||
|
|
||||||
|
for (const entry of applyingEntries) {
|
||||||
|
if (entry.max_roles === 0) continue;
|
||||||
|
|
||||||
|
let foundRoles = 0;
|
||||||
|
|
||||||
|
for (const roleId of newRoleIds) {
|
||||||
|
if (entry.roles[roleId]) {
|
||||||
|
if (foundRoles < entry.max_roles) {
|
||||||
|
foundRoles++;
|
||||||
|
} else {
|
||||||
|
newRoleIds.delete(roleId);
|
||||||
|
rolesToAdd.delete(roleId);
|
||||||
|
|
||||||
|
if (msg.member.roles.includes(roleId)) {
|
||||||
|
removed.add(pluginData.guild.roles.get(roleId));
|
||||||
|
} else {
|
||||||
|
skipped.add(pluginData.guild.roles.get(roleId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await msg.member.edit({
|
||||||
|
roles: Array.from(newRoleIds),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
sendErrorMessage(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
`<@!${msg.author.id}> Got an error while trying to grant you the roles`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mentionRoles = pluginData.config.get().mention_roles;
|
||||||
|
const addedRolesStr = Array.from(rolesToAdd.values()).map(r => (mentionRoles ? `<@&${r.id}>` : `**${r.name}**`));
|
||||||
|
const addedRolesWord = rolesToAdd.size === 1 ? "role" : "roles";
|
||||||
|
|
||||||
|
const messageParts = [];
|
||||||
|
messageParts.push(`Granted you the ${addedRolesStr.join(", ")} ${addedRolesWord}`);
|
||||||
|
|
||||||
|
if (skipped.size || removed.size) {
|
||||||
|
const skippedRolesStr = skipped.size
|
||||||
|
? "skipped " +
|
||||||
|
Array.from(skipped.values())
|
||||||
|
.map(r => (mentionRoles ? `<@&${r.id}>` : `**${r.name}**`))
|
||||||
|
.join(",")
|
||||||
|
: null;
|
||||||
|
const removedRolesStr = removed.size
|
||||||
|
? "removed " + Array.from(removed.values()).map(r => (mentionRoles ? `<@&${r.id}>` : `**${r.name}**`))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const skippedRemovedStr = [skippedRolesStr, removedRolesStr].filter(Boolean).join(" and ");
|
||||||
|
|
||||||
|
messageParts.push(`${skippedRemovedStr} due to role limits`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasUnknownRoles) {
|
||||||
|
messageParts.push("couldn't recognize some of the roles");
|
||||||
|
}
|
||||||
|
|
||||||
|
sendSuccessMessage(pluginData, msg.channel, `<@!${msg.author.id}> ${messageParts.join("; ")}`);
|
||||||
|
|
||||||
|
lock.unlock();
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { selfGrantableRolesCmd } from "../types";
|
||||||
|
import { asSingleLine, trimLines } from "src/utils";
|
||||||
|
import { getApplyingEntries } from "../util/getApplyingEntries";
|
||||||
|
|
||||||
|
export const RoleHelpCmd = selfGrantableRolesCmd({
|
||||||
|
trigger: ["role help", "role"],
|
||||||
|
permission: null,
|
||||||
|
|
||||||
|
async run({ message: msg, pluginData }) {
|
||||||
|
const applyingEntries = getApplyingEntries(pluginData, msg);
|
||||||
|
if (applyingEntries.length === 0) return;
|
||||||
|
|
||||||
|
const allPrimaryAliases = [];
|
||||||
|
for (const entry of applyingEntries) {
|
||||||
|
for (const aliases of Object.values(entry.roles)) {
|
||||||
|
if (aliases[0]) {
|
||||||
|
allPrimaryAliases.push(aliases[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = pluginData.guildConfig.prefix;
|
||||||
|
const [firstRole, secondRole] = allPrimaryAliases;
|
||||||
|
|
||||||
|
const help1 = asSingleLine(`
|
||||||
|
To give yourself a role, type e.g. \`${prefix}role ${firstRole}\` where **${firstRole}** is the role you want.
|
||||||
|
${secondRole ? `You can also add multiple roles at once, e.g. \`${prefix}role ${firstRole} ${secondRole}\`` : ""}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const help2 = asSingleLine(`
|
||||||
|
To remove a role, type \`${prefix}role remove ${firstRole}\`,
|
||||||
|
again replacing **${firstRole}** with the role you want to remove.
|
||||||
|
`);
|
||||||
|
|
||||||
|
const helpMessage = trimLines(`
|
||||||
|
${help1}
|
||||||
|
|
||||||
|
${help2}
|
||||||
|
|
||||||
|
**Roles available to you:**
|
||||||
|
${allPrimaryAliases.join(", ")}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const helpEmbed = {
|
||||||
|
title: "How to get roles",
|
||||||
|
description: helpMessage,
|
||||||
|
color: parseInt("42bff4", 16),
|
||||||
|
};
|
||||||
|
|
||||||
|
msg.channel.createMessage({ embed: helpEmbed });
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { selfGrantableRolesCmd } from "../types";
|
||||||
|
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||||
|
import { getApplyingEntries } from "../util/getApplyingEntries";
|
||||||
|
import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils";
|
||||||
|
import { splitRoleNames } from "../util/splitRoleNames";
|
||||||
|
import { normalizeRoleNames } from "../util/normalizeRoleNames";
|
||||||
|
import { findMatchingRoles } from "../util/findMatchingRoles";
|
||||||
|
|
||||||
|
export const RoleRemoveCmd = selfGrantableRolesCmd({
|
||||||
|
trigger: "role remove",
|
||||||
|
permission: null,
|
||||||
|
|
||||||
|
signature: {
|
||||||
|
roleNames: ct.string({ rest: true }),
|
||||||
|
},
|
||||||
|
|
||||||
|
async run({ message: msg, args, pluginData }) {
|
||||||
|
const lock = await pluginData.locks.acquire(`grantableRoles:${msg.author.id}`);
|
||||||
|
|
||||||
|
const applyingEntries = getApplyingEntries(pluginData, msg);
|
||||||
|
if (applyingEntries.length === 0) {
|
||||||
|
lock.unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleNames = normalizeRoleNames(splitRoleNames(args.roleNames));
|
||||||
|
const matchedRoleIds = findMatchingRoles(roleNames, applyingEntries);
|
||||||
|
|
||||||
|
const rolesToRemove = Array.from(matchedRoleIds.values()).map(id => pluginData.guild.roles.get(id));
|
||||||
|
const roleIdsToRemove = rolesToRemove.map(r => r.id);
|
||||||
|
|
||||||
|
// Remove the roles
|
||||||
|
if (rolesToRemove.length) {
|
||||||
|
const newRoleIds = msg.member.roles.filter(roleId => !roleIdsToRemove.includes(roleId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await msg.member.edit({
|
||||||
|
roles: newRoleIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
const removedRolesStr = rolesToRemove.map(r => `**${r.name}**`);
|
||||||
|
const removedRolesWord = rolesToRemove.length === 1 ? "role" : "roles";
|
||||||
|
|
||||||
|
if (rolesToRemove.length !== roleNames.length) {
|
||||||
|
sendSuccessMessage(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
`<@!${msg.author.id}> Removed ${removedRolesStr.join(", ")} ${removedRolesWord};` +
|
||||||
|
` couldn't recognize the other roles you mentioned`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sendSuccessMessage(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
`<@!${msg.author.id}> Removed ${removedRolesStr.join(", ")} ${removedRolesWord}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
sendSuccessMessage(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
`<@!${msg.author.id}> Got an error while trying to remove the roles`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendErrorMessage(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
`<@!${msg.author.id}> Unknown ${args.roleNames.length === 1 ? "role" : "roles"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock.unlock();
|
||||||
|
},
|
||||||
|
});
|
31
backend/src/plugins/SelfGrantableRoles/types.ts
Normal file
31
backend/src/plugins/SelfGrantableRoles/types.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import * as t from "io-ts";
|
||||||
|
import { BasePluginType, command } from "knub";
|
||||||
|
|
||||||
|
const RoleMap = t.record(t.string, t.array(t.string));
|
||||||
|
|
||||||
|
const SelfGrantableRoleEntry = t.type({
|
||||||
|
roles: RoleMap,
|
||||||
|
can_use: t.boolean,
|
||||||
|
can_ignore_cooldown: t.boolean,
|
||||||
|
max_roles: t.number,
|
||||||
|
});
|
||||||
|
const PartialRoleEntry = t.partial(SelfGrantableRoleEntry.props);
|
||||||
|
export type TSelfGrantableRoleEntry = t.TypeOf<typeof SelfGrantableRoleEntry>;
|
||||||
|
|
||||||
|
export const ConfigSchema = t.type({
|
||||||
|
entries: t.record(t.string, SelfGrantableRoleEntry),
|
||||||
|
mention_roles: t.boolean,
|
||||||
|
});
|
||||||
|
type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
||||||
|
|
||||||
|
export const defaultSelfGrantableRoleEntry: t.TypeOf<typeof PartialRoleEntry> = {
|
||||||
|
can_use: false,
|
||||||
|
can_ignore_cooldown: false,
|
||||||
|
max_roles: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SelfGrantableRolesPluginType extends BasePluginType {
|
||||||
|
config: TConfigSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selfGrantableRolesCmd = command<SelfGrantableRolesPluginType>();
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { TSelfGrantableRoleEntry } from "../types";
|
||||||
|
|
||||||
|
export function findMatchingRoles(roleNames, entries: TSelfGrantableRoleEntry[]): string[] {
|
||||||
|
const aliasToRoleId = entries.reduce((map, entry) => {
|
||||||
|
for (const [roleId, aliases] of Object.entries(entry.roles)) {
|
||||||
|
for (const alias of aliases) {
|
||||||
|
map.set(alias, roleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, new Map());
|
||||||
|
|
||||||
|
return roleNames.map(roleName => aliasToRoleId.get(roleName)).filter(Boolean);
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { TSelfGrantableRoleEntry, SelfGrantableRolesPluginType } from "../types";
|
||||||
|
import { PluginData } from "knub";
|
||||||
|
|
||||||
|
export function getApplyingEntries(
|
||||||
|
pluginData: PluginData<SelfGrantableRolesPluginType>,
|
||||||
|
msg,
|
||||||
|
): TSelfGrantableRoleEntry[] {
|
||||||
|
const config = pluginData.config.getForMessage(msg);
|
||||||
|
return Object.entries(config.entries)
|
||||||
|
.filter(
|
||||||
|
([k, e]) =>
|
||||||
|
e.can_use && !(!e.can_ignore_cooldown && pluginData.state.cooldowns.isOnCooldown(`${k}:${msg.author.id}`)),
|
||||||
|
)
|
||||||
|
.map(pair => pair[1]);
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function normalizeRoleNames(roleNames: string[]) {
|
||||||
|
return roleNames.map(v => v.toLowerCase());
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export function splitRoleNames(roleNames: string[]) {
|
||||||
|
return roleNames
|
||||||
|
.map(v => v.split(/[\s,]+/))
|
||||||
|
.flat()
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import { RolesPlugin } from "./Roles/RolesPlugin";
|
||||||
import { SlowmodePlugin } from "./Slowmode/SlowmodePlugin";
|
import { SlowmodePlugin } from "./Slowmode/SlowmodePlugin";
|
||||||
import { StarboardPlugin } from "./Starboard/StarboardPlugin";
|
import { StarboardPlugin } from "./Starboard/StarboardPlugin";
|
||||||
import { ChannelArchiverPlugin } from "./ChannelArchiver/ChannelArchiverPlugin";
|
import { ChannelArchiverPlugin } from "./ChannelArchiver/ChannelArchiverPlugin";
|
||||||
|
import { SelfGrantableRolesPlugin } from "./SelfGrantableRoles/SelfGrantableRolesPlugin";
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export const guildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
|
export const guildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
|
||||||
|
@ -39,6 +40,7 @@ export const guildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
|
||||||
NameHistoryPlugin,
|
NameHistoryPlugin,
|
||||||
RemindersPlugin,
|
RemindersPlugin,
|
||||||
RolesPlugin,
|
RolesPlugin,
|
||||||
|
SelfGrantableRolesPlugin,
|
||||||
SlowmodePlugin,
|
SlowmodePlugin,
|
||||||
StarboardPlugin,
|
StarboardPlugin,
|
||||||
TagsPlugin,
|
TagsPlugin,
|
||||||
|
|
Loading…
Add table
Reference in a new issue