diff --git a/package.json b/package.json index 2c36e9d..a64bc6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "knightrider", - "version": "4.8.434", + "version": "4.9.000", "description": "a bot for a private discord server", "main": "./dist/index.js", "scripts": { diff --git a/src/bot/commands/banreaction.ts b/src/bot/commands/banreaction.ts new file mode 100644 index 0000000..90c60ca --- /dev/null +++ b/src/bot/commands/banreaction.ts @@ -0,0 +1,53 @@ +import { Client, ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; + + +import YALAS from 'mcstatusbot-logger'; + +import * as AddBanReaction from './banreaction/add'; +import * as RemoveBanReaction from './banreaction/remove'; +import { GuildInstance } from "../../database/schemas/Guild"; +import { UserInstance } from "../../database/schemas/User"; + +const data = { + allowSuspendedUserAccess: false, + command: new SlashCommandBuilder() + .setName('banreaction') + .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers) + .setDescription("setup reaction to ban members from your server") + .addSubcommand(subcommand => + subcommand + .setName('add') + .setDescription("Add a ban reaction to your channel") + .addChannelOption(option => + option.setName('channel') + .setDescription('The channel in which you want to add a reaction') + .setRequired(true) + ) + + ) + .addSubcommand(subcommand => + subcommand + .setName('remove') + .setDescription("remove a ban reaction") + .addChannelOption(option => + option.setName('channel') + .setDescription('The channel in which you want to remove a reaction') + .setRequired(true) + ) + + ) +} + +export { data }; + + +export async function chatInputCommand(client: Client, interaction: ChatInputCommandInteraction, guild: GuildInstance, user: UserInstance) { + switch (interaction.options.getSubcommand()) { + case 'add': + AddBanReaction.chatInputCommand(client, interaction, guild, user) + break; + case 'remove': + RemoveBanReaction.chatInputCommand(client, interaction, guild, user) + break; + } +} \ No newline at end of file diff --git a/src/bot/commands/banreaction/add.ts b/src/bot/commands/banreaction/add.ts new file mode 100644 index 0000000..d31d69c --- /dev/null +++ b/src/bot/commands/banreaction/add.ts @@ -0,0 +1,116 @@ +import { Client, ChatInputCommandInteraction, GuildChannelResolvable, PermissionsBitField, ChannelType, EmbedBuilder } from "discord.js"; + +import { schemas } from "../../../database"; + +import YALAS from 'mcstatusbot-logger'; + +import { GuildInstance } from "../../../database/schemas/Guild"; +import { UserInstance } from "../../../database/schemas/User"; + + +export async function chatInputCommand(client: Client, interaction: ChatInputCommandInteraction, guild: GuildInstance, user: UserInstance) { + if (!interaction.guild) return interaction.reply("must be done in discord server"); + + const botMember = interaction.guild.members.me; + if (!botMember) { + YALAS.error("reactionrole add: bot in guild null"); + return; + } + + + //get and validate channel + const channel = interaction.options.getChannel('channel'); + if (channel == undefined) { + return interaction.reply({ + embeds: [{ + title: "Channel Not Found", + description: "The selected channel could not be found please try again." + }] + }); + } + + if (channel.type !== ChannelType.GuildText) { + return interaction.reply({ + embeds: [{ + title: "That Will Not Work", + description: "The selected channel is not a text channel please select a text channel." + }] + }); + } + + const botPermSM = botMember.permissionsIn((channel as GuildChannelResolvable)).toArray(); + if (botPermSM && !botPermSM.includes('SendMessages')) { + return interaction.reply({ + embeds: [{ + title: "Missing permission", + description: "you need to give me the send message permission in the selected channel." + }] + }); + } + + + //check bot has manage roles perm + const botPermMR = botMember.permissions.has(PermissionsBitField.Flags.BanMembers); + if (!botPermMR) { + return interaction.reply({ + embeds: [{ + title: "Missing permission", + description: "you need to give me the Ban Members permission in this server." + }] + }); + } + + + await interaction.deferReply(); + //check emoji does net exist for reaction in channel + try { + const BanReaction = await schemas['BanReaction'].findOne({ + where: { + guild: guild.id, + channel: channel.id + } + }); + + if (BanReaction !== null) { + return interaction.editReply({ + embeds: [{ + title: "Ban Reaction Exists", + description: "This is already setup in selected channel." + }] + }); + } + } catch (err: any) { + YALAS.error(err) + YALAS.error(err.stack || err); + return interaction.editReply("error when checking for existing ban reaction in database."); + } + + try { + await schemas['BanReaction'].create({ + guild: guild.id, + channel: channel.id + }); + } catch (err: any) { + YALAS.error(err.stack || err); + return interaction.editReply("error when saving ban reaction to database."); + } + + + try { + const textChannel = interaction.guild?.channels.cache.get(channel.id); + if (textChannel === undefined || textChannel.type !== 0) return; + + const embed = new EmbedBuilder() + .setDescription("React with a ❌") + .setFooter({ text: 'to get banned this is no joke' }); + + + const BanReactionMsg = await textChannel.send({ embeds: [embed] }); + await BanReactionMsg.react('❌'); + return interaction.editReply("ban reaction added to channel <#" + textChannel.id + ">."); + } catch (err: any) { + YALAS.error(err.stack || err); + return interaction.editReply("error when making ban reaction embed."); + } + +} \ No newline at end of file diff --git a/src/bot/commands/banreaction/remove.ts b/src/bot/commands/banreaction/remove.ts new file mode 100644 index 0000000..534001e --- /dev/null +++ b/src/bot/commands/banreaction/remove.ts @@ -0,0 +1,67 @@ +import { GuildInstance } from "../../../database/schemas/Guild"; +import { UserInstance } from "../../../database/schemas/User"; + +import { Client, ChatInputCommandInteraction, Role, GuildChannelResolvable, TextChannel, ChannelType } from "discord.js"; + +import parseEmoji from "../../functions/parseEmoji"; +import { schemas } from "../../../database"; + +import YALAS from 'mcstatusbot-logger'; + +import SendReactionRoleEmbed from "../../functions/SendReactionRoleEmbed"; + + + +export async function chatInputCommand(client: Client, interaction: ChatInputCommandInteraction, guild: GuildInstance, user: UserInstance) { + if (!interaction.guild) return interaction.reply("must be done in discord server"); + + //get and validate channel + const channel = interaction.options.getChannel('channel'); + if (channel == undefined) { + return interaction.reply({ + embeds: [{ + title: "Channel Not Found", + description: "The selected channel could not be found please try again." + }] + }); + } + + if (channel.type !== ChannelType.GuildText) { + return interaction.reply({ + embeds: [{ + title: "That Will Not Work", + description: "The selected channel is not a text channel please select a text channel." + }] + }); + } + + + + + await interaction.deferReply(); + try { + const BanReaction = await schemas['BanReaction'].findOne({ + where: { + guild: guild.id, + channel: channel.id, + }, + raw: true + }); + + if (BanReaction == null) { + return interaction.editReply({ + embeds: [{ + title: "Ban Role Doesn't Exists", + description: "This ban reaction could not be found in the selected channel." + }] + }); + } + await schemas['BanReaction'].destroy({ where: { id: BanReaction.id } }); + return interaction.editReply("ban reaction deleted."); + } catch (err: any) { + YALAS.error(err) + YALAS.error(err.stack || err); + return interaction.editReply("error when getting and deleting reaction role in database."); + } + +} \ No newline at end of file diff --git a/src/bot/commands/reactionrole/add.ts b/src/bot/commands/reactionrole/add.ts index accd6c1..0952599 100644 --- a/src/bot/commands/reactionrole/add.ts +++ b/src/bot/commands/reactionrole/add.ts @@ -1,4 +1,4 @@ -import { Client, ChatInputCommandInteraction, Role, GuildChannelResolvable, PermissionsBitField } from "discord.js"; +import { Client, ChatInputCommandInteraction, Role, GuildChannelResolvable, PermissionsBitField, ChannelType } from "discord.js"; import parseEmoji from "../../functions/parseEmoji"; import { schemas } from "../../../database"; @@ -31,7 +31,7 @@ export async function chatInputCommand(client: Client, interaction: ChatInputCom }); } - if (channel.type !== 0) { + if (channel.type !== ChannelType.GuildText) { return interaction.reply({ embeds: [{ title: "That Will Not Work", @@ -75,6 +75,15 @@ export async function chatInputCommand(client: Client, interaction: ChatInputCom }); } + if (cleanEmoji == '❌') { + return interaction.reply({ + embeds: [{ + title: "Invalid Emoji", + description: "The selected emoji is used for the ban reaction to avoid confusion you can not use it." + }] + }); + } + //get and validate role const role = interaction.options.getMentionable('role'); @@ -146,6 +155,7 @@ export async function chatInputCommand(client: Client, interaction: ChatInputCom const textChannel = interaction.guild?.channels.cache.get(channel.id); if (textChannel === undefined || textChannel.type !== 0) return; const embed = await SendReactionRoleEmbed(textChannel, guild.id, channel.id); + return interaction.editReply({ embeds: [{ diff --git a/src/bot/functions/BanReactionHandler.ts b/src/bot/functions/BanReactionHandler.ts new file mode 100644 index 0000000..10ce1ba --- /dev/null +++ b/src/bot/functions/BanReactionHandler.ts @@ -0,0 +1,41 @@ +import { Client, MessageReaction, PartialMessageReaction, User, PartialUser, Guild, Role, GuildMember } from "discord.js"; +import { schemas } from "../../database"; + +import * as YALAS from 'mcstatusbot-logger'; +export default async function ReactionRoleAddHandler(reaction: MessageReaction | PartialMessageReaction, user: User | PartialUser) { + + const emoji: string | null = reaction.emoji.id ?? reaction.emoji.name; + const guild: Guild | null = reaction.message.guild; + const channelId: string = reaction.message.channelId; + + + if (guild === null || emoji === null) { + YALAS.error("failed to find guild"); + return false; + } + if (emoji!=='❌') return false; + + + //TODO: implement redis cache or better table lookup + const banReaction = await schemas['BanReaction'].findOne({ + where: { + guild: guild.id, + channel: channelId + }, + raw: true + }); + if (banReaction===null) return false; + + + try { + const member: GuildMember | null = guild.members.cache.get(user.id) || (await guild.members.fetch(user.id).catch(() => null)); + if (!member) return YALAS.error("Ban Reaction Handler: Member not found in the guild '" + guild.id + "'"); + + await member.ban({reason: "ban reaction"}); + return true; + } catch (error){ + YALAS.error("Ban Reaction Handler: Error banning user '" + user.id + "' for guild '" + guild.id + "'"); + YALAS.error(error); + return true; + } +} \ No newline at end of file diff --git a/src/bot/functions/BanReactionRoleHandler.ts b/src/bot/functions/BanReactionRoleHandler.ts deleted file mode 100644 index 5126e22..0000000 --- a/src/bot/functions/BanReactionRoleHandler.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Client, MessageReaction, PartialMessageReaction, User, PartialUser, Guild, Role, GuildMember } from "discord.js"; -import { schemas } from "../../database"; - -import * as YALAS from 'mcstatusbot-logger'; -export default async function ReactionRoleAddHandler(reaction: MessageReaction | PartialMessageReaction, user: User | PartialUser) { - - const emoji: string | null = reaction.emoji.id ?? reaction.emoji.name; - const guild: Guild | null = reaction.message.guild; - const channelId: string = reaction.message.channelId; - - if (guild === null || emoji === null) return YALAS.error("failed to find guild"); - - - async function getRole(roleId: string) { - if (!guild) return null; - let role: Role | null | undefined = guild.roles.cache.get(roleId); - if (role) return role - try { - role = await guild.roles.fetch(roleId); - } catch (error) { - YALAS.error('Error fetching role:'); - YALAS.error(error); - return null; - } - if (role) return role - return null; - } - - const reactionRoles = await schemas['ReactionRole'].findAll({ - where: { - guild: guild.id, - channel: channelId, - reaction: emoji, - }, - raw: true - }); - - for (const rRole of reactionRoles) { - let role = await getRole(rRole.role); - if (role == null) return YALAS.error("failed to find role '" + rRole.role + "' in guild '" + guild.id + "' ") - - // Fetch role if not cached - - - // Get the member from the user - const member: GuildMember | null = guild.members.cache.get(user.id) || (await guild.members.fetch(user.id).catch(() => null)); - if (!member) return YALAS.error("Member not found in the guild '" + guild.id + "'"); - - - // Add the role to the member - try { - await member.roles.add(role); - if (process.env.DEBUG === 'true') YALAS.info(`Added role "${role.name}" to user "${user.tag}".`); - } catch (error) { - YALAS.error("Error adding role '" + role.id + "' to user '" + user.id + "' for guild '" + guild.id + "'"); - YALAS.error(error); - } - } -} \ No newline at end of file diff --git a/src/bot/functions/PrefixCache.ts b/src/bot/functions/PrefixCache.ts index 298a4cb..ffe4e93 100644 --- a/src/bot/functions/PrefixCache.ts +++ b/src/bot/functions/PrefixCache.ts @@ -1,3 +1,4 @@ +//TODO: move to redis cache? export default class PrefixCache { private cache = new Map(); diff --git a/src/bot/functions/ReactionRoleAddHandler.ts b/src/bot/functions/ReactionRoleAddHandler.ts index a780ec2..07a3f66 100644 --- a/src/bot/functions/ReactionRoleAddHandler.ts +++ b/src/bot/functions/ReactionRoleAddHandler.ts @@ -44,14 +44,14 @@ export default async function ReactionRoleAddHandler(reaction: MessageReaction | let role = await getRole(rRole.role); if (role == null) return YALAS.error("failed to find role '" + rRole.role + "' in guild '" + guild.id + "' ") - //TODO: mod channel logs ad role error etc + //TODO: implement audit log // Add the role to the member try { await member.roles.add(role); if (process.env.DEBUG === 'true') YALAS.info(`Added role "${role.name}" to user "${user.tag}".`); } catch (error) { - //TODO: implement ignore reaction reomve when using this + //TODO: implement ignoring this reaction remove to avoid errors with msg reaction remove handler //reaction.users.remove(user.id); const errUsrMsg = await (channel as TextChannel).send(`Sorry <@!${user.id}>, that did not work please try again later or ask a moderator.`); diff --git a/src/bot/functions/ReactionRoleRemoveHandler.ts b/src/bot/functions/ReactionRoleRemoveHandler.ts index a6bae82..ecfd35b 100644 --- a/src/bot/functions/ReactionRoleRemoveHandler.ts +++ b/src/bot/functions/ReactionRoleRemoveHandler.ts @@ -9,6 +9,7 @@ export default async function ReactionRoleRemoveHandler(reaction: MessageReactio const channel = reaction.message.channel; if (guild === null || emoji === null) return YALAS.error("failed to find guild"); + if (emoji=='❌') return; async function getRole(roleId: string) { @@ -46,13 +47,14 @@ export default async function ReactionRoleRemoveHandler(reaction: MessageReactio if (rRole.dataValues.reactionRemove === false) continue; + //TODO: implement audit log // Add the role to the member try { await member.roles.remove(role); if (process.env.DEBUG === 'true') YALAS.info(`Added role "${role.name}" to user "${user.tag}".`); } catch (error) { reaction.users.remove(user.id); - + const errUsrMsg = await (channel as TextChannel).send(`Sorry <@!${user.id}>, that did not work please try again later or ask a moderator.`); setTimeout(() => { diff --git a/src/bot/handlers/MessageReactionAdd.ts b/src/bot/handlers/MessageReactionAdd.ts index 37cad70..14039ed 100644 --- a/src/bot/handlers/MessageReactionAdd.ts +++ b/src/bot/handlers/MessageReactionAdd.ts @@ -1,5 +1,6 @@ import { Client, MessageReaction, PartialMessageReaction, User, PartialUser } from "discord.js"; import ReactionRoleAddHandler from "../functions/ReactionRoleAddHandler"; +import BanReactionHandler from "../functions/BanReactionHandler"; import * as YALAS from 'mcstatusbot-logger'; export default async function MessageReactionAdd(client: Client, reaction: MessageReaction | PartialMessageReaction, user: User | PartialUser) { @@ -13,7 +14,7 @@ export default async function MessageReactionAdd(client: Client, reaction: Messa } } if (!reaction.message.guild) return; - + if (user.partial) { try { @@ -23,12 +24,12 @@ export default async function MessageReactionAdd(client: Client, reaction: Messa return; } } - if (user.id===client.user?.id) return; + if (user.id === client.user?.id) return; + + const memberBanned = await BanReactionHandler(reaction, user); + //non need to try and run reactions roles if member banned + if (memberBanned === true) return; await ReactionRoleAddHandler(reaction, user); - - //TODO: ban role hndler - //TODO: kick role handler - return; } diff --git a/src/database/index.ts b/src/database/index.ts index 1f122bf..64eb17a 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -23,7 +23,7 @@ const sequelize = new Sequelize(process.env.DB_URI, { }); const schemas = { - BanRole: BanReaction(sequelize), + BanReaction: BanReaction(sequelize), Guild: Guild(sequelize), KickRole: KickReaction(sequelize), MessageMacro: MessageMacro(sequelize), @@ -34,7 +34,6 @@ const schemas = { export async function connect() { - YALAS.info("attempting to connect to db"); try { await sequelize.authenticate(); diff --git a/src/database/schemas/BanReaction.ts b/src/database/schemas/BanReaction.ts index c1b4c77..391dd83 100644 --- a/src/database/schemas/BanReaction.ts +++ b/src/database/schemas/BanReaction.ts @@ -4,7 +4,6 @@ export interface BanReactionAttributes { id: number; guild: string; channel: string; - reaction: string; } export interface BanReactionCreationAttributes extends Optional { } @@ -28,11 +27,7 @@ export default function BanRole(sequelize: Sequelize) { channel: { type: DataTypes.STRING, allowNull: false, - }, - reaction: { - type: DataTypes.STRING, - allowNull: false, - }, + } }); // Sync the model with the database