3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-15 05:41:51 +00:00

Merge pull request #80 from DarkView/k30_tags

[K30] Migrated Tags
This commit is contained in:
Miikka 2020-07-22 23:03:13 +03:00 committed by GitHub
commit 2893ddc21c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 521 additions and 0 deletions

View file

@ -0,0 +1,76 @@
import { zeppelinPlugin } from "../ZeppelinPluginBlueprint";
import { ConfigSchema, TagsPluginType } from "./types";
import { PluginOptions } from "knub";
import { GuildArchives } from "src/data/GuildArchives";
import { GuildTags } from "src/data/GuildTags";
import { GuildSavedMessages } from "src/data/GuildSavedMessages";
import { GuildLogs } from "src/data/GuildLogs";
import { onMessageCreate } from "./util/onMessageCreate";
import { onMessageDelete } from "./util/onMessageDelete";
import { TagCreateCmd } from "./commands/TagCreateCmd";
import { TagDeleteCmd } from "./commands/TagDeleteCmd";
import { TagEvalCmd } from "./commands/TagEvalCmd";
import { TagListCmd } from "./commands/TagListCmd";
import { TagSourceCmd } from "./commands/TagSourceCmd";
const defaultOptions: PluginOptions<TagsPluginType> = {
config: {
prefix: "!!",
delete_with_command: true,
user_tag_cooldown: null,
global_tag_cooldown: null,
user_cooldown: null,
global_cooldown: null,
categories: {},
can_create: false,
can_use: false,
can_list: false,
},
overrides: [
{
level: ">=50",
config: {
can_use: true,
can_create: true,
can_list: true,
},
},
],
};
export const TagsPlugin = zeppelinPlugin<TagsPluginType>()("tags", {
configSchema: ConfigSchema,
defaultOptions,
// prettier-ignore
commands: [
TagEvalCmd,
TagCreateCmd,
TagDeleteCmd,
TagListCmd,
TagSourceCmd,
],
onLoad(pluginData) {
const { state, guild } = pluginData;
state.archives = GuildArchives.getGuildInstance(guild.id);
state.tags = GuildTags.getGuildInstance(guild.id);
state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id);
state.logs = new GuildLogs(guild.id);
state.onMessageCreateFn = msg => onMessageCreate(pluginData, msg);
state.savedMessages.events.on("create", state.onMessageCreateFn);
state.onMessageDeleteFn = msg => onMessageDelete(pluginData, msg);
state.savedMessages.events.on("delete", state.onMessageDeleteFn);
},
onUnload(pluginData) {
pluginData.state.savedMessages.events.off("create", pluginData.state.onMessageCreateFn);
pluginData.state.savedMessages.events.off("delete", pluginData.state.onMessageDeleteFn);
},
});

View file

@ -0,0 +1,32 @@
import { tagsCmd } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { parseTemplate, TemplateParseError } from "src/templateFormatter";
import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils";
export const TagCreateCmd = tagsCmd({
trigger: "tag",
permission: "can_create",
signature: {
tag: ct.string(),
body: ct.string({ catchAll: true }),
},
async run({ message: msg, args, pluginData }) {
try {
parseTemplate(args.body);
} catch (e) {
if (e instanceof TemplateParseError) {
sendErrorMessage(pluginData, msg.channel, `Invalid tag syntax: ${e.message}`);
return;
} else {
throw e;
}
}
await pluginData.state.tags.createOrUpdate(args.tag, args.body, msg.author.id);
const prefix = pluginData.config.get().prefix;
sendSuccessMessage(pluginData, msg.channel, `Tag set! Use it with: \`${prefix}${args.tag}\``);
},
});

View file

@ -0,0 +1,23 @@
import { tagsCmd } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils";
export const TagDeleteCmd = tagsCmd({
trigger: "tag delete",
permission: "can_create",
signature: {
tag: ct.string(),
},
async run({ message: msg, args, pluginData }) {
const tag = await pluginData.state.tags.find(args.tag);
if (!tag) {
sendErrorMessage(pluginData, msg.channel, "No tag with that name");
return;
}
await pluginData.state.tags.delete(args.tag);
sendSuccessMessage(pluginData, msg.channel, "Tag deleted!");
},
});

View file

@ -0,0 +1,17 @@
import { tagsCmd } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { renderTag } from "../util/renderTag";
export const TagEvalCmd = tagsCmd({
trigger: "tag eval",
permission: "can_create",
signature: {
body: ct.string({ catchAll: true }),
},
async run({ message: msg, args, pluginData }) {
const rendered = await renderTag(pluginData, args.body);
msg.channel.createMessage(rendered);
},
});

