Skip to content

Commit

Permalink
feat(chatInput): add NG word filter functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
gx1285 committed Sep 29, 2024
1 parent 613b4d8 commit 4a4fa61
Show file tree
Hide file tree
Showing 14 changed files with 380 additions and 27 deletions.
143 changes: 143 additions & 0 deletions refactor/commands/chatInput/ngword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
ApplicationIntegrationType,
AutoModerationActionType,
AutoModerationRuleEventType,
AutoModerationRuleTriggerType,
ChatInputCommandInteraction,
InteractionContextType,
PermissionFlagsBits,
SlashCommandBuilder,
type SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js';
import { constants } from '../../config/constants.js';
import { type ChatInputCommand } from '../../core/types/ChatInputCommand.js';
import { type CommandSetting } from '../../core/types/CommandSetting.js';
import { GuildSetting } from '../../database/entities/GuildSetting.js';
import { GuildSettingManager } from '../../utils/GuildSettingManager.js';
import { userFormat } from '../../utils/userFormat.js';
type Rule = {
name: string;
value: 'invite-link' | 'token' | 'mention' | 'email';
};
export default class NgWord implements ChatInputCommand {
public command: SlashCommandBuilder | SlashCommandSubcommandsOnlyBuilder;
public settings: CommandSetting;
get rules(): Rule[] {
return [
{ name: '招待リンク(おすすめ)', value: 'invite-link' },
{ name: 'トークン(おすすめ)', value: 'token' },
{ name: '全員メンション', value: 'mention' },
{ name: 'メールアドレス', value: 'email' },
];
}
getRuleRegex(ruleName: 'invite-link' | 'token' | 'mention' | 'email') {
const data = {
'invite-link': [
constants.regexs.inviteUrls.dicoall,
constants.regexs.inviteUrls.disboard,
constants.regexs.inviteUrls.discoparty,
constants.regexs.inviteUrls.discord,
constants.regexs.inviteUrls.discordCafe,
constants.regexs.inviteUrls.dissoku,
constants.regexs.inviteUrls.sabach,
],
token: [constants.regexs.discordToken],
mention: [constants.regexs.mention],
email: [constants.regexs.email],
};
return data[ruleName].map((regex) => `${regex}`);
}
constructor() {
this.command = new SlashCommandBuilder()
.setName('ngword')
.setDescription('NGワード関連コマンド')
.addSubcommand((input) =>
input
.setName('add')
.setDescription('AutoModにAquedが提供するルールを追加します')
.addStringOption((input) =>
input.setName('rule').setDescription('ルール').addChoices(this.rules).setRequired(true),
),
)
.addSubcommand((input) =>
input
.setName('remove')
.setDescription('AutoModからAquedが提供するルールを削除します')
.addStringOption((input) =>
input.setName('rule').setDescription('ルール').addChoices(this.rules).setRequired(true),
),
)
.setContexts(InteractionContextType.Guild)
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);

this.settings = { enable: true, permissions: [PermissionFlagsBits.ManageGuild] };
}
isValidValue(value: string): value is Rule['value'] {
return this.rules.some((rule) => rule.value === value);
}

