3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-05-25 02:25:01 +00:00
This commit is contained in:
Ruby 2022-05-20 13:55:07 +00:00 committed by GitHub
commit b0053c6849
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 359 additions and 47 deletions

View file

@ -30,6 +30,7 @@ const migrationsDir = path.relative(process.cwd(), path.resolve(backendRoot, "sr
module.exports = {
type: "mysql",
host: process.env.DB_HOST,
port: 13306,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,

View file

@ -13,7 +13,7 @@
"cors": "^2.8.5",
"cross-env": "^5.2.0",
"deep-diff": "^1.0.2",
"discord-api-types": "^0.31.0",
"discord-api-types": "^0.31.2",
"discord.js": "^13.6.0",
"dotenv": "^4.0.0",
"emoji-regex": "^8.0.0",
@ -1693,8 +1693,9 @@
}
},
"node_modules/discord-api-types": {
"version": "0.31.0",
"license": "MIT"
"version": "0.31.2",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.31.2.tgz",
"integrity": "sha512-gpzXTvFVg7AjKVVJFH0oJGC0q0tO34iJGSHZNz9u3aqLxlD6LfxEs9wWVVikJqn9gra940oUTaPFizCkRDcEiA=="
},
"node_modules/discord.js": {
"version": "13.6.0",
@ -3018,8 +3019,9 @@
}
},
"node_modules/moment": {
"version": "2.24.0",
"license": "MIT",
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==",
"engines": {
"node": "*"
}
@ -6346,7 +6348,9 @@
}
},
"discord-api-types": {
"version": "0.31.0"
"version": "0.31.2",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.31.2.tgz",
"integrity": "sha512-gpzXTvFVg7AjKVVJFH0oJGC0q0tO34iJGSHZNz9u3aqLxlD6LfxEs9wWVVikJqn9gra940oUTaPFizCkRDcEiA=="
},
"discord.js": {
"version": "13.6.0",
@ -6425,7 +6429,7 @@
},
"erlpack": {
"version": "git+ssh://git@github.com/discord/erlpack.git#3b793a333dd3f6a140b9168ea91e9fa9660753ce",
"from": "erlpack@github:discord/erlpack.git",
"from": "erlpack@github:discord/erlpack",
"requires": {
"bindings": "^1.5.0",
"nan": "^2.15.0"
@ -7175,7 +7179,9 @@
}
},
"moment": {
"version": "2.24.0"
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw=="
},
"moment-timezone": {
"version": "0.5.27",

View file

@ -28,7 +28,7 @@
"cors": "^2.8.5",
"cross-env": "^5.2.0",
"deep-diff": "^1.0.2",
"discord-api-types": "^0.31.0",
"discord-api-types": "^0.31.2",
"discord.js": "^13.6.0",
"dotenv": "^4.0.0",
"emoji-regex": "^8.0.0",

View file

@ -0,0 +1,66 @@
import { getRepository, Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { TagAlias } from "./entities/TagAlias";
export class GuildTagAliases extends BaseGuildRepository {
private tagAliases: Repository<TagAlias>;
constructor(guildId) {
super(guildId);
this.tagAliases = getRepository(TagAlias);
}
async all(): Promise<TagAlias[]> {
return this.tagAliases.find({
where: {
guild_id: this.guildId,
},
});
}
async find(alias): Promise<TagAlias | undefined> {
return this.tagAliases.findOne({
where: {
guild_id: this.guildId,
alias,
},
});
}
async findAllWithTag(tag): Promise<TagAlias[] | undefined> {
const all = await this.all();
const aliases = all.filter((a) => a.tag === tag);
return aliases.length > 0 ? aliases : undefined;
}
async createOrUpdate(alias, tag, userId) {
const existingTagAlias = await this.find(alias);
if (existingTagAlias) {
await this.tagAliases
.createQueryBuilder()
.update()
.set({
tag,
user_id: userId,
created_at: () => "NOW()",
})
.where("guild_id = :guildId", { guildId: this.guildId })
.andWhere("alias = :alias", { alias })
.execute();
} else {
await this.tagAliases.insert({
guild_id: this.guildId,
user_id: userId,
alias,
tag,
});
}
}
async delete(alias) {
await this.tagAliases.delete({
guild_id: this.guildId,
alias,
});
}
}

View file

@ -0,0 +1,20 @@
import { Column, Entity, PrimaryColumn } from "typeorm";
@Entity("tag_aliases")
export class TagAlias {
@Column()
@PrimaryColumn()
guild_id: string;
@Column()
@PrimaryColumn()
alias: string;
@Column()
@PrimaryColumn()
tag: string;
@Column() user_id: string;
@Column() created_at: string;
}

View file

@ -0,0 +1,43 @@
import { MigrationInterface, QueryRunner, Table } from "typeorm";
export class CreateTagAliasesTable1650721595278 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: "tag_aliases",
columns: [
{
name: "guild_id",
type: "bigint",
isPrimary: true,
},
{
name: "alias",
type: "varchar",
length: "255",
isPrimary: true,
},
{
name: "tag",
type: "varchar",
length: "255",
isPrimary: true,
},
{
name: "user_id",
type: "bigint",
},
{
name: "created_at",
type: "datetime",
default: "now()",
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
queryRunner.dropTable("tag_aliases");
}
}

View file

@ -7,6 +7,7 @@ import { GuildArchives } from "../../data/GuildArchives";
import { GuildLogs } from "../../data/GuildLogs";
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
import { GuildTags } from "../../data/GuildTags";
import { GuildTagAliases } from "src/data/GuildTagAliases";
import { mapToPublicFn } from "../../pluginUtils";
import { convertDelayStringToMS, trimPluginDescription } from "../../utils";
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
@ -68,7 +69,7 @@ export const TagsPlugin = zeppelinGuildPlugin<TagsPluginType>()({
You use them by adding a \`{}\` on your tag.
Here are the functions you can use in your tags:
${generateTemplateMarkdown(TemplateFunctions)}
`),
},
@ -123,6 +124,7 @@ export const TagsPlugin = zeppelinGuildPlugin<TagsPluginType>()({
state.archives = GuildArchives.getGuildInstance(guild.id);
state.tags = GuildTags.getGuildInstance(guild.id);
state.tagAliases = GuildTagAliases.getGuildInstance(guild.id);
state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id);
state.logs = new GuildLogs(guild.id);

View file

@ -8,11 +8,24 @@ export const TagCreateCmd = tagsCmd({
permission: "can_create",
signature: {
alias: ct.bool({ option: true, shortcut: "a", isSwitch: true }),
tag: ct.string(),
body: ct.string({ catchAll: true }),
},
async run({ message: msg, args, pluginData }) {
const prefix = pluginData.config.get().prefix;
if (args.alias) {
const existingTag = await pluginData.state.tagAliases.find(args.body);
if (existingTag) {
sendErrorMessage(pluginData, msg.channel, `You cannot create an alias of an alias`);
return;
}
await pluginData.state.tagAliases.createOrUpdate(args.tag, args.body, msg.author.id);
sendSuccessMessage(pluginData, msg.channel, `Alias set! Use it with: \`${prefix}${args.tag}\``);
return;
}
try {
parseTemplate(args.body);
} catch (e) {
@ -26,7 +39,6 @@ export const TagCreateCmd = tagsCmd({
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

@ -11,13 +11,25 @@ export const TagDeleteCmd = tagsCmd({
},
async run({ message: msg, args, pluginData }) {
const alias = await pluginData.state.tagAliases.find(args.tag);
const tag = await pluginData.state.tags.find(args.tag);
if (!tag) {
if (!tag && !alias) {
sendErrorMessage(pluginData, msg.channel, "No tag with that name");
return;
}
await pluginData.state.tags.delete(args.tag);
sendSuccessMessage(pluginData, msg.channel, "Tag deleted!");
if (tag) {
const aliasesOfTag = await pluginData.state.tagAliases.findAllWithTag(tag?.tag);
if (aliasesOfTag) {
// tslint:disable-next-line:no-shadowed-variable
aliasesOfTag.forEach((alias) => pluginData.state.tagAliases.delete(alias.alias));
}
await pluginData.state.tags.delete(args.tag);
} else {
await pluginData.state.tagAliases.delete(alias?.alias);
}
sendSuccessMessage(pluginData, msg.channel, `${tag ? "Tag" : "Alias"} deleted!`);
},
});

View file

@ -1,3 +1,6 @@
import { sendErrorMessage } from "src/pluginUtils";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { createChunkedMessage } from "../../../utils";
import { tagsCmd } from "../types";
@ -5,16 +8,61 @@ export const TagListCmd = tagsCmd({
trigger: ["tag list", "tags", "taglist"],
permission: "can_list",
async run({ message: msg, pluginData }) {
signature: {
noaliases: ct.bool({ option: true, isSwitch: true, shortcut: "na" }),
aliasesonly: ct.bool({ option: true, isSwitch: true, shortcut: "ao" }),
tag: ct.string({ option: true }),
},
async run({ message: msg, args, pluginData }) {
const prefix = (await pluginData.config.getForMessage(msg)).prefix;
const tags = await pluginData.state.tags.all();
if (tags.length === 0) {
msg.channel.send(`No tags created yet! Use \`tag create\` command to create one.`);
return;
}
const prefix = (await pluginData.config.getForMessage(msg)).prefix;
const tagNames = tags.map((tag) => tag.tag).sort();
const allAliases = await pluginData.state.tagAliases.all();
createChunkedMessage(msg.channel, `Available tags (use with ${prefix}tag): \`\`\`${tagNames.join(", ")}\`\`\``);
if (args.aliasesonly) {
let aliasesArr: string[] = [];
if (args.tag) {
const tag = await pluginData.state.tags.find(args.tag);
if (!tag) {
sendErrorMessage(pluginData, msg.channel, `Tag \`${args.tag}\` doesn't exist.`);
return;
}
const aliasesForTag = await pluginData.state.tagAliases.findAllWithTag(args.tag);
if (!aliasesForTag) {
sendErrorMessage(pluginData, msg.channel, `No aliases for tag \`${args.tag}\`.`);
return;
}
aliasesArr = aliasesForTag.map((a) => a.alias);
createChunkedMessage(
msg.channel,
`Available aliases for tag \`${args.tag}\` (use with \`${prefix}alias\`: \`\`\`${aliasesArr.join(
", ",
)}\`\`\``,
);
return;
}
aliasesArr = allAliases.map((a) => a.alias);
createChunkedMessage(
msg.channel,
`Available aliases (use with \`${prefix}alias\`: \`\`\`${aliasesArr.join(", ")}\`\`\``,
);
return;
}
const tagNames = tags.map((tag) => tag.tag).sort();
const tagAliasesNames = allAliases.map((alias) => alias.alias).sort();
const tagAndAliasesNames = tagNames
.join(", ")
.concat(args.noaliases ? "" : tagAliasesNames.length > 0 ? `, ${tagAliasesNames.join(", ")}` : "");
createChunkedMessage(
msg.channel,
`Available tags (use with ${prefix}tag/alias): \`\`\`${tagAndAliasesNames}\`\`\``,
);
},
});

View file

@ -14,24 +14,42 @@ export const TagSourceCmd = tagsCmd({
},
async run({ message: msg, args, pluginData }) {
const alias = await pluginData.state.tagAliases.find(args.tag);
const aliasedTag = await pluginData.state.tags.find(alias?.tag ?? null);
const tag = (await pluginData.state.tags.find(args.tag)) || aliasedTag;
if (args.delete) {
const actualTag = await pluginData.state.tags.find(args.tag);
if (!actualTag) {
if (!actualTag && !aliasedTag) {
sendErrorMessage(pluginData, msg.channel, "No tag with that name");
return;
}
await pluginData.state.tags.delete(args.tag);
sendSuccessMessage(pluginData, msg.channel, "Tag deleted!");
if (actualTag) {
const aliasesOfTag = await pluginData.state.tagAliases.findAllWithTag(actualTag?.tag);
if (aliasesOfTag) {
// tslint:disable-next-line:no-shadowed-variable
aliasesOfTag.forEach((alias) => pluginData.state.tagAliases.delete(alias.alias));
}
await pluginData.state.tags.delete(args.tag);
} else {
await pluginData.state.tagAliases.delete(alias?.alias);
}
sendSuccessMessage(pluginData, msg.channel, `${actualTag ? "Tag" : "Alias"} deleted!`);
return;
}
const tag = await pluginData.state.tags.find(args.tag);
if (!tag) {
if (!tag && !aliasedTag) {
sendErrorMessage(pluginData, msg.channel, "No tag with that name");
return;
}
if (!tag?.body) {
return;
}
const archiveId = await pluginData.state.archives.create(tag.body, moment.utc().add(10, "minutes"));
const url = pluginData.state.archives.getUrl(getBaseUrl(pluginData), archiveId);

View file

@ -4,6 +4,7 @@ import { GuildArchives } from "../../data/GuildArchives";
import { GuildLogs } from "../../data/GuildLogs";
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
import { GuildTags } from "../../data/GuildTags";
import { GuildTagAliases } from "../../data/GuildTagAliases";
import { tEmbed, tNullable } from "../../utils";
export const Tag = t.union([t.string, tEmbed]);
@ -50,6 +51,7 @@ export interface TagsPluginType extends BasePluginType {
state: {
archives: GuildArchives;
tags: GuildTags;
tagAliases: GuildTagAliases;
savedMessages: GuildSavedMessages;
logs: GuildLogs;

View file

@ -20,8 +20,18 @@ export async function findTagByName(
return config.categories[categoryName]?.tags[tagName] ?? null;
}
let tag: string | null;
// Dynamic tag
// Format: "tag"
const dynamicTag = await pluginData.state.tags.find(name);
return dynamicTag?.body ?? null;
tag = dynamicTag?.body ?? null;
// Aliased tag
// Format: "alias"
const aliasedTagName = await pluginData.state.tagAliases.find(name);
const aliasedTag = await pluginData.state.tags.find(aliasedTagName?.tag);
tag ? (tag = tag) : (tag = aliasedTag?.body ?? null);
return tag;
}

View file

@ -0,0 +1,30 @@
// https://rosettacode.org/wiki/Levenshtein_distance#JavaScript
function levenshtein(a: string, b: string): number {
let t: number[] = [];
let u: number[] = [];
const m = a.length;
const n = b.length;
if (!m) {
return n;
}
if (!n) {
return m;
}
for (let j = 0; j <= n; j++) {
t[j] = j;
}
for (let i = 1; i <= m; i++) {
let j: number;
// tslint:disable-next-line:ban-comma-operator
for (u = [i], j = 1; j <= n; j++) {
u[j] = a[i - 1] === b[j - 1] ? t[j - 1] : Math.min(t[j - 1], t[j], u[j - 1]) + 1;
}
t = u;
}
return u[n];
}
export function distance(str: string, t: string): number {
return levenshtein(str, t);
}

View file

@ -1,9 +1,12 @@
import { GuildMember } from "discord.js";
import { GuildMember, Snowflake, TextChannel } from "discord.js";
import escapeStringRegexp from "escape-string-regexp";
import { GuildPluginData } from "knub";
import { ExtendedMatchParams } from "knub/dist/config/PluginConfigManager";
import { StrictMessageContent } from "../../../utils";
import { TagsPluginType, TTagCategory } from "../types";
import { distance } from "./fuzzySearch";
import { renderTagFromString } from "./renderTagFromString";
interface BaseResult {
@ -44,7 +47,8 @@ export async function matchAndRenderTagFromString(
const withoutPrefix = str.slice(prefix.length);
for (const [tagName, tagBody] of Object.entries(category.tags)) {
// tslint:disable-next-line:no-shadowed-variable
for (const [tagName, _tagBody] of Object.entries(category.tags)) {
const regex = new RegExp(`^${escapeStringRegexp(tagName)}(?:\\s|$)`);
if (regex.test(withoutPrefix)) {
const renderedContent = await renderTagFromString(
@ -70,43 +74,79 @@ export async function matchAndRenderTagFromString(
}
}
// Dynamic tags
// Dynamic + Aliased tags
if (config.can_use !== true) {
return null;
}
const dynamicTagPrefix = config.prefix;
if (!str.startsWith(dynamicTagPrefix)) {
const tagPrefix = config.prefix;
if (!str.startsWith(tagPrefix)) {
return null;
}
const dynamicTagNameMatch = str.slice(dynamicTagPrefix.length).match(/^\S+/);
if (dynamicTagNameMatch === null) {
const tagNameMatch = str.slice(tagPrefix.length).match(/^\S+/);
if (tagNameMatch == null) {
return null;
}
const dynamicTagName = dynamicTagNameMatch[0];
const dynamicTag = await pluginData.state.tags.find(dynamicTagName);
if (!dynamicTag) {
const tagName = tagNameMatch[0];
const aliasName = await pluginData.state.tagAliases.find(tagName);
const aliasedTag = await pluginData.state.tags.find(aliasName?.tag);
const dynamicTag = await pluginData.state.tags.find(tagName);
if (!aliasedTag && !dynamicTag) {
// fuzzy search the list of aliases and tags to see if there's a match and
// inform the user
const tags = await pluginData.state.tags.all();
const aliases = await pluginData.state.tagAliases.all();
let lowest: [number, [string]] = [999999, [""]];
tags.forEach((tag) => {
const tagname = tag?.tag;
const dist = distance(tagname, tagName);
if (dist < lowest[0]) {
lowest = [dist, [`**${tagname}**`]];
} else if (dist === lowest[0]) {
lowest[1].push(`**${tagname}**`);
}
});
aliases.forEach((alias) => {
const aliasname = alias?.alias;
const dist = distance(aliasname, tagName);
if (dist < lowest[0]) {
lowest = [dist, [`**${aliasname}**`]];
} else if (dist === lowest[0]) {
lowest[1].push(`**${aliasname}**`);
}
});
if (lowest[0] > 6) return null;
const content: StrictMessageContent = {
content: `Did you mean:\n${lowest[1].join("\n")}`,
};
return {
renderedContent: content,
tagName: "",
category: null,
categoryName: null,
};
}
const tagBody = aliasedTag?.body ?? dynamicTag?.body;
if (!tagBody) {
return null;
}
const renderedDynamicTagContent = await renderTagFromString(
pluginData,
str,
dynamicTagPrefix,
dynamicTagName,
dynamicTag.body,
member,
);
const renderedTagContent = await renderTagFromString(pluginData, str, tagPrefix, tagName, tagBody, member);
if (renderedDynamicTagContent == null) {
if (renderedTagContent == null) {
return null;
}
return {
renderedContent: renderedDynamicTagContent,
tagName: dynamicTagName,
renderedContent: renderedTagContent,
tagName,
categoryName: null,
category: null,
};

View file

@ -1,14 +1,16 @@
import { Snowflake, TextChannel } from "discord.js";
import { GuildPluginData } from "knub";
import { erisAllowedMentionsToDjsMentionOptions } from "src/utils/erisAllowedMentionsToDjsMentionOptions";
import { SavedMessage } from "../../../data/entities/SavedMessage";
import { LogType } from "../../../data/LogType";
import { convertDelayStringToMS, resolveMember, tStrictMessageContent } from "../../../utils";
import { messageIsEmpty } from "../../../utils/messageIsEmpty";
import { validate } from "../../../validatorUtils";
import { TagsPluginType } from "../types";
import { matchAndRenderTagFromString } from "./matchAndRenderTagFromString";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { TagsPluginType } from "../types";
import { matchAndRenderTagFromString } from "./matchAndRenderTagFromString";
export async function onMessageCreate(pluginData: GuildPluginData<TagsPluginType>, msg: SavedMessage) {
if (msg.is_bot) return;