aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIRONM00N <64110067+IRONM00N@users.noreply.github.com>2021-08-25 14:47:07 -0400
committerIRONM00N <64110067+IRONM00N@users.noreply.github.com>2021-08-25 14:47:07 -0400
commit0af0be5ab4ea0d972d9c406b28b81ee41a06cbdb (patch)
treecb5d6f06a115d7e9c3d38ea44995161f6c158082
parent76622cfcd13727949ef3a0baa30bf72007132cd2 (diff)
downloadtanzanite-0af0be5ab4ea0d972d9c406b28b81ee41a06cbdb.tar.gz
tanzanite-0af0be5ab4ea0d972d9c406b28b81ee41a06cbdb.tar.bz2
tanzanite-0af0be5ab4ea0d972d9c406b28b81ee41a06cbdb.zip
join roles, sticky roles, join messages, support threads etc
-rw-r--r--src/commands/config/autoPublishChannel.ts3
-rw-r--r--src/commands/config/features.ts23
-rw-r--r--src/commands/config/joinRoles.ts54
-rw-r--r--src/commands/moderation/ban.ts4
-rw-r--r--src/commands/utilities/viewraw.ts51
-rw-r--r--src/lib/extensions/discord.js/BushClientEvents.d.ts102
-rw-r--r--src/lib/extensions/discord.js/BushGuild.ts7
-rw-r--r--src/lib/extensions/discord.js/BushGuildBan.d.ts14
-rw-r--r--src/lib/extensions/discord.js/BushGuildMember.ts35
-rw-r--r--src/lib/extensions/discord.js/BushMessage.ts2
-rw-r--r--src/lib/extensions/discord.js/BushMessageReaction.ts4
-rw-r--r--src/lib/extensions/discord.js/BushUser.ts4
-rw-r--r--src/lib/models/Guild.ts27
-rw-r--r--src/lib/models/StickyRole.ts17
-rw-r--r--src/listeners/client/interactionCreate.ts4
-rw-r--r--src/listeners/guild/guildCreate.ts18
-rw-r--r--src/listeners/guild/guildDelete.ts16
-rw-r--r--src/listeners/guild/guildMemberAdd.ts103
-rw-r--r--src/listeners/guild/guildMemberRemove.ts67
-rw-r--r--src/listeners/guild/syncUnban.ts7
-rw-r--r--src/listeners/message/automodCreate.ts14
-rw-r--r--src/listeners/message/supportThreads.ts43
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}>>.`
+ )
+ );
+ }
+}