async run(interaction: ChatInputCommandInteraction) {
const commandName = interaction.options.getSubcommand();
const ruleName = interaction.options.getString('rule');
if (!ruleName || !commandName || !interaction.inCachedGuild()) return;
if (!this.isValidValue(ruleName)) {
return await interaction.reply({ content: 'ルールが存在しません。', ephemeral: true });
}

const settings = new GuildSettingManager(interaction.guildId);
const setting = (await settings.getSetting()) ?? new GuildSetting(interaction.guildId);
switch (commandName) {
case 'add': {
const rule = await interaction.guild.autoModerationRules.create({
name: `${ruleName} By Aqued`,
eventType: AutoModerationRuleEventType.MessageSend,
triggerType: AutoModerationRuleTriggerType.Keyword,
triggerMetadata: {
regexPatterns: this.getRuleRegex(ruleName).map((value) => String(value)),
},
actions: [{ type: AutoModerationActionType.BlockMessage }],
enabled: true,
reason: `${userFormat(interaction.member)}によって作成されました。`,
});

const autoMods: string[] = setting.autoMods ?? [];
autoMods.push(rule.id);
await settings.setSetting({ autoMods });
return await interaction.reply('登録しました!');
}

case 'remove': {
if (!setting.autoMods || setting.autoMods.length === 0) {
return await interaction.reply({ content: 'AutoModがAquedによって登録されていません', ephemeral: true });
}

let removedCount = 0;
for (const id of setting.autoMods) {
const rule = interaction.guild.autoModerationRules.cache.find(
(value) => value.id === id && value.name === `${ruleName} By Aqued`,
);

if (rule) {
await rule.delete(`${userFormat(interaction.member)}によって${rule.name}が削除されました`);
const index = setting.autoMods.indexOf(id);
if (index !== -1) {
setting.autoMods.splice(index, 1);
removedCount++;
}
}
}

if (removedCount > 0) {
await settings.setSetting({ autoMods: setting.autoMods });
return await interaction.reply(`削除しました (${removedCount} 件のルール)。`);
} else {
return await interaction.reply({ content: '削除するルールが見つかりませんでした。', ephemeral: true });
}
}

default:
return;
}
}
}
6 changes: 4 additions & 2 deletions refactor/commands/chatInput/ping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ export default class Ping implements ChatInputCommand {
public settings: CommandSetting;
constructor() {
this.command = new SlashCommandBuilder().setName('ping').setDescription('ping!!!');
this.settings = {};
this.settings = { enable: true };
}
async run(interaction: ChatInputCommandInteraction) {
await interaction.reply({
embeds: [new EmbedBuilder().setTitle(':ping_pong: | Pong!').setDescription(`\`${interaction.client.ws.ping} ms\``)],
embeds: [
new EmbedBuilder().setTitle(':ping_pong: | Pong!').setDescription(`\`${interaction.client.ws.ping} ms\``),
],
});
}
}
38 changes: 38 additions & 0 deletions refactor/commands/chatInput/top.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ChatInputCommandInteraction, EmbedBuilder, InteractionContextType, SlashCommandBuilder } from 'discord.js';
import { inspect } from 'util';
import { Logger } from '../../core/Logger.js';
import { type ChatInputCommand } from '../../core/types/ChatInputCommand.js';
import { type CommandSetting } from '../../core/types/CommandSetting.js';

export default class Top implements ChatInputCommand {
public command: SlashCommandBuilder;
public settings: CommandSetting;
constructor() {
this.command = new SlashCommandBuilder()
.setName('top')
.setDescription('top!!!')
.setContexts(InteractionContextType.Guild);
this.settings = { enable: true };
}
async run(interaction: ChatInputCommandInteraction) {
try {
if (!interaction.channel) return;
const messages = await interaction.channel.messages.fetch({ after: '0', limit: 1 });
const message = messages.first();
if (message) {
await interaction.reply({
embeds: [new EmbedBuilder().setDescription(`[**一番上のメッセージへジャンプ!**](${message.url})`)],
});
} else {
await interaction.reply({
embeds: [new EmbedBuilder().setAuthor({ name: 'メッセージの取得に失敗' })],
});
}
} catch (error) {
Logger.error(inspect(error));
await interaction.reply({
embeds: [new EmbedBuilder().setAuthor({ name: 'メッセージの取得に失敗' })],
});
}
}
}
14 changes: 14 additions & 0 deletions refactor/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,18 @@ export const constants = {
},
loggerThreadId: '',
},
regexs: {
inviteUrls: {
dissoku: /dissoku\.net/g,
disboard: /disboard\.org/g,
discoparty: /discoparty\.jp/g,
discord: /(https?:\/\/)?(www\.)?(discord\.(gg|com|net)|discordapp\.(com|net)\/invite)\/[\dA-Za-z]+/g,
discordCafe: /discordcafe\.app/g,
dicoall: /dicoall\.com/g,
sabach: /sabach\.jp/g,
},
mention: /@everyone|@here/,
discordToken: /^(mfa\.[a-z0-9_-]{20,})|([a-z0-9_-]{23,28}\.[a-z0-9_-]{6,7}\.[a-z0-9_-]{27})$/gm,
email: /[^@\s]+@[^@\s]+\.[^@\s]+/,
},
};
4 changes: 4 additions & 0 deletions refactor/core/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ActivityType, Client, GatewayIntentBits, SnowflakeUtil } from 'discord.
import { config } from '../config/config.js';
import { CommandLoader } from './CommandLoader.js';
import { EventLoader } from './EventLoader.js';
import { dataSource } from './typeorm.config.js';
export const client = new Client({
intents: [
GatewayIntentBits.Guilds,
Expand All @@ -22,6 +23,7 @@ declare module 'discord.js' {
config: typeof config;
commands: { chatInput: CommandLoader };
readyId: string;
cooldown: Map<string, Map<string, number>>;
};
}
}
Expand All @@ -32,6 +34,8 @@ client.aqued = {
chatInput: new CommandLoader('commands/chatInput'),
},
readyId: SnowflakeUtil.generate().toString(),
cooldown: new Map(),
};