View file

@ -0,0 +1,18 @@
import { tagsCmd } from "../types";
export const TagListCmd = tagsCmd({
trigger: ["tag list", "tags", "taglist"],
permission: "can_list",
async run({ message: msg, pluginData }) {
const tags = await pluginData.state.tags.all();
if (tags.length === 0) {
msg.channel.createMessage(`No tags created yet! Use \`tag create\` command to create one.`);
return;
}
const prefix = pluginData.config.getForMessage(msg).prefix;
const tagNames = tags.map(tag => tag.tag).sort();
msg.channel.createMessage(`Available tags (use with ${prefix}tag): \`\`\`${tagNames.join(", ")}\`\`\``);
},
});

View file

@ -0,0 +1,40 @@
import { tagsCmd } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { sendErrorMessage, getBaseUrl, sendSuccessMessage } from "src/pluginUtils";
import moment from "moment-timezone";
export const TagSourceCmd = tagsCmd({
trigger: "tag",
permission: "can_create",
signature: {
tag: ct.string(),
delete: ct.bool({ option: true, shortcut: "d", isSwitch: true }),
},
async run({ message: msg, args, pluginData }) {
if (args.delete) {
const actualTag = await pluginData.state.tags.find(args.tag);
if (!actualTag) {
sendErrorMessage(pluginData, msg.channel, "No tag with that name");
return;
}
await pluginData.state.tags.delete(args.tag);
sendSuccessMessage(pluginData, msg.channel, "Tag deleted!");
return;
}
const tag = await pluginData.state.tags.find(args.tag);
if (!tag) {
sendErrorMessage(pluginData, msg.channel, "No tag with that name");
return;
}
const archiveId = await pluginData.state.archives.create(tag.body, moment().add(10, "minutes"));
const url = pluginData.state.archives.getUrl(getBaseUrl(pluginData), archiveId);
msg.channel.createMessage(`Tag source:\n${url}`);
},
});

View file

@ -0,0 +1,58 @@
import * as t from "io-ts";
import { BasePluginType, command, eventListener } from "knub";
import { tNullable, tEmbed } from "src/utils";
import { GuildArchives } from "src/data/GuildArchives";
import { GuildTags } from "src/data/GuildTags";
import { GuildSavedMessages } from "src/data/GuildSavedMessages";
import { GuildLogs } from "src/data/GuildLogs";
export const Tag = t.union([t.string, tEmbed]);
const TagCategory = t.type({
prefix: tNullable(t.string),
delete_with_command: tNullable(t.boolean),
user_tag_cooldown: tNullable(t.union([t.string, t.number])), // Per user, per tag
user_category_cooldown: tNullable(t.union([t.string, t.number])), // Per user, per tag category
global_tag_cooldown: tNullable(t.union([t.string, t.number])), // Any user, per tag
global_category_cooldown: tNullable(t.union([t.string, t.number])), // Any user, per category
tags: t.record(t.string, Tag),
can_use: tNullable(t.boolean),
});
export const ConfigSchema = t.type({
prefix: t.string,
delete_with_command: t.boolean,
user_tag_cooldown: tNullable(t.union([t.string, t.number])), // Per user, per tag
global_tag_cooldown: tNullable(t.union([t.string, t.number])), // Any user, per tag
user_cooldown: tNullable(t.union([t.string, t.number])), // Per user
global_cooldown: tNullable(t.union([t.string, t.number])), // Any tag use
categories: t.record(t.string, TagCategory),
can_create: t.boolean,
can_use: t.boolean,
can_list: t.boolean,
});
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
export interface TagsPluginType extends BasePluginType {
config: TConfigSchema;
state: {
archives: GuildArchives;
tags: GuildTags;
savedMessages: GuildSavedMessages;
logs: GuildLogs;
onMessageCreateFn;
onMessageDeleteFn;
tagFunctions: any;
};
}
export const tagsCmd = command<TagsPluginType>();
export const tagsEvent = eventListener<TagsPluginType>();

View file

