diff options
author | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2021-08-25 14:47:07 -0400 |
---|---|---|
committer | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2021-08-25 14:47:07 -0400 |
commit | 0af0be5ab4ea0d972d9c406b28b81ee41a06cbdb (patch) | |
tree | cb5d6f06a115d7e9c3d38ea44995161f6c158082 | |
parent | 76622cfcd13727949ef3a0baa30bf72007132cd2 (diff) | |
download | tanzanite-0af0be5ab4ea0d972d9c406b28b81ee41a06cbdb.tar.gz tanzanite-0af0be5ab4ea0d972d9c406b28b81ee41a06cbdb.tar.bz2 tanzanite-0af0be5ab4ea0d972d9c406b28b81ee41a06cbdb.zip |
join roles, sticky roles, join messages, support threads etc
22 files changed, 544 insertions, 75 deletions
diff --git a/src/commands/config/autoPublishChannel.ts b/src/commands/config/autoPublishChannel.ts index f058402..10c4ab6 100644 --- a/src/commands/config/autoPublishChannel.ts +++ b/src/commands/config/autoPublishChannel.ts @@ -46,6 +46,9 @@ export default class AutoPublishChannelCommand extends BushCommand { channel.id ); await message.guild!.setSetting('autoPublishChannels', newValue); + client.logger.debugRaw(autoPublishChannels); + client.logger.debugRaw(channel.id); + client.logger.debugRaw(autoPublishChannels.includes(channel.id)); return await message.util.reply({ content: `${util.emojis.success} Successfully ${ autoPublishChannels.includes(channel.id) ? 'disabled' : 'enabled' diff --git a/src/commands/config/features.ts b/src/commands/config/features.ts index d37ce25..cb7f4bc 100644 --- a/src/commands/config/features.ts +++ b/src/commands/config/features.ts @@ -14,16 +14,31 @@ // slash: true, // channel: 'guild', // clientPermissions: ['SEND_MESSAGES', 'EMBED_LINKS'], -// userPermissions: ['SEND_MESSAGES', 'MANAGE_GUILD'] +// userPermissions: ['SEND_MESSAGES', 'MANAGE_GUILD'], +// ownerOnly: true // }); // } // public override async exec(message: BushMessage | BushSlashMessage): Promise<unknown> { // if (!message.guild) return await message.util.reply(`${util.emojis.error} This command can only be used in servers.`); -// const featureEmbed = new MessageEmbed().setTitle(`${message.guild.name}'s Features`).setColor(util.colors.default); +// const featureEmbed = await this.generateEmbed(message); +// return await message.util.reply({ embeds: [featureEmbed] }); +// } + +// public async handleInteraction(): Promise<void> { + +// } + +// public async generateEmbed(message: BushMessage | BushSlashMessage): Promise<MessageEmbed> { +// const featureEmbed = new MessageEmbed().setTitle(`${message.guild!.name}'s Features`).setColor(util.colors.default); // const featureList: string[] = []; -// const enabledFeatures = await message.guild.getSetting('enabledFeatures'); +// const enabledFeatures = await message.guild!.getSetting('enabledFeatures'); // guildFeatures.forEach((feature) => { -// // featureList.push(`**${feature}:** ${enabledFeatures.includes(feature)? util.emojis.}`); +// featureList.push(`**${feature}:** ${enabledFeatures.includes(feature) ? util.emojis.check : util.emojis.cross}`); // }); +// return featureEmbed.setDescription(featureList.join('\n')); +// } + +// public async generateButtons(): Promise<void>{ + // } // } diff --git a/src/commands/config/joinRoles.ts b/src/commands/config/joinRoles.ts new file mode 100644 index 0000000..ee2ce75 --- /dev/null +++ b/src/commands/config/joinRoles.ts @@ -0,0 +1,54 @@ +import { AllowedMentions, BushCommand, BushMessage, BushSlashMessage } from '@lib'; +import { Channel } from 'discord.js'; + +export default class JoinRolesCommand extends BushCommand { + public constructor() { + super('joinRoles', { + aliases: ['joinroles', 'joinrole', 'jr'], + category: 'config', + description: { + content: 'Configure what roles to assign to someone when they join the server.', + usage: 'joinroles <role>', + examples: ['joinroles @member'] + }, + args: [ + { + id: 'role', + type: 'role', + prompt: { + start: 'What role would you like me to assign to users when they join the server?', + retry: '{error} Choose a valid role', + optional: true + } + } + ], + slash: true, + slashOptions: [ + { + name: 'role', + description: 'What role would you like me to assign to users when they join the server?', + type: 'ROLE', + required: false + } + ], + channel: 'guild', + clientPermissions: ['SEND_MESSAGES'], + userPermissions: ['SEND_MESSAGES', 'MANAGE_GUILD'] + }); + } + public override async exec(message: BushMessage | BushSlashMessage, { channel }: { channel: Channel }): Promise<unknown> { + const autoPublishChannels = await message.guild!.getSetting('joinRoles'); + const newValue = util.addOrRemoveFromArray( + autoPublishChannels.includes(channel.id) ? 'remove' : 'add', + autoPublishChannels, + channel.id + ); + await message.guild!.setSetting('joinRoles', newValue); + return await message.util.reply({ + content: `${util.emojis.success} Successfully ${ + autoPublishChannels.includes(channel.id) ? 'disabled' : 'enabled' + } auto publishing in <#${channel.id}>.`, + allowedMentions: AllowedMentions.none() + }); + } +} diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 8a98998..3a86244 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -5,7 +5,7 @@ import { User } from 'discord.js'; export default class BanCommand extends BushCommand { public constructor() { super('ban', { - aliases: ['ban', 'forceban'], + aliases: ['ban', 'forceban', 'dban'], category: 'moderation', description: { content: 'Ban a member from the server.', @@ -99,6 +99,8 @@ export default class BanCommand extends BushCommand { return message.util.reply(canModerateResponse); } + if (message.util.parsed?.alias === 'dban' && !days) days = 1; + if (!Number.isInteger(days) || days! < 0 || days! > 7) { return message.util.reply(`${util.emojis.error} The delete days must be an integer between 0 and 7.`); } diff --git a/src/commands/utilities/viewraw.ts b/src/commands/utilities/viewraw.ts index bc79b7a..bd185d4 100644 --- a/src/commands/utilities/viewraw.ts +++ b/src/commands/utilities/viewraw.ts @@ -1,5 +1,4 @@ -import { DMChannel, Message, MessageEmbed, NewsChannel, Snowflake, TextChannel } from 'discord.js'; -import { inspect } from 'util'; +import { DMChannel, MessageEmbed, NewsChannel, Snowflake, TextChannel } from 'discord.js'; import { BushCommand, BushMessage, BushSlashMessage } from '../../lib'; export default class ViewRawCommand extends BushCommand { @@ -8,15 +7,16 @@ export default class ViewRawCommand extends BushCommand { aliases: ['viewraw'], category: 'utilities', clientPermissions: ['EMBED_LINKS'], + channel: 'guild', description: { usage: 'viewraw <message id> <channel>', examples: ['viewraw 322862723090219008'], - content: 'Gives information about a specified user.' + content: 'Shows raw information about a message.' }, args: [ { id: 'message', - customType: util.arg.union('message', 'bigint'), + type: 'snowflake', prompt: { start: 'What message would you like to view?', retry: '{error} Choose a valid message.', @@ -30,8 +30,7 @@ export default class ViewRawCommand extends BushCommand { start: 'What channel is the message in?', retry: '{error} Choose a valid channel.', optional: true - }, - default: (m: Message) => m.channel + } }, { id: 'json', @@ -49,21 +48,14 @@ export default class ViewRawCommand extends BushCommand { public override async exec( message: BushMessage | BushSlashMessage, - args: { message: Message | BigInt; channel: TextChannel | NewsChannel | DMChannel; json?: boolean; js: boolean } + args: { message: Snowflake; channel: TextChannel | NewsChannel | DMChannel; json?: boolean; js: boolean } ): Promise<unknown> { - let newMessage: Message | 0; - if (!(typeof args.message === 'object')) { - newMessage = await args.channel.messages.fetch(`${args.message}` as Snowflake).catch(() => { - return 0; - }); - if (!newMessage) { - return await message.util.reply( - `${util.emojis.error} There was an error fetching that message, try supplying a channel.` - ); - } - } else { - newMessage = args.message as Message; - } + if (!args.channel) args.channel = (message.channel as TextChannel | NewsChannel | DMChannel)!; + const newMessage = await args.channel.messages.fetch(`${args.message}` as Snowflake).catch(() => null); + if (!newMessage) + return await message.util.reply( + `${util.emojis.error} There was an error fetching that message, make sure that is a valid id and if the message is not in this channel, please provide a channel.` + ); const messageEmbed = await ViewRawCommand.getRawData(newMessage as BushMessage, { json: args.json, js: args.js }); @@ -74,18 +66,15 @@ export default class ViewRawCommand extends BushCommand { const content = options.json || options.js ? options.json - ? inspect(JSON.stringify(message.toJSON())) - : inspect(message.toJSON()) || '[No Content]' + ? JSON.stringify(message.toJSON(), undefined, 2) + : util.inspect(message.toJSON()) || '[No Content]' : message.content || '[No Content]'; const lang = options.json ? 'json' : options.js ? 'js' : undefined; - return ( - new MessageEmbed() - .setFooter(message.author.tag, message.author.avatarURL({ dynamic: true }) ?? undefined) - .setTimestamp(message.createdTimestamp) - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - .setColor(message.member?.roles?.color?.color || util.colors.default) - .setTitle('Raw Message Information') - .setDescription(await util.codeblock(content, 2048, lang)) - ); + return new MessageEmbed() + .setFooter(message.author.tag, message.author.avatarURL({ dynamic: true }) ?? undefined) + .setTimestamp(message.createdTimestamp) + .setColor(message.member?.roles?.color?.color ?? util.colors.default) + .setTitle('Raw Message Information') + .setDescription(await util.codeblock(content, 2048, lang)); } } diff --git a/src/lib/extensions/discord.js/BushClientEvents.d.ts b/src/lib/extensions/discord.js/BushClientEvents.d.ts index da5d647..ae9b186 100644 --- a/src/lib/extensions/discord.js/BushClientEvents.d.ts +++ b/src/lib/extensions/discord.js/BushClientEvents.d.ts @@ -1,7 +1,103 @@ -import { ClientEvents } from 'discord.js'; -import { BushMessage, BushPartialMessage } from './BushMessage'; +import { + ClientEvents, + Collection, + Interaction, + InvalidRequestWarningData, + Invite, + RateLimitData, + Snowflake, + Sticker, + Typing +} from 'discord.js'; +import { BushClient, BushTextBasedChannels } from '../discord-akairo/BushClient'; +import { BushApplicationCommand } from './BushApplicationCommand'; +import { BushDMChannel } from './BushDMChannel'; +import { BushGuild } from './BushGuild'; +import { BushGuildBan } from './BushGuildBan'; +import { BushGuildChannel } from './BushGuildChannel'; +import { BushGuildEmoji } from './BushGuildEmoji'; +import { BushGuildMember, PartialBushGuildMember } from './BushGuildMember'; +import { BushMessage, PartialBushMessage } from './BushMessage'; +import { BushMessageReaction, PartialBushMessageReaction } from './BushMessageReaction'; +import { BushPresence } from './BushPresence'; +import { BushRole } from './BushRole'; +import { BushStageInstance } from './BushStageInstance'; +import { BushTextChannel } from './BushTextChannel'; +import { BushThreadChannel } from './BushThreadChannel'; +import { BushThreadMember } from './BushThreadMember'; +import { BushUser, PartialBushUser } from './BushUser'; +import { BushVoiceState } from './BushVoiceState'; export interface BushClientEvents extends ClientEvents { + applicationCommandCreate: [command: BushApplicationCommand]; + applicationCommandDelete: [command: BushApplicationCommand]; + applicationCommandUpdate: [oldCommand: BushApplicationCommand | null, newCommand: BushApplicationCommand]; + channelCreate: [channel: BushGuildChannel]; + channelDelete: [channel: BushDMChannel | BushGuildChannel]; + channelPinsUpdate: [channel: BushTextBasedChannels, date: Date]; + channelUpdate: [oldChannel: BushDMChannel | BushGuildChannel, newChannel: BushDMChannel | BushGuildChannel]; + debug: [message: string]; + warn: [message: string]; + emojiCreate: [emoji: BushGuildEmoji]; + emojiDelete: [emoji: BushGuildEmoji]; + emojiUpdate: [oldEmoji: BushGuildEmoji, newEmoji: BushGuildEmoji]; + error: [error: Error]; + guildBanAdd: [ban: BushGuildBan]; + guildBanRemove: [ban: BushGuildBan]; + guildCreate: [guild: BushGuild]; + guildDelete: [guild: BushGuild]; + guildUnavailable: [guild: BushGuild]; + guildIntegrationsUpdate: [guild: BushGuild]; + guildMemberAdd: [member: BushGuildMember]; + guildMemberAvailable: [member: BushGuildMember | PartialBushGuildMember]; + guildMemberRemove: [member: BushGuildMember | PartialBushGuildMember]; + guildMembersChunk: [ + members: Collection<Snowflake, BushGuildMember>, + guild: BushGuild, + data: { count: number; index: number; nonce: string | undefined } + ]; + guildMemberUpdate: [oldMember: BushGuildMember | PartialBushGuildMember, newMember: BushGuildMember]; + guildUpdate: [oldGuild: BushGuild, newGuild: BushGuild]; + inviteCreate: [invite: Invite]; + inviteDelete: [invite: Invite]; messageCreate: [message: BushMessage]; - messageUpdate: [oldMessage: BushMessage | BushPartialMessage, newMessage: BushMessage | BushPartialMessage]; + messageDelete: [message: BushMessage | PartialBushMessage]; + messageReactionRemoveAll: [message: BushMessage | PartialBushMessage]; + messageReactionRemoveEmoji: [reaction: BushMessageReaction | PartialBushMessageReaction]; + messageDeleteBulk: [messages: Collection<Snowflake, BushMessage | PartialBushMessage>]; + messageReactionAdd: [reaction: BushMessageReaction | PartialBushMessageReaction, user: BushUser | PartialBushUser]; + messageReactionRemove: [reaction: BushMessageReaction | PartialBushMessageReaction, user: BushUser | PartialBushUser]; + messageUpdate: [oldMessage: BushMessage | PartialBushMessage, newMessage: BushMessage | PartialBushMessage]; + presenceUpdate: [oldPresence: BushPresence | null, newPresence: BushPresence]; + rateLimit: [rateLimitData: RateLimitData]; + invalidRequestWarning: [invalidRequestWarningData: InvalidRequestWarningData]; + ready: [client: BushClient<true>]; + invalidated: []; + roleCreate: [role: BushRole]; + roleDelete: [role: BushRole]; + roleUpdate: [oldRole: BushRole, newRole: BushRole]; + threadCreate: [thread: BushThreadChannel]; + threadDelete: [thread: BushThreadChannel]; + threadListSync: [threads: Collection<Snowflake, BushThreadChannel>]; + threadMemberUpdate: [oldMember: BushThreadMember, newMember: BushThreadMember]; + threadMembersUpdate: [ + oldMembers: Collection<Snowflake, BushThreadMember>, + mewMembers: Collection<Snowflake, BushThreadMember> + ]; + threadUpdate: [oldThread: BushThreadChannel, newThread: BushThreadChannel]; + typingStart: [typing: Typing]; + userUpdate: [oldUser: BushUser | PartialBushUser, newUser: BushUser]; + voiceStateUpdate: [oldState: BushVoiceState, newState: BushVoiceState]; + webhookUpdate: [channel: BushTextChannel]; + interactionCreate: [interaction: Interaction]; + shardError: [error: Error, shardId: number]; + shardReady: [shardId: number, unavailableGuilds: Set<Snowflake> | undefined]; + shardReconnecting: [shardId: number]; + shardResume: [shardId: number, replayedEvents: number]; + stageInstanceCreate: [stageInstance: BushStageInstance]; + stageInstanceUpdate: [oldStageInstance: BushStageInstance | null, newStageInstance: BushStageInstance]; + stageInstanceDelete: [stageInstance: BushStageInstance]; + stickerCreate: [sticker: Sticker]; + stickerDelete: [sticker: Sticker]; + stickerUpdate: [oldSticker: Sticker, newSticker: Sticker]; } diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts index 81c0108..efc780d 100644 --- a/src/lib/extensions/discord.js/BushGuild.ts +++ b/src/lib/extensions/discord.js/BushGuild.ts @@ -1,6 +1,6 @@ import { Guild } from 'discord.js'; import { RawGuildData } from 'discord.js/typings/rawDataTypes'; -import { Guild as GuildDB, GuildModel } from '../../models/Guild'; +import { Guild as GuildDB, GuildFeatures, GuildModel } from '../../models/Guild'; import { ModLogType } from '../../models/ModLog'; import { BushClient, BushUserResolvable } from '../discord-akairo/BushClient'; import { BushGuildMember } from './BushGuildMember'; @@ -15,6 +15,11 @@ export class BushGuild extends Guild { super(client, data); } + public async hasFeature(feature: GuildFeatures): Promise<boolean> { + const features = await this.getSetting('enabledFeatures'); + return features.includes(feature); + } + public async getSetting<K extends keyof GuildModel>(setting: K): Promise<GuildModel[K]> { return ( client.cache.guilds.get(this.id)?.[setting] ?? diff --git a/src/lib/extensions/discord.js/BushGuildBan.d.ts b/src/lib/extensions/discord.js/BushGuildBan.d.ts new file mode 100644 index 0000000..0174220 --- /dev/null +++ b/src/lib/extensions/discord.js/BushGuildBan.d.ts @@ -0,0 +1,14 @@ +import { GuildBan } from 'discord.js'; +import { RawGuildBanData } from 'discord.js/typings/rawDataTypes'; +import { BushClient } from '../discord-akairo/BushClient'; +import { BushGuild } from './BushGuild'; +import { BushUser } from './BushUser'; + +export class BushGuildBan extends GuildBan { + public constructor(client: BushClient, data: RawGuildBanData, guild: BushGuild); + public guild: BushGuild; + public user: BushUser; + public readonly partial: boolean; + public reason?: string | null; + public fetch(force?: boolean): Promise<BushGuildBan>; +} diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts index 641cc74..e596c82 100644 --- a/src/lib/extensions/discord.js/BushGuildMember.ts +++ b/src/lib/extensions/discord.js/BushGuildMember.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { GuildMember, Role } from 'discord.js'; +import { GuildMember, Partialize, Role } from 'discord.js'; import { RawGuildMemberData } from 'discord.js/typings/rawDataTypes'; import { ModLogType } from '../../models/ModLog'; import { BushClient, BushUserResolvable } from '../discord-akairo/BushClient'; @@ -72,6 +72,12 @@ interface BushBanOptions extends BushTimedPunishmentOptions { type BanResponse = PunishmentResponse | 'missing permissions' | 'error creating ban entry' | 'error banning'; +export type PartialBushGuildMember = Partialize< + BushGuildMember, + 'joinedAt' | 'joinedTimestamp', + 'user' | 'warn' | 'addRole' | 'removeRole' | 'mute' | 'unmute' | 'bushKick' | 'bushBan' | 'isOwner' | 'isSuperUser' +>; + export class BushGuildMember extends GuildMember { public declare readonly client: BushClient; public declare guild: BushGuild; @@ -98,8 +104,7 @@ export class BushGuildMember extends GuildMember { // dm user const ending = await this.guild.getSetting('punishmentEnding'); const dmSuccess = await this.send({ - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - content: `You have been warned in **${this.guild}** for **${options.reason || 'No reason provided'}**.${ + content: `You have been warned in **${this.guild.name}** for **${options.reason ?? 'No reason provided'}**.${ ending ? `\n\n${ending}` : '' }` }).catch(() => false); @@ -200,8 +205,7 @@ export class BushGuildMember extends GuildMember { // add role const muteSuccess = await this.roles - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - .add(muteRole, `[Mute] ${moderator.tag} | ${options.reason || 'No reason provided.'}`) + .add(muteRole, `[Mute] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) .catch(async (e) => { await client.console.warn('muteRoleAddError', e?.stack || e); return false; @@ -236,8 +240,7 @@ export class BushGuildMember extends GuildMember { const dmSuccess = await this.send({ content: `You have been muted ${ options.duration ? 'for ' + util.humanizeDuration(options.duration) : 'permanently' - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - } in **${this.guild}** for **${options.reason || 'No reason provided'}**.${ending ? `\n\n${ending}` : ''}` + } in **${this.guild.name}** for **${options.reason ?? 'No reason provided'}**.${ending ? `\n\n${ending}` : ''}` }).catch(() => false); if (!dmSuccess) return 'failed to dm'; @@ -258,8 +261,7 @@ export class BushGuildMember extends GuildMember { //remove role const muteSuccess = await this.roles - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - .remove(muteRole, `[Unmute] ${moderator.tag} | ${options.reason || 'No reason provided.'}`) + .remove(muteRole, `[Unmute] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) .catch(async (e) => { await client.console.warn('muteRoleAddError', e?.stack || e); return false; @@ -288,8 +290,7 @@ export class BushGuildMember extends GuildMember { //dm user const dmSuccess = await this.send({ - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - content: `You have been unmuted in **${this.guild}** because **${options.reason || 'No reason provided'}**.` + content: `You have been unmuted in **${this.guild.name}** because **${options.reason ?? 'No reason provided'}**.` }).catch(() => false); if (!dmSuccess) return 'failed to dm'; @@ -306,15 +307,13 @@ export class BushGuildMember extends GuildMember { // dm user const ending = await this.guild.getSetting('punishmentEnding'); const dmSuccess = await this.send({ - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - content: `You have been kicked from **${this.guild}** for **${options.reason || 'No reason provided'}**.${ + content: `You have been kicked from **${this.guild.name}** for **${options.reason ?? 'No reason provided'}**.${ ending ? `\n\n${ending}` : '' }` }).catch(() => false); // kick - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const kickSuccess = await this.kick(`${moderator?.tag} | ${options.reason || 'No reason provided.'}`).catch(() => false); + const kickSuccess = await this.kick(`${moderator?.tag} | ${options.reason ?? 'No reason provided.'}`).catch(() => false); if (!kickSuccess) return 'error kicking'; // add modlog entry @@ -343,14 +342,12 @@ export class BushGuildMember extends GuildMember { const dmSuccess = await this.send({ content: `You have been banned ${ options?.duration ? 'for ' + util.humanizeDuration(options.duration) : 'permanently' - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - } from **${this.guild}** for **${options.reason || 'No reason provided'}**.${ending ? `\n\n${ending}` : ''}` + } from **${this.guild.name}** for **${options.reason ?? 'No reason provided'}**.${ending ? `\n\n${ending}` : ''}` }).catch(() => false); // ban const banSuccess = await this.ban({ - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - reason: `${moderator.tag} | ${options.reason || 'No reason provided.'}`, + reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, days: options.deleteDays }).catch(() => false); if (!banSuccess) return 'error banning'; diff --git a/src/lib/extensions/discord.js/BushMessage.ts b/src/lib/extensions/discord.js/BushMessage.ts index 6d9a332..7907efe 100644 --- a/src/lib/extensions/discord.js/BushMessage.ts +++ b/src/lib/extensions/discord.js/BushMessage.ts @@ -7,7 +7,7 @@ import { BushGuild } from './BushGuild'; import { BushGuildMember } from './BushGuildMember'; import { BushUser } from './BushUser'; -export interface BushPartialMessage +export interface PartialBushMessage extends Partialize<BushMessage, 'type' | 'system' | 'pinned' | 'tts', 'content' | 'cleanContent' | 'author'> {} export class BushMessage extends Message { public declare readonly client: BushClient; diff --git a/src/lib/extensions/discord.js/BushMessageReaction.ts b/src/lib/extensions/discord.js/BushMessageReaction.ts index b0bc5d7..056e4e6 100644 --- a/src/lib/extensions/discord.js/BushMessageReaction.ts +++ b/src/lib/extensions/discord.js/BushMessageReaction.ts @@ -1,10 +1,12 @@ -import { MessageReaction } from 'discord.js'; +import { MessageReaction, Partialize } from 'discord.js'; import { RawMessageReactionData } from 'discord.js/typings/rawDataTypes'; import { BushClient } from '../discord-akairo/BushClient'; import { BushGuildEmoji } from './BushGuildEmoji'; import { BushMessage } from './BushMessage'; import { BushReactionEmoji } from './BushReactionEmoji'; +export type PartialBushMessageReaction = Partialize<BushMessageReaction, 'count'>; + export class BushMessageReaction extends MessageReaction { public declare readonly client: BushClient; public declare readonly emoji: BushGuildEmoji | BushReactionEmoji; diff --git a/src/lib/extensions/discord.js/BushUser.ts b/src/lib/extensions/discord.js/BushUser.ts index 9a183e1..15a0d03 100644 --- a/src/lib/extensions/discord.js/BushUser.ts +++ b/src/lib/extensions/discord.js/BushUser.ts @@ -1,8 +1,10 @@ -import { User } from 'discord.js'; +import { Partialize, User } from 'discord.js'; import { RawUserData } from 'discord.js/typings/rawDataTypes'; import { BushClient } from '../discord-akairo/BushClient'; import { BushDMChannel } from './BushDMChannel'; +export type PartialBushUser = Partialize<BushUser, 'username' | 'tag' | 'discriminator' | 'isOwner' | 'isSuperUser'>; + export class BushUser extends User { public declare readonly client: BushClient; public declare readonly dmChannel: BushDMChannel | null; diff --git a/src/lib/models/Guild.ts b/src/lib/models/Guild.ts index dfba90c..4640d70 100644 --- a/src/lib/models/Guild.ts +++ b/src/lib/models/Guild.ts @@ -16,6 +16,7 @@ export interface GuildModel { lockdownChannels: Snowflake[]; autoModPhases: string[]; enabledFeatures: string[]; + joinRoles: Snowflake[]; } export interface GuildModelCreationAttributes { @@ -31,6 +32,7 @@ export interface GuildModelCreationAttributes { lockdownChannels?: Snowflake[]; autoModPhases?: string[]; enabledFeatures?: string[]; + joinRoles?: Snowflake[]; } export const guildSettings = { @@ -39,10 +41,12 @@ export const guildSettings = { welcomeChannel: { type: 'channel-array' }, muteRole: { type: 'role' }, punishmentEnding: { type: 'string' }, - lockdownChannels: { type: 'channel-array' } + lockdownChannels: { type: 'channel-array' }, + joinRoles: { type: 'role-array' } }; export const guildFeatures = ['automodEnabled', 'supportThreads', 'stickyRoles']; +export type GuildFeatures = 'automodEnabled' | 'supportThreads' | 'stickyRoles'; const NEVER_USED = 'This should never be executed'; @@ -167,6 +171,16 @@ export class Guild extends BaseModel<GuildModel, GuildModelCreationAttributes> i throw new Error(NEVER_USED); } + /** + * The roles to assign to a user if they are not assigned sticky roles + */ + public get joinRoles(): Snowflake[] { + throw new Error(NEVER_USED); + } + public set joinRoles(_: Snowflake[]) { + throw new Error(NEVER_USED); + } + public static initModel(sequelize: Sequelize, client: BushClient): void { Guild.init( { @@ -267,6 +281,17 @@ export class Guild extends BaseModel<GuildModel, GuildModelCreationAttributes> i }, allowNull: false, defaultValue: '[]' + }, + joinRoles: { + type: DataTypes.TEXT, + get: function () { + return JSON.parse(this.getDataValue('enabledFeatures') as unknown as string); + }, + set: function (val: string[]) { + return this.setDataValue('enabledFeatures', JSON.stringify(val) as unknown as string[]); + }, + allowNull: false, + defaultValue: '[]' } }, { sequelize: sequelize } diff --git a/src/lib/models/StickyRole.ts b/src/lib/models/StickyRole.ts index b49af80..d304370 100644 --- a/src/lib/models/StickyRole.ts +++ b/src/lib/models/StickyRole.ts @@ -6,11 +6,13 @@ export interface StickyRoleModel { user: Snowflake; guild: Snowflake; roles: Snowflake[]; + nickname: string; } export interface StickyRoleModelCreationAttributes { user: Snowflake; guild: Snowflake; roles: Snowflake[]; + nickname?: string; } const NEVER_USED = 'This should never be executed'; @@ -46,6 +48,16 @@ export class StickyRole extends BaseModel<StickyRoleModel, StickyRoleModelCreati throw new Error(NEVER_USED); } + /** + * The user's previous nickname + */ + public get nickname(): string { + throw new Error(NEVER_USED); + } + public set nickname(_: string) { + throw new Error(NEVER_USED); + } + public static initModel(sequelize: Sequelize): void { StickyRole.init( { @@ -57,7 +69,6 @@ export class StickyRole extends BaseModel<StickyRoleModel, StickyRoleModelCreati type: DataTypes.STRING, allowNull: false }, - roles: { type: DataTypes.STRING, get: function () { @@ -67,6 +78,10 @@ export class StickyRole extends BaseModel<StickyRoleModel, StickyRoleModelCreati return this.setDataValue('roles', JSON.stringify(val) as unknown as Snowflake[]); }, allowNull: true + }, + nickname: { + type: DataTypes.STRING, + allowNull: true } }, { sequelize } diff --git a/src/listeners/client/interactionCreate.ts b/src/listeners/client/interactionCreate.ts index 7dc20ec..d76a484 100644 --- a/src/listeners/client/interactionCreate.ts +++ b/src/listeners/client/interactionCreate.ts @@ -1,5 +1,5 @@ import { BushListener } from '@lib'; -import { ClientEvents } from 'discord.js'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; export default class InteractionCreateListener extends BushListener { public constructor() { @@ -10,7 +10,7 @@ export default class InteractionCreateListener extends BushListener { }); } - public override async exec(...[interaction]: ClientEvents['interactionCreate']): Promise<unknown> { + public override async exec(...[interaction]: BushClientEvents['interactionCreate']): Promise<unknown> { if (!interaction) return; if (interaction.isCommand()) { void client.console.info( diff --git a/src/listeners/guild/guildCreate.ts b/src/listeners/guild/guildCreate.ts new file mode 100644 index 0000000..21a7ab0 --- /dev/null +++ b/src/listeners/guild/guildCreate.ts @@ -0,0 +1,18 @@ +import { BushListener, Guild } from '../../lib'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class GuildCreateListener extends BushListener { + public constructor() { + super('guildCreate', { + emitter: 'client', + event: 'guildCreate', // when the bot joins a guild + category: 'client' + }); + } + + public override async exec(...[guild]: BushClientEvents['guildCreate']): Promise<void> { + void client.console.info('JoinGuild', `Joined <<${guild.name}>> with <<${guild.memberCount?.toLocaleString()}>> members.`); + const g = await Guild.findByPk(guild.id); + if (!g) void Guild.create({ id: guild.id }); + } +} diff --git a/src/listeners/guild/guildDelete.ts b/src/listeners/guild/guildDelete.ts new file mode 100644 index 0000000..a59f45e --- /dev/null +++ b/src/listeners/guild/guildDelete.ts @@ -0,0 +1,16 @@ +import { BushListener } from '../../lib'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class GuildDeleteListener extends BushListener { + public constructor() { + super('guildDelete', { + emitter: 'client', + event: 'guildDelete', //when the bot leaves a guild + category: 'client' + }); + } + + public override exec(...[guild]: BushClientEvents['guildDelete']): void { + void client.console.info('LeaveGuild', `Left <<${guild.name}>> with <<${guild.memberCount?.toLocaleString()}>> members.`); + } +} diff --git a/src/listeners/guild/guildMemberAdd.ts b/src/listeners/guild/guildMemberAdd.ts new file mode 100644 index 0000000..bf6e0b6 --- /dev/null +++ b/src/listeners/guild/guildMemberAdd.ts @@ -0,0 +1,103 @@ +import { MessageEmbed, Snowflake, Util } from 'discord.js'; +import { BushGuildMember, BushListener, BushTextChannel, StickyRole } from '../../lib'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class GuildMemberAddListener extends BushListener { + public constructor() { + super('guildMemberAdd', { + emitter: 'client', + event: 'guildMemberAdd', + category: 'client' + }); + } + + public override async exec(...[member]: BushClientEvents['guildMemberAdd']): Promise<void> { + void this.sendWelcomeMessage(member); + void this.joinAndStickyRoles(member); + } + + public async sendWelcomeMessage(member: BushGuildMember): Promise<void> { + if (client.config.isDevelopment) return; + const welcomeChannel = await member.guild.getSetting('welcomeChannel'); + if (!welcomeChannel) return; + const welcome = this.client.channels.cache.get(welcomeChannel) as BushTextChannel | undefined; + if (!welcome) return; + if (member.guild.id !== welcome?.guild.id) throw new Error('Welcome channel must be in the guild.'); + const embed = new MessageEmbed() + .setDescription( + `${this.client.util.emojis.join} **${Util.escapeMarkdown( + member.user.tag + )}** joined the server. There are now ${member.guild.memberCount.toLocaleString()} members.` + ) + .setColor(this.client.util.colors.green); + await welcome + .send({ embeds: [embed] }) + .then(() => this.client.console.info('OnJoin', `Sent a message for <<${member.user.tag}>> in <<${member.guild.name}>>.`)) + .catch(() => + this.client.console.warn('OnJoin', `Failed to send message for <<${member.user.tag}>> in <<${member.guild.name}>>.`) + ); + } + + public async joinAndStickyRoles(member: BushGuildMember): Promise<void> { + if (client.config.isDevelopment) return; + if (await member.guild.hasFeature('stickyRoles')) { + const hadRoles = await StickyRole.findOne({ where: { guild: member.guild.id, user: member.id } }); + if (hadRoles?.roles?.length) { + const rolesArray = hadRoles.roles + .map((roleID: Snowflake) => { + const role = member.guild.roles.cache.get(roleID); + if (role && !member.roles.cache.has(roleID)) { + if (role.name !== '@everyone' || !role.managed) return role.id; + } + }) + .filter((role) => role) as Snowflake[]; + if (hadRoles.nickname && member.manageable) { + void member.setNickname(hadRoles.nickname).catch(() => {}); + } + if (rolesArray?.length) { + const addedRoles = await member.roles + .add(rolesArray, "Returning member's previous roles.") + .catch( + () => void this.client.console.warn('ReturnRoles', `There was an error returning <<${member.user.tag}>>'s roles.`) + ); + if (addedRoles) { + void this.client.console.info( + 'RoleData', + `Assigned sticky roles to <<${member.user.tag}>> in <<${member.guild.name}>>.` + ); + } else if (!addedRoles) { + const failedRoles: string[] = []; + for (let i = 0; i < rolesArray.length; i++) { + await member.roles + .add(rolesArray[i], "[Fallback] Returning member's previous roles.") + .catch(() => failedRoles.push(rolesArray[i])); + } + if (failedRoles.length) { + void this.client.console.warn('RoleData', 'Failed assigning the following roles on Fallback:' + failedRoles); + } else { + void this.client.console.info( + 'RoleData', + `[Fallback] Assigned sticky roles to <<${member.user.tag}>> in <<${member.guild.name}>>.` + ); + } + } + } + } + } else { + const joinRoles = await member.guild.getSetting('joinRoles'); + if (!joinRoles) return; + await member.roles + .add(joinRoles, 'Join roles.') + .then(() => + this.client.console.info('RoleData', `Assigned join roles to <<${member.user.tag}>> in <<${member.guild.name}>>.`) + ) + .catch( + () => + void this.client.console.warn( + 'OnJoin', + `Failed to assign join roles to <<${member.user.tag}>>, in <<${member.guild.name}>>.` + ) + ); + } + } +} diff --git a/src/listeners/guild/guildMemberRemove.ts b/src/listeners/guild/guildMemberRemove.ts new file mode 100644 index 0000000..f25108f --- /dev/null +++ b/src/listeners/guild/guildMemberRemove.ts @@ -0,0 +1,67 @@ +import { MessageEmbed, Util } from 'discord.js'; +import { BushGuildMember, BushListener, BushTextChannel, PartialBushGuildMember, StickyRole } from '../../lib'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class GuildMemberRemoveListener extends BushListener { + public constructor() { + super('guildMemberRemove', { + emitter: 'client', + event: 'guildMemberRemove', + category: 'guild' + }); + } + + public override async exec(...[member]: BushClientEvents['guildMemberRemove']): Promise<void> { + void this.sendWelcomeMessage(member); + void this.stickyRoles(member); + } + + public async sendWelcomeMessage(member: BushGuildMember | PartialBushGuildMember): Promise<void> { + if (client.config.isDevelopment) return; + const user = member.partial ? await client.users.fetch(member.id) : member.user; + await util.sleep(0.05); // ban usually triggers after member leave + const isBan = member.guild.bans.cache.has(member.id); + const welcomeChannel = await member.guild.getSetting('welcomeChannel'); + if (!welcomeChannel) return; + const welcome = this.client.channels.cache.get(welcomeChannel) as BushTextChannel | undefined; + if (member.guild.id !== welcome?.guild.id) throw new Error('Welcome channel must be in the guild.'); + const embed: MessageEmbed = new MessageEmbed() + .setDescription( + `${this.client.util.emojis.leave} **${Util.escapeBold(user.tag)}** ${ + isBan ? 'banned from' : 'left' + } the server. There are now ${welcome.guild.memberCount.toLocaleString()} members.` + ) + .setColor(isBan ? util.colors.orange : util.colors.red); + welcome + .send({ embeds: [embed] }) + .then(() => client.console.info('OnLeave', `Sent a message for <<${user.tag}>> in <<${member.guild.name}>>.`)) + .catch(() => + this.client.console.warn('OnLeave', `Failed to send message for <<${user.tag}>> in <<${member.guild.name}>>.`) + ); + } + + public async stickyRoles(member: BushGuildMember | PartialBushGuildMember): Promise<void> { + if (!(await member.guild.hasFeature('stickyRoles'))) return; + if (member.partial) throw new Error('Partial member, cannot save sticky roles.'); + const rolesArray = member.roles.cache.filter((role) => role.name !== '@everyone').map((role) => role.id); + const nickname = member.nickname; + if (rolesArray) { + const [row, isNew] = await StickyRole.findOrBuild({ + where: { + user: member.user.id, + guild: member.guild.id + }, + defaults: { + user: member.user.id, + guild: member.guild.id, + roles: rolesArray + } + }); + row.roles = rolesArray; + if (nickname) row.nickname = nickname; + await row + .save() + .then(() => this.client.console.info('RoleData', `${isNew ? 'Created' : 'Updated'} info for <<${member.user.tag}>>.`)); + } + } +} diff --git a/src/listeners/guild/syncUnban.ts b/src/listeners/guild/syncUnban.ts index b1e4fd9..9a6a607 100644 --- a/src/listeners/guild/syncUnban.ts +++ b/src/listeners/guild/syncUnban.ts @@ -1,15 +1,16 @@ import { ActivePunishment, ActivePunishmentType, BushListener } from '@lib'; -import { ClientEvents } from 'discord.js'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; export default class SyncUnbanListener extends BushListener { public constructor() { super('guildBanRemove', { emitter: 'client', - event: 'guildBanRemove' + event: 'guildBanRemove', + category: 'guild' }); } - public override async exec(...[ban]: ClientEvents['guildBanRemove']): Promise<void> { + public override async exec(...[ban]: BushClientEvents['guildBanRemove']): Promise<void> { const bans = await ActivePunishment.findAll({ where: { user: ban.user.id, diff --git a/src/listeners/message/automodCreate.ts b/src/listeners/message/automodCreate.ts index ff87513..ca61dd0 100644 --- a/src/listeners/message/automodCreate.ts +++ b/src/listeners/message/automodCreate.ts @@ -59,7 +59,7 @@ export default class AutomodMessageCreateListener extends BushListener { void message.delete().catch(() => {}); void message.member?.warn({ moderator: message.guild.me!, - reason: 'Saying a blacklisted word.' + reason: 'Saying a blacklisted word' }); break; @@ -68,7 +68,7 @@ export default class AutomodMessageCreateListener extends BushListener { void message.delete().catch(() => {}); void message.member?.mute({ moderator: message.guild.me!, - reason: 'Saying a blacklisted word.', + reason: 'Saying a blacklisted word', duration: 900_000 // 15 minutes }); break; @@ -77,7 +77,7 @@ export default class AutomodMessageCreateListener extends BushListener { void message.delete().catch(() => {}); void message.member?.mute({ moderator: message.guild.me!, - reason: 'Saying a blacklisted word.', + reason: 'Saying a blacklisted word', duration: 0 // perm }); break; @@ -102,9 +102,11 @@ export default class AutomodMessageCreateListener extends BushListener { new MessageEmbed() .setTitle(`[Severity ${highestOffence}] Automod Action Performed`) .setDescription( - `**User:** ${message.author} (${message.author.tag})\n**Sent From**: <#${message.channel.id}> [Jump to context](${ - message.url - })\n**Blacklisted Words:** ${util.surroundArray(Object.keys(offences), '`').join(', ')}` + `**User:** ${message.author.tag} (${message.author.tag})\n**Sent From**: <#${ + message.channel.id + }> [Jump to context](${message.url})\n**Blacklisted Words:** ${util + .surroundArray(Object.keys(offences), '`') + .join(', ')}` ) .addField('Message Content', `${await util.codeblock(message.content, 1024)}`) .setColor(color) diff --git a/src/listeners/message/supportThreads.ts b/src/listeners/message/supportThreads.ts new file mode 100644 index 0000000..ce2aa0d --- /dev/null +++ b/src/listeners/message/supportThreads.ts @@ -0,0 +1,43 @@ +import { GuildTextBasedChannels } from 'discord-akairo'; +import { MessageEmbed } from 'discord.js'; +import { BushListener, BushTextChannel } from '../../lib'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class MessageVerboseListener extends BushListener { + public constructor() { + super('supportThreads', { + emitter: 'client', + event: 'messageCreate', + category: 'message' + }); + } + + public override async exec(...[message]: BushClientEvents['messageCreate']): Promise<Promise<void> | undefined> { + if (!message.guild || !message.channel) return; + // todo: make these configurable etc... + if (message.guild.id !== '516977525906341928') return; // mb + if (message.channel.id !== '714332750156660756') return; // neu-support-1 + if (!(message.channel as BushTextChannel).permissionsFor(message.guild.me!).has('USE_PUBLIC_THREADS')) return; + const thread = await message.startThread({ + name: `Support - ${message.author.username}#${message.author.discriminator}`, + autoArchiveDuration: 1440, + reason: 'Support Thread' + }); + const embed = new MessageEmbed() + .setTitle('NotEnoughUpdates Support') + .setDescription( + `Welcome to Moulberry Bush Support:tm:\n\nPlease make sure you have the latest prerelease found in <#693586404256645231>.\nAdditionally if you need help installing the mod be sure to read <#737444942724726915> for a guide on how to do so.` + ) + .setColor('BLURPLE'); + void thread + .send({ embeds: [embed] }) + .then(() => + client.console.info( + 'supportThread', + `opened a support thread for <<${message.author.tag}>> in <<${ + (message.channel as GuildTextBasedChannels).name + }>> in <<${message.guild!.name}>>.` + ) + ); + } +} |