await client.aqued.events.loadAllEvents();
await dataSource.initialize();
16 changes: 16 additions & 0 deletions refactor/core/typeorm.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { config } from '../config/config.js';
import { entities } from '../database/entities/index.js';

export const dataSource = new DataSource({
type: 'mysql',
host: config.mysql.host,
username: config.mysql.user,
password: config.mysql.password,
port: config.mysql.port,
database: 'aqued',
synchronize: true, // for "develop"
dropSchema: true, // for "develop"
entities: entities,
});
6 changes: 4 additions & 2 deletions refactor/core/types/ChatInputCommand.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
import { ChatInputCommandInteraction, SlashCommandBuilder, type SlashCommandSubcommandsOnlyBuilder } from 'discord.js';
import type { CommandSetting } from './CommandSetting.js';

export interface ChatInputCommand {
command: SlashCommandBuilder;
command: SlashCommandBuilder | SlashCommandSubcommandsOnlyBuilder;
settings: CommandSetting;
run(interaction: ChatInputCommandInteraction): Promise<unknown>;
}
18 changes: 1 addition & 17 deletions refactor/core/types/CommandSetting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import { ChannelType } from 'discord.js';
*/
export interface CommandSetting {
/**
* 実行前に評価する関数
*/
check?: () => boolean;
/**
* 必須権限(実行時にもチェックします)
* 必須権限(実行時にBotと実行ユーザー側でチェックします)
*/
permissions?: bigint[];
/**
Expand All @@ -31,16 +27,4 @@ export interface CommandSetting {
* 実行できるチャンネルのタイプ
*/
channelTypes?: ChannelType[];
/**
* クールダウン
*/
cooldown?: number;
/**
* クールダウンの対象
*/
cooldownTargets?: 'guild' | 'user' | 'channel' | 'bot';
/**
* クールダウンになるまでの回数
*/
cooldownLimit?: number;
}
13 changes: 13 additions & 0 deletions refactor/database/entities/GuildSetting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Column, Entity, PrimaryColumn } from 'typeorm';

@Entity({ name: 'GUILD_SETTING' })
export class GuildSetting {
@PrimaryColumn({ name: 'GUILD_ID', type: 'bigint', comment: 'ギルドID' })
guildId: string;
@Column({ name: 'AUTO_MODS', type: 'simple-array', comment: 'Aquedにより設定されたAutoModのId配列', nullable: true })
autoMods?: string[];

constructor(guildId: string) {
this.guildId = guildId;
}
}
3 changes: 3 additions & 0 deletions refactor/database/entities/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { GuildSetting } from './GuildSetting.js';

export const entities = [GuildSetting];
Loading

0 comments on commit 4a4fa61

Please sign in to comment.