@ -0,0 +1,145 @@
import { TagsPluginType } from "../types";
import { SavedMessage } from "src/data/entities/SavedMessage";
import { PluginData } from "knub";
import { resolveMember, convertDelayStringToMS, tStrictMessageContent } from "src/utils";
import escapeStringRegexp from "escape-string-regexp";
import { validate } from "src/validatorUtils";
import { LogType } from "src/data/LogType";
import { TextChannel } from "eris";
import { renderSafeTagFromMessage } from "./renderSafeTagFromMessage";
export async function onMessageCreate(pluginData: PluginData<TagsPluginType>, msg: SavedMessage) {
if (msg.is_bot) return;
if (!msg.data.content) return;
const member = await resolveMember(pluginData.client, pluginData.guild, msg.user_id);
if (!member) return;
const config = pluginData.config.getMatchingConfig({ member, channelId: msg.channel_id });
let deleteWithCommand = false;
// Find potential matching tag, looping through categories first and checking dynamic tags last
let renderedTag = null;
let matchedTagName;
const cooldowns = [];
for (const [name, category] of Object.entries(config.categories)) {
const canUse = category.can_use != null ? category.can_use : config.can_use;
if (canUse !== true) continue;
const prefix = category.prefix != null ? category.prefix : config.prefix;
if (prefix !== "" && !msg.data.content.startsWith(prefix)) continue;
const withoutPrefix = msg.data.content.slice(prefix.length);
for (const [tagName, tagBody] of Object.entries(category.tags)) {
const regex = new RegExp(`^${escapeStringRegexp(tagName)}(?:\\s|$)`);
if (regex.test(withoutPrefix)) {
renderedTag = await renderSafeTagFromMessage(
pluginData,
msg.data.content,
prefix,
tagName,
category.tags[tagName],
member,
);
if (renderedTag) {
matchedTagName = tagName;
break;
}
}
}
if (renderedTag) {
if (category.user_tag_cooldown) {
const delay = convertDelayStringToMS(String(category.user_tag_cooldown), "s");
cooldowns.push([`tags-category-${name}-user-${msg.user_id}-tag-${matchedTagName}`, delay]);
}
if (category.global_tag_cooldown) {
const delay = convertDelayStringToMS(String(category.global_tag_cooldown), "s");
cooldowns.push([`tags-category-${name}-tag-${matchedTagName}`, delay]);
}
if (category.user_category_cooldown) {
const delay = convertDelayStringToMS(String(category.user_category_cooldown), "s");
cooldowns.push([`tags-category-${name}-user--${msg.user_id}`, delay]);
}
if (category.global_category_cooldown) {
const delay = convertDelayStringToMS(String(category.global_category_cooldown), "s");
cooldowns.push([`tags-category-${name}`, delay]);
}
deleteWithCommand =
category.delete_with_command != null ? category.delete_with_command : config.delete_with_command;
break;
}
}
// Matching tag was not found from the config, try a dynamic tag
if (!renderedTag) {
if (config.can_use !== true) return;
const prefix = config.prefix;
if (!msg.data.content.startsWith(prefix)) return;
const tagNameMatch = msg.data.content.slice(prefix.length).match(/^\S+/);
if (tagNameMatch === null) return;
const tagName = tagNameMatch[0];
const tag = await pluginData.state.tags.find(tagName);
if (!tag) return;
matchedTagName = tagName;
renderedTag = await renderSafeTagFromMessage(pluginData, msg.data.content, prefix, tagName, tag.body, member);
}
if (!renderedTag) return;
if (config.user_tag_cooldown) {
const delay = convertDelayStringToMS(String(config.user_tag_cooldown), "s");
cooldowns.push([`tags-user-${msg.user_id}-tag-${matchedTagName}`, delay]);
}
if (config.global_tag_cooldown) {
const delay = convertDelayStringToMS(String(config.global_tag_cooldown), "s");
cooldowns.push([`tags-tag-${matchedTagName}`, delay]);
}
if (config.user_cooldown) {
const delay = convertDelayStringToMS(String(config.user_cooldown), "s");
cooldowns.push([`tags-user-${matchedTagName}`, delay]);
}
if (config.global_cooldown) {
const delay = convertDelayStringToMS(String(config.global_cooldown), "s");
cooldowns.push([`tags`, delay]);
}
const isOnCooldown = cooldowns.some(cd => pluginData.cooldowns.isOnCooldown(cd[0]));
if (isOnCooldown) return;
for (const cd of cooldowns) {
pluginData.cooldowns.setCooldown(cd[0], cd[1]);
}
deleteWithCommand = config.delete_with_command;
const validationError = await validate(tStrictMessageContent, renderedTag);
if (validationError) {
pluginData.state.logs.log(LogType.BOT_ALERT, {
body: `Rendering tag ${matchedTagName} resulted in an invalid message: ${validationError.message}`,
});
return;
}
const channel = pluginData.guild.channels.get(msg.channel_id) as TextChannel;
const responseMsg = await channel.createMessage(renderedTag);
// Save the command-response message pair once the message is in our database
if (deleteWithCommand) {
pluginData.state.savedMessages.onceMessageAvailable(responseMsg.id, async () => {
await pluginData.state.tags.addResponse(msg.id, responseMsg.id);
});
}
}

View file

@ -0,0 +1,32 @@
import { PluginData } from "knub";
import { TagsPluginType } from "../types";
import { SavedMessage } from "src/data/entities/SavedMessage";
import { TextChannel } from "eris";
export async function onMessageDelete(pluginData: PluginData<TagsPluginType>, msg: SavedMessage) {
// Command message was deleted -> delete the response as well
const commandMsgResponse = await pluginData.state.tags.findResponseByCommandMessageId(msg.id);
if (commandMsgResponse) {
const channel = pluginData.guild.channels.get(msg.channel_id) as TextChannel;
if (!channel) return;
const responseMsg = await pluginData.state.savedMessages.find(commandMsgResponse.response_message_id);
if (!responseMsg || responseMsg.deleted_at != null) return;
await channel.deleteMessage(commandMsgResponse.response_message_id);
return;
}
// Response was deleted -> delete the command message as well
const responseMsgResponse = await pluginData.state.tags.findResponseByResponseMessageId(msg.id);
if (responseMsgResponse) {
const channel = pluginData.guild.channels.get(msg.channel_id) as TextChannel;
if (!channel) return;
const commandMsg = await pluginData.state.savedMessages.find(responseMsgResponse.command_message_id);
if (!commandMsg || commandMsg.deleted_at != null) return;
await channel.deleteMessage(responseMsgResponse.command_message_id);
return;
}
}

View file

@ -0,0 +1,45 @@
import { Tag, TagsPluginType } from "../types";
import { Member } from "eris";
import * as t from "io-ts";
import { StrictMessageContent, stripObjectToScalars, renderRecursively } from "src/utils";
import { parseArguments } from "knub-command-manager";
import { TemplateParseError } from "src/templateFormatter";
import { PluginData } from "knub";
import { renderTag } from "./renderTag";
import { logger } from "src/logger";
export async function renderSafeTagFromMessage(
pluginData: PluginData<TagsPluginType>,
str: string,
prefix: string,
tagName: string,
tagBody: t.TypeOf<typeof Tag>,
member: Member,
): Promise<StrictMessageContent | null> {
const variableStr = str.slice(prefix.length + tagName.length).trim();
const tagArgs = parseArguments(variableStr).map(v => v.value);
const renderTagString = async _str => {
let rendered = await renderTag(pluginData, _str, tagArgs, {
member: stripObjectToScalars(member, ["user"]),
user: stripObjectToScalars(member.user),
});
rendered = rendered.trim();
return rendered;
};
// Format the string
try {
return typeof tagBody === "string"
? { content: await renderTagString(tagBody) }
: await renderRecursively(tagBody, renderTagString);
} catch (e) {
if (e instanceof TemplateParseError) {
logger.warn(`Invalid tag format!\nError: ${e.message}\nFormat: ${tagBody}`);
return null;
} else {
throw e;
}
}
}

View file

@ -0,0 +1,33 @@
import { renderTemplate } from "src/templateFormatter";
import { PluginData, plugin } from "knub";
import { TagsPluginType } from "../types";
export async function renderTag(pluginData: PluginData<TagsPluginType>, body, args = [], extraData = {}) {
const dynamicVars = {};
const maxTagFnCalls = 25;
let tagFnCalls = 0;
const data = {
args,
...extraData,
...pluginData.state.tagFunctions,
set(name, val) {
if (typeof name !== "string") return;
dynamicVars[name] = val;
},
get(name) {
return dynamicVars[name] == null ? "" : dynamicVars[name];
},
tag: async (name, ...subTagArgs) => {
if (tagFnCalls++ > maxTagFnCalls) return "\\_recursion\\_";
if (typeof name !== "string") return "";
if (name === "") return "";
// TODO: Incorporate tag categories here
const subTag = await pluginData.state.tags.find(name);
if (!subTag) return "";
return renderTemplate(subTag.body, { ...data, args: subTagArgs });
},
};
return renderTemplate(body, data);
}

View file

@ -12,6 +12,7 @@ import { PingableRolesPlugin } from "./PingableRoles/PingableRolesPlugin";
import { GuildConfigReloaderPlugin } from "./GuildConfigReloader/GuildConfigReloaderPlugin";
import { CasesPlugin } from "./Cases/CasesPlugin";
import { MutesPlugin } from "./Mutes/MutesPlugin";
import { TagsPlugin } from "./Tags/TagsPlugin";
// prettier-ignore
export const guildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
@ -22,6 +23,7 @@ export const guildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
MessageSaverPlugin,
NameHistoryPlugin,
RemindersPlugin,
TagsPlugin,
UsernameSaverPlugin,
UtilityPlugin,
WelcomeMessagePlugin,