From 2356d2c44736fb83021dacb551625852111c8ce6 Mon Sep 17 00:00:00 2001 From: IRONM00N <64110067+IRONM00N@users.noreply.github.com> Date: Thu, 18 Aug 2022 22:42:12 -0400 Subject: restructure, experimental presence and member automod, fixed bugs probably made some more bugs --- .eslintrc.cjs | 2 +- .gitignore | 1 + .vscode/settings.json | 5 +- config/tsconfig.json | 7 + lib/arguments/abbreviatedNumber.ts | 13 + lib/arguments/contentWithDuration.ts | 5 + lib/arguments/discordEmoji.ts | 14 + lib/arguments/duration.ts | 5 + lib/arguments/durationSeconds.ts | 6 + lib/arguments/globalUser.ts | 7 + lib/arguments/index.ts | 10 + lib/arguments/messageLink.ts | 20 + lib/arguments/permission.ts | 12 + lib/arguments/roleWithDuration.ts | 17 + lib/arguments/snowflake.ts | 8 + lib/arguments/tinyColor.ts | 10 + lib/automod/AutomodShared.ts | 310 + lib/automod/MemberAutomod.ts | 72 + lib/automod/MessageAutomod.ts | 286 + lib/automod/PresenceAutomod.ts | 85 + lib/badlinks.ts | 6930 ++++++++++++++++++++ lib/badwords.ts | 845 +++ lib/common/BushCache.ts | 26 + lib/common/ButtonPaginator.ts | 224 + lib/common/CanvasProgressBar.ts | 83 + lib/common/ConfirmationPrompt.ts | 64 + lib/common/DeleteButton.ts | 78 + lib/common/HighlightManager.ts | 488 ++ lib/common/Moderation.ts | 556 ++ lib/common/Sentry.ts | 24 + lib/common/tags.ts | 34 + .../discord-akairo/BushArgumentTypeCaster.ts | 3 + lib/extensions/discord-akairo/BushClient.ts | 600 ++ lib/extensions/discord-akairo/BushCommand.ts | 586 ++ .../discord-akairo/BushCommandHandler.ts | 37 + lib/extensions/discord-akairo/BushInhibitor.ts | 19 + .../discord-akairo/BushInhibitorHandler.ts | 3 + lib/extensions/discord-akairo/BushListener.ts | 3 + .../discord-akairo/BushListenerHandler.ts | 3 + lib/extensions/discord-akairo/BushTask.ts | 3 + lib/extensions/discord-akairo/BushTaskHandler.ts | 3 + lib/extensions/discord-akairo/SlashMessage.ts | 3 + lib/extensions/discord.js/BushClientEvents.ts | 200 + lib/extensions/discord.js/ExtendedGuild.ts | 919 +++ lib/extensions/discord.js/ExtendedGuildMember.ts | 1255 ++++ lib/extensions/discord.js/ExtendedMessage.ts | 12 + lib/extensions/discord.js/ExtendedUser.ts | 35 + lib/extensions/global.ts | 13 + lib/index.ts | 56 + lib/models/BaseModel.ts | 13 + lib/models/instance/ActivePunishment.ts | 94 + lib/models/instance/Guild.ts | 431 ++ lib/models/instance/Highlight.ts | 81 + lib/models/instance/Level.ts | 70 + lib/models/instance/ModLog.ts | 127 + lib/models/instance/Reminder.ts | 84 + lib/models/instance/StickyRole.ts | 58 + lib/models/shared/Global.ts | 67 + lib/models/shared/GuildCount.ts | 38 + lib/models/shared/MemberCount.ts | 37 + lib/models/shared/Shared.ts | 84 + lib/models/shared/Stat.ts | 72 + lib/tsconfig.json | 9 + lib/types/BushInspectOptions.ts | 123 + lib/types/CodeBlockLang.ts | 311 + lib/utils/AllowedMentions.ts | 68 + lib/utils/Arg.ts | 192 + lib/utils/BushClientUtils.ts | 499 ++ lib/utils/BushConstants.ts | 531 ++ lib/utils/BushLogger.ts | 315 + lib/utils/BushUtils.ts | 613 ++ lib/utils/Format.ts | 119 + lib/utils/Minecraft.ts | 351 + lib/utils/Minecraft_Test.ts | 86 + misc/test.js | 365 ++ misc/test.png | Bin 0 -> 41015 bytes misc/tooltips.nnb | 118 + package.json | 14 +- src/arguments/abbreviatedNumber.ts | 13 - src/arguments/contentWithDuration.ts | 5 - src/arguments/discordEmoji.ts | 14 - src/arguments/duration.ts | 5 - src/arguments/durationSeconds.ts | 6 - src/arguments/globalUser.ts | 7 - src/arguments/index.ts | 10 - src/arguments/messageLink.ts | 20 - src/arguments/permission.ts | 12 - src/arguments/roleWithDuration.ts | 17 - src/arguments/snowflake.ts | 8 - src/arguments/tinyColor.ts | 10 - src/bot.ts | 6 +- src/commands/admin/channelPermissions.ts | 2 +- src/commands/dev/test.ts | 6 +- src/commands/info/help.ts | 2 +- src/commands/moderation/massEvidence.ts | 2 +- src/commands/moderation/myLogs.ts | 2 +- src/commands/moderation/unmute.ts | 2 +- src/commands/moulberry-bush/neuRepo.ts | 2 +- src/commands/moulberry-bush/rule.ts | 2 +- src/commands/utilities/calculator.ts | 2 +- src/commands/utilities/highlight-block.ts | 2 +- src/commands/utilities/highlight-unblock.ts | 2 +- src/commands/utilities/uuid.ts | 2 +- src/commands/utilities/wolframAlpha.ts | 2 + src/context-menu-commands/message/viewRaw.ts | 2 +- src/context-menu-commands/user/modlog.ts | 2 +- src/context-menu-commands/user/userInfo.ts | 2 +- src/lib/badlinks.ts | 6930 -------------------- src/lib/badwords.ts | 752 --- src/lib/common/AutoMod.ts | 529 -- src/lib/common/ButtonPaginator.ts | 219 - src/lib/common/ConfirmationPrompt.ts | 64 - src/lib/common/DeleteButton.ts | 78 - src/lib/common/HighlightManager.ts | 485 -- src/lib/common/Sentry.ts | 24 - src/lib/common/tags.ts | 34 - src/lib/common/typings/BushInspectOptions.ts | 123 - src/lib/common/typings/CodeBlockLang.ts | 311 - src/lib/common/util/Arg.ts | 192 - src/lib/common/util/Format.ts | 119 - src/lib/common/util/Minecraft.ts | 349 - src/lib/common/util/Minecraft_Test.ts | 86 - src/lib/common/util/Moderation.ts | 556 -- .../discord-akairo/BushArgumentTypeCaster.ts | 3 - src/lib/extensions/discord-akairo/BushClient.ts | 586 -- src/lib/extensions/discord-akairo/BushCommand.ts | 586 -- .../discord-akairo/BushCommandHandler.ts | 37 - src/lib/extensions/discord-akairo/BushInhibitor.ts | 19 - .../discord-akairo/BushInhibitorHandler.ts | 3 - src/lib/extensions/discord-akairo/BushListener.ts | 3 - .../discord-akairo/BushListenerHandler.ts | 3 - src/lib/extensions/discord-akairo/BushTask.ts | 3 - .../extensions/discord-akairo/BushTaskHandler.ts | 3 - src/lib/extensions/discord-akairo/SlashMessage.ts | 3 - src/lib/extensions/discord.js/BushClientEvents.ts | 200 - src/lib/extensions/discord.js/ExtendedGuild.ts | 916 --- .../extensions/discord.js/ExtendedGuildMember.ts | 1255 ---- src/lib/extensions/discord.js/ExtendedMessage.ts | 12 - src/lib/extensions/discord.js/ExtendedUser.ts | 35 - src/lib/extensions/global.ts | 13 - src/lib/index.ts | 53 - src/lib/models/BaseModel.ts | 13 - src/lib/models/instance/ActivePunishment.ts | 94 - src/lib/models/instance/Guild.ts | 422 -- src/lib/models/instance/Highlight.ts | 81 - src/lib/models/instance/Level.ts | 70 - src/lib/models/instance/ModLog.ts | 127 - src/lib/models/instance/Reminder.ts | 84 - src/lib/models/instance/StickyRole.ts | 58 - src/lib/models/shared/Global.ts | 67 - src/lib/models/shared/GuildCount.ts | 39 - src/lib/models/shared/MemberCount.ts | 38 - src/lib/models/shared/Shared.ts | 84 - src/lib/models/shared/Stat.ts | 72 - src/lib/utils/AllowedMentions.ts | 68 - src/lib/utils/BushCache.ts | 26 - src/lib/utils/BushClientUtils.ts | 498 -- src/lib/utils/BushConstants.ts | 531 -- src/lib/utils/BushLogger.ts | 315 - src/lib/utils/BushUtils.ts | 612 -- src/lib/utils/CanvasProgressBar.ts | 83 - src/listeners/automod/automodCreate.ts | 16 + src/listeners/automod/automodUpdate.ts | 17 + src/listeners/automod/memberAutomod.ts | 21 + src/listeners/automod/presenceAutomod.ts | 27 + src/listeners/commands/commandError.ts | 4 +- src/listeners/interaction/interactionCreate.ts | 4 +- src/listeners/member-custom/bushLevelUpdate.ts | 6 +- src/listeners/message/automodCreate.ts | 15 - src/listeners/message/automodUpdate.ts | 17 - src/tasks/cache/updateCache.ts | 4 +- src/tasks/cache/updateHighlightCache.ts | 4 +- src/tasks/cache/updatePriceItemCache.ts | 7 +- src/tasks/feature/handleReminders.ts | 3 +- src/tasks/stats/guildCount.ts | 2 +- src/tsconfig.json | 9 + test.js | 365 -- test.png | Bin 41015 -> 0 bytes tests/arguments/abbreviatedNumber.test.ts | 4 +- tooltips.nnb | 118 - tsconfig.base.json | 67 + tsconfig.eslint.json | 19 +- tsconfig.json | 33 +- 183 files changed, 19172 insertions(+), 18695 deletions(-) create mode 100644 config/tsconfig.json create mode 100644 lib/arguments/abbreviatedNumber.ts create mode 100644 lib/arguments/contentWithDuration.ts create mode 100644 lib/arguments/discordEmoji.ts create mode 100644 lib/arguments/duration.ts create mode 100644 lib/arguments/durationSeconds.ts create mode 100644 lib/arguments/globalUser.ts create mode 100644 lib/arguments/index.ts create mode 100644 lib/arguments/messageLink.ts create mode 100644 lib/arguments/permission.ts create mode 100644 lib/arguments/roleWithDuration.ts create mode 100644 lib/arguments/snowflake.ts create mode 100644 lib/arguments/tinyColor.ts create mode 100644 lib/automod/AutomodShared.ts create mode 100644 lib/automod/MemberAutomod.ts create mode 100644 lib/automod/MessageAutomod.ts create mode 100644 lib/automod/PresenceAutomod.ts create mode 100644 lib/badlinks.ts create mode 100644 lib/badwords.ts create mode 100644 lib/common/BushCache.ts create mode 100644 lib/common/ButtonPaginator.ts create mode 100644 lib/common/CanvasProgressBar.ts create mode 100644 lib/common/ConfirmationPrompt.ts create mode 100644 lib/common/DeleteButton.ts create mode 100644 lib/common/HighlightManager.ts create mode 100644 lib/common/Moderation.ts create mode 100644 lib/common/Sentry.ts create mode 100644 lib/common/tags.ts create mode 100644 lib/extensions/discord-akairo/BushArgumentTypeCaster.ts create mode 100644 lib/extensions/discord-akairo/BushClient.ts create mode 100644 lib/extensions/discord-akairo/BushCommand.ts create mode 100644 lib/extensions/discord-akairo/BushCommandHandler.ts create mode 100644 lib/extensions/discord-akairo/BushInhibitor.ts create mode 100644 lib/extensions/discord-akairo/BushInhibitorHandler.ts create mode 100644 lib/extensions/discord-akairo/BushListener.ts create mode 100644 lib/extensions/discord-akairo/BushListenerHandler.ts create mode 100644 lib/extensions/discord-akairo/BushTask.ts create mode 100644 lib/extensions/discord-akairo/BushTaskHandler.ts create mode 100644 lib/extensions/discord-akairo/SlashMessage.ts create mode 100644 lib/extensions/discord.js/BushClientEvents.ts create mode 100644 lib/extensions/discord.js/ExtendedGuild.ts create mode 100644 lib/extensions/discord.js/ExtendedGuildMember.ts create mode 100644 lib/extensions/discord.js/ExtendedMessage.ts create mode 100644 lib/extensions/discord.js/ExtendedUser.ts create mode 100644 lib/extensions/global.ts create mode 100644 lib/index.ts create mode 100644 lib/models/BaseModel.ts create mode 100644 lib/models/instance/ActivePunishment.ts create mode 100644 lib/models/instance/Guild.ts create mode 100644 lib/models/instance/Highlight.ts create mode 100644 lib/models/instance/Level.ts create mode 100644 lib/models/instance/ModLog.ts create mode 100644 lib/models/instance/Reminder.ts create mode 100644 lib/models/instance/StickyRole.ts create mode 100644 lib/models/shared/Global.ts create mode 100644 lib/models/shared/GuildCount.ts create mode 100644 lib/models/shared/MemberCount.ts create mode 100644 lib/models/shared/Shared.ts create mode 100644 lib/models/shared/Stat.ts create mode 100644 lib/tsconfig.json create mode 100644 lib/types/BushInspectOptions.ts create mode 100644 lib/types/CodeBlockLang.ts create mode 100644 lib/utils/AllowedMentions.ts create mode 100644 lib/utils/Arg.ts create mode 100644 lib/utils/BushClientUtils.ts create mode 100644 lib/utils/BushConstants.ts create mode 100644 lib/utils/BushLogger.ts create mode 100644 lib/utils/BushUtils.ts create mode 100644 lib/utils/Format.ts create mode 100644 lib/utils/Minecraft.ts create mode 100644 lib/utils/Minecraft_Test.ts create mode 100644 misc/test.js create mode 100644 misc/test.png create mode 100644 misc/tooltips.nnb delete mode 100644 src/arguments/abbreviatedNumber.ts delete mode 100644 src/arguments/contentWithDuration.ts delete mode 100644 src/arguments/discordEmoji.ts delete mode 100644 src/arguments/duration.ts delete mode 100644 src/arguments/durationSeconds.ts delete mode 100644 src/arguments/globalUser.ts delete mode 100644 src/arguments/index.ts delete mode 100644 src/arguments/messageLink.ts delete mode 100644 src/arguments/permission.ts delete mode 100644 src/arguments/roleWithDuration.ts delete mode 100644 src/arguments/snowflake.ts delete mode 100644 src/arguments/tinyColor.ts delete mode 100644 src/lib/badlinks.ts delete mode 100644 src/lib/badwords.ts delete mode 100644 src/lib/common/AutoMod.ts delete mode 100644 src/lib/common/ButtonPaginator.ts delete mode 100644 src/lib/common/ConfirmationPrompt.ts delete mode 100644 src/lib/common/DeleteButton.ts delete mode 100644 src/lib/common/HighlightManager.ts delete mode 100644 src/lib/common/Sentry.ts delete mode 100644 src/lib/common/tags.ts delete mode 100644 src/lib/common/typings/BushInspectOptions.ts delete mode 100644 src/lib/common/typings/CodeBlockLang.ts delete mode 100644 src/lib/common/util/Arg.ts delete mode 100644 src/lib/common/util/Format.ts delete mode 100644 src/lib/common/util/Minecraft.ts delete mode 100644 src/lib/common/util/Minecraft_Test.ts delete mode 100644 src/lib/common/util/Moderation.ts delete mode 100644 src/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts delete mode 100644 src/lib/extensions/discord-akairo/BushClient.ts delete mode 100644 src/lib/extensions/discord-akairo/BushCommand.ts delete mode 100644 src/lib/extensions/discord-akairo/BushCommandHandler.ts delete mode 100644 src/lib/extensions/discord-akairo/BushInhibitor.ts delete mode 100644 src/lib/extensions/discord-akairo/BushInhibitorHandler.ts delete mode 100644 src/lib/extensions/discord-akairo/BushListener.ts delete mode 100644 src/lib/extensions/discord-akairo/BushListenerHandler.ts delete mode 100644 src/lib/extensions/discord-akairo/BushTask.ts delete mode 100644 src/lib/extensions/discord-akairo/BushTaskHandler.ts delete mode 100644 src/lib/extensions/discord-akairo/SlashMessage.ts delete mode 100644 src/lib/extensions/discord.js/BushClientEvents.ts delete mode 100644 src/lib/extensions/discord.js/ExtendedGuild.ts delete mode 100644 src/lib/extensions/discord.js/ExtendedGuildMember.ts delete mode 100644 src/lib/extensions/discord.js/ExtendedMessage.ts delete mode 100644 src/lib/extensions/discord.js/ExtendedUser.ts delete mode 100644 src/lib/extensions/global.ts delete mode 100644 src/lib/index.ts delete mode 100644 src/lib/models/BaseModel.ts delete mode 100644 src/lib/models/instance/ActivePunishment.ts delete mode 100644 src/lib/models/instance/Guild.ts delete mode 100644 src/lib/models/instance/Highlight.ts delete mode 100644 src/lib/models/instance/Level.ts delete mode 100644 src/lib/models/instance/ModLog.ts delete mode 100644 src/lib/models/instance/Reminder.ts delete mode 100644 src/lib/models/instance/StickyRole.ts delete mode 100644 src/lib/models/shared/Global.ts delete mode 100644 src/lib/models/shared/GuildCount.ts delete mode 100644 src/lib/models/shared/MemberCount.ts delete mode 100644 src/lib/models/shared/Shared.ts delete mode 100644 src/lib/models/shared/Stat.ts delete mode 100644 src/lib/utils/AllowedMentions.ts delete mode 100644 src/lib/utils/BushCache.ts delete mode 100644 src/lib/utils/BushClientUtils.ts delete mode 100644 src/lib/utils/BushConstants.ts delete mode 100644 src/lib/utils/BushLogger.ts delete mode 100644 src/lib/utils/BushUtils.ts delete mode 100644 src/lib/utils/CanvasProgressBar.ts create mode 100644 src/listeners/automod/automodCreate.ts create mode 100644 src/listeners/automod/automodUpdate.ts create mode 100644 src/listeners/automod/memberAutomod.ts create mode 100644 src/listeners/automod/presenceAutomod.ts delete mode 100644 src/listeners/message/automodCreate.ts delete mode 100644 src/listeners/message/automodUpdate.ts create mode 100644 src/tsconfig.json delete mode 100644 test.js delete mode 100644 test.png delete mode 100644 tooltips.nnb create mode 100644 tsconfig.base.json diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f62abfd..d246897 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -131,7 +131,7 @@ module.exports = { project: './tsconfig.eslint.json' }, plugins: ['@typescript-eslint', 'deprecation', 'import'], - ignorePatterns: ['dist'], + ignorePatterns: ['dist', 'node_modules'], rules: { 'no-return-await': 'off', '@typescript-eslint/no-empty-interface': 'warn', diff --git a/.gitignore b/.gitignore index 1ce2d89..2ec4b06 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ src/config/options.ts config/options.ts src/lib/badlinks-secret.ts +lib/badlinks-secret.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index bf4dd62..d86374b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,9 +6,8 @@ "**/CVS": true, "**/.DS_Store": true, "dist": false, - ".pnp.js": true, - "**/node_modules": true, - "**/dist/**/*.js.map": true + ".pnp.js": false, + "**/node_modules": true }, "javascript.preferences.importModuleSpecifier": "project-relative", "typescript.preferences.importModuleSpecifier": "project-relative", diff --git a/config/tsconfig.json b/config/tsconfig.json new file mode 100644 index 0000000..46b2d15 --- /dev/null +++ b/config/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../dist/config" + }, + "files": ["./Config.ts", "example-options.ts", "options.ts"] +} diff --git a/lib/arguments/abbreviatedNumber.ts b/lib/arguments/abbreviatedNumber.ts new file mode 100644 index 0000000..a7d8ce5 --- /dev/null +++ b/lib/arguments/abbreviatedNumber.ts @@ -0,0 +1,13 @@ +import type { BushArgumentTypeCaster } from '#lib'; +import assert from 'assert/strict'; +import numeral from 'numeral'; +assert(typeof numeral === 'function'); + +export const abbreviatedNumber: BushArgumentTypeCaster = (_, phrase) => { + if (!phrase) return null; + const num = numeral(phrase?.toLowerCase()).value(); + + if (typeof num !== 'number' || isNaN(num)) return null; + + return num; +}; diff --git a/lib/arguments/contentWithDuration.ts b/lib/arguments/contentWithDuration.ts new file mode 100644 index 0000000..0efba39 --- /dev/null +++ b/lib/arguments/contentWithDuration.ts @@ -0,0 +1,5 @@ +import { parseDuration, type BushArgumentTypeCaster, type ParsedDuration } from '#lib'; + +export const contentWithDuration: BushArgumentTypeCaster> = async (_, phrase) => { + return parseDuration(phrase); +}; diff --git a/lib/arguments/discordEmoji.ts b/lib/arguments/discordEmoji.ts new file mode 100644 index 0000000..92d6502 --- /dev/null +++ b/lib/arguments/discordEmoji.ts @@ -0,0 +1,14 @@ +import { regex, type BushArgumentTypeCaster } from '#lib'; +import type { Snowflake } from 'discord.js'; + +export const discordEmoji: BushArgumentTypeCaster = (_, phrase) => { + if (!phrase) return null; + const validEmoji: RegExpExecArray | null = regex.discordEmoji.exec(phrase); + if (!validEmoji || !validEmoji.groups) return null; + return { name: validEmoji.groups.name, id: validEmoji.groups.id }; +}; + +export interface DiscordEmojiInfo { + name: string; + id: Snowflake; +} diff --git a/lib/arguments/duration.ts b/lib/arguments/duration.ts new file mode 100644 index 0000000..09dd3d5 --- /dev/null +++ b/lib/arguments/duration.ts @@ -0,0 +1,5 @@ +import { parseDuration, type BushArgumentTypeCaster } from '#lib'; + +export const duration: BushArgumentTypeCaster = (_, phrase) => { + return parseDuration(phrase).duration; +}; diff --git a/lib/arguments/durationSeconds.ts b/lib/arguments/durationSeconds.ts new file mode 100644 index 0000000..d8d6749 --- /dev/null +++ b/lib/arguments/durationSeconds.ts @@ -0,0 +1,6 @@ +import { parseDuration, type BushArgumentTypeCaster } from '#lib'; + +export const durationSeconds: BushArgumentTypeCaster = (_, phrase) => { + phrase += 's'; + return parseDuration(phrase).duration; +}; diff --git a/lib/arguments/globalUser.ts b/lib/arguments/globalUser.ts new file mode 100644 index 0000000..4324aa9 --- /dev/null +++ b/lib/arguments/globalUser.ts @@ -0,0 +1,7 @@ +import type { BushArgumentTypeCaster } from '#lib'; +import type { User } from 'discord.js'; + +// resolve non-cached users +export const globalUser: BushArgumentTypeCaster> = async (message, phrase) => { + return message.client.users.resolve(phrase) ?? (await message.client.users.fetch(`${phrase}`).catch(() => null)); +}; diff --git a/lib/arguments/index.ts b/lib/arguments/index.ts new file mode 100644 index 0000000..eebf0a2 --- /dev/null +++ b/lib/arguments/index.ts @@ -0,0 +1,10 @@ +export * from './abbreviatedNumber.js'; +export * from './contentWithDuration.js'; +export * from './discordEmoji.js'; +export * from './duration.js'; +export * from './durationSeconds.js'; +export * from './globalUser.js'; +export * from './messageLink.js'; +export * from './permission.js'; +export * from './roleWithDuration.js'; +export * from './snowflake.js'; diff --git a/lib/arguments/messageLink.ts b/lib/arguments/messageLink.ts new file mode 100644 index 0000000..c95e42d --- /dev/null +++ b/lib/arguments/messageLink.ts @@ -0,0 +1,20 @@ +import { BushArgumentTypeCaster, regex } from '#lib'; +import type { Message } from 'discord.js'; + +export const messageLink: BushArgumentTypeCaster> = async (message, phrase) => { + const match = new RegExp(regex.messageLink).exec(phrase); + if (!match || !match.groups) return null; + + const { guild_id, channel_id, message_id } = match.groups; + + if (!guild_id || !channel_id || message_id) return null; + + const guild = message.client.guilds.cache.get(guild_id); + if (!guild) return null; + + const channel = guild.channels.cache.get(channel_id); + if (!channel || (!channel.isTextBased() && !channel.isThread())) return null; + + const msg = await channel.messages.fetch(message_id).catch(() => null); + return msg; +}; diff --git a/lib/arguments/permission.ts b/lib/arguments/permission.ts new file mode 100644 index 0000000..98bfe74 --- /dev/null +++ b/lib/arguments/permission.ts @@ -0,0 +1,12 @@ +import type { BushArgumentTypeCaster } from '#lib'; +import { PermissionFlagsBits, type PermissionsString } from 'discord.js'; + +export const permission: BushArgumentTypeCaster = (_, phrase) => { + if (!phrase) return null; + phrase = phrase.toUpperCase().replace(/ /g, '_'); + if (!(phrase in PermissionFlagsBits)) { + return null; + } else { + return phrase as PermissionsString; + } +}; diff --git a/lib/arguments/roleWithDuration.ts b/lib/arguments/roleWithDuration.ts new file mode 100644 index 0000000..b97f205 --- /dev/null +++ b/lib/arguments/roleWithDuration.ts @@ -0,0 +1,17 @@ +import { Arg, BushArgumentTypeCaster, parseDuration } from '#lib'; +import type { Role } from 'discord.js'; + +export const roleWithDuration: BushArgumentTypeCaster> = async (message, phrase) => { + // eslint-disable-next-line prefer-const + let { duration, content } = parseDuration(phrase); + if (content === null || content === undefined) return null; + content = content.trim(); + const role = await Arg.cast('role', message, content); + if (!role) return null; + return { duration, role }; +}; + +export interface RoleWithDuration { + duration: number | null; + role: Role | null; +} diff --git a/lib/arguments/snowflake.ts b/lib/arguments/snowflake.ts new file mode 100644 index 0000000..b98a20f --- /dev/null +++ b/lib/arguments/snowflake.ts @@ -0,0 +1,8 @@ +import { BushArgumentTypeCaster, regex } from '#lib'; +import type { Snowflake } from 'discord.js'; + +export const snowflake: BushArgumentTypeCaster = (_, phrase) => { + if (!phrase) return null; + if (regex.snowflake.test(phrase)) return phrase; + return null; +}; diff --git a/lib/arguments/tinyColor.ts b/lib/arguments/tinyColor.ts new file mode 100644 index 0000000..148c078 --- /dev/null +++ b/lib/arguments/tinyColor.ts @@ -0,0 +1,10 @@ +import type { BushArgumentTypeCaster } from '#lib'; +import assert from 'assert/strict'; +import tinycolorModule from 'tinycolor2'; +assert(tinycolorModule); + +export const tinyColor: BushArgumentTypeCaster = (_message, phrase) => { + // if the phase is a number it converts it to hex incase it could be representing a color in decimal + const newPhase = isNaN(phrase as any) ? phrase : `#${Number(phrase).toString(16)}`; + return tinycolorModule(newPhase).isValid() ? newPhase : null; +}; diff --git a/lib/automod/AutomodShared.ts b/lib/automod/AutomodShared.ts new file mode 100644 index 0000000..5d031d0 --- /dev/null +++ b/lib/automod/AutomodShared.ts @@ -0,0 +1,310 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + GuildMember, + Message, + PermissionFlagsBits, + Snowflake +} from 'discord.js'; +import UnmuteCommand from '../../src/commands/moderation/unmute.js'; +import * as Moderation from '../common/Moderation.js'; +import { unmuteResponse } from '../extensions/discord.js/ExtendedGuildMember.js'; +import { colors, emojis } from '../utils/BushConstants.js'; +import * as Format from '../utils/Format.js'; + +/** + * Handles shared auto moderation functionality. + */ +export abstract class Automod { + /** + * Whether or not a punishment has already been given to the user + */ + protected punished = false; + + /** + * @param member The guild member that the automod is checking + */ + protected constructor(protected readonly member: GuildMember) {} + + /** + * The user + */ + protected get user() { + return this.member.user; + } + + /** + * The client instance + */ + protected get client() { + return this.member.client; + } + + /** + * The guild member that the automod is checking + */ + protected get guild() { + return this.member.guild; + } + + /** + * Whether or not the member should be immune to auto moderation + */ + protected get isImmune() { + if (this.member.user.isOwner()) return true; + if (this.member.guild.ownerId === this.member.id) return true; + if (this.member.permissions.has('Administrator')) return true; + + return false; + } + + protected buttons(userId: Snowflake, reason: string, undo = true): ActionRowBuilder { + const row = new ActionRowBuilder().addComponents([ + new ButtonBuilder({ + style: ButtonStyle.Danger, + label: 'Ban User', + customId: `automod;ban;${userId};${reason}` + }) + ]); + + if (undo) { + row.addComponents( + new ButtonBuilder({ + style: ButtonStyle.Success, + label: 'Unmute User', + customId: `automod;unmute;${userId}` + }) + ); + } + + return row; + } + + protected logColor(severity: Severity) { + switch (severity) { + case Severity.DELETE: + return colors.lightGray; + case Severity.WARN: + return colors.yellow; + case Severity.TEMP_MUTE: + return colors.orange; + case Severity.PERM_MUTE: + return colors.red; + } + throw new Error(`Unknown severity: ${severity}`); + } + + /** + * Checks if any of the words provided are in the message + * @param words The words to check for + * @returns The blacklisted words found in the message + */ + protected checkWords(words: BadWordDetails[], str: string): BadWordDetails[] { + if (words.length === 0) return []; + + const matchedWords: BadWordDetails[] = []; + for (const word of words) { + if (word.regex) { + if (new RegExp(word.match).test(this.format(word.match, word))) { + matchedWords.push(word); + } + } else { + if (this.format(str, word).includes(this.format(word.match, word))) { + matchedWords.push(word); + } + } + } + return matchedWords; + } + + /** + * Format a string according to the word options + * @param string The string to format + * @param wordOptions The word options to format with + * @returns The formatted string + */ + protected format(string: string, wordOptions: BadWordDetails) { + const temp = wordOptions.ignoreCapitalization ? string.toLowerCase() : string; + return wordOptions.ignoreSpaces ? temp.replace(/ /g, '') : temp; + } + + /** + * Handles the auto moderation + */ + protected abstract handle(): Promise; +} + +/** + * Handles the ban button in the automod log. + * @param interaction The button interaction. + */ +export async function handleAutomodInteraction(interaction: ButtonInteraction) { + if (!interaction.memberPermissions?.has(PermissionFlagsBits.BanMembers)) + return interaction.reply({ + content: `${emojis.error} You are missing the **Ban Members** permission.`, + ephemeral: true + }); + const [action, userId, reason] = interaction.customId.replace('automod;', '').split(';') as ['ban' | 'unmute', string, string]; + + if (!(['ban', 'unmute'] as const).includes(action)) throw new TypeError(`Invalid automod button action: ${action}`); + + const victim = await interaction.guild!.members.fetch(userId).catch(() => null); + const moderator = + interaction.member instanceof GuildMember ? interaction.member : await interaction.guild!.members.fetch(interaction.user.id); + + switch (action) { + case 'ban': { + if (!interaction.guild?.members.me?.permissions.has('BanMembers')) + return interaction.reply({ + content: `${emojis.error} I do not have permission to ${action} members.`, + ephemeral: true + }); + + const check = victim ? await Moderation.permissionCheck(moderator, victim, 'ban', true) : true; + if (check !== true) return interaction.reply({ content: check, ephemeral: true }); + + const result = await interaction.guild?.bushBan({ + user: userId, + reason, + moderator: interaction.user.id, + evidence: (interaction.message as Message).url ?? undefined + }); + + const victimUserFormatted = (await interaction.client.utils.resolveNonCachedUser(userId))?.tag ?? userId; + + const content = (() => { + if (result === unmuteResponse.SUCCESS) { + return `${emojis.success} Successfully banned ${Format.input(victimUserFormatted)}.`; + } else if (result === unmuteResponse.DM_ERROR) { + return `${emojis.warn} Banned ${Format.input(victimUserFormatted)} however I could not send them a dm.`; + } else { + return `${emojis.error} Could not ban ${Format.input(victimUserFormatted)}: \`${result}\` .`; + } + })(); + + return interaction.reply({ + content: content, + ephemeral: true + }); + } + + case 'unmute': { + if (!victim) + return interaction.reply({ + content: `${emojis.error} Cannot find member, they may have left the server.`, + ephemeral: true + }); + + if (!interaction.guild) + return interaction.reply({ + content: `${emojis.error} This is weird, I don't seem to be in the server...`, + ephemeral: true + }); + + const check = await Moderation.permissionCheck(moderator, victim, 'unmute', true); + if (check !== true) return interaction.reply({ content: check, ephemeral: true }); + + const check2 = await Moderation.checkMutePermissions(interaction.guild); + if (check2 !== true) return interaction.reply({ content: UnmuteCommand.formatCode('/', victim!, check2), ephemeral: true }); + + const result = await victim.bushUnmute({ + reason, + moderator: interaction.member as GuildMember, + evidence: (interaction.message as Message).url ?? undefined + }); + + const victimUserFormatted = victim.user.tag; + + const content = (() => { + if (result === unmuteResponse.SUCCESS) { + return `${emojis.success} Successfully unmuted ${Format.input(victimUserFormatted)}.`; + } else if (result === unmuteResponse.DM_ERROR) { + return `${emojis.warn} Unmuted ${Format.input(victimUserFormatted)} however I could not send them a dm.`; + } else { + return `${emojis.error} Could not unmute ${Format.input(victimUserFormatted)}: \`${result}\` .`; + } + })(); + + return interaction.reply({ + content: content, + ephemeral: true + }); + } + } +} + +/** + * The severity of the blacklisted word + */ +export const enum Severity { + /** + * Delete message + */ + DELETE, + + /** + * Delete message and warn user + */ + WARN, + + /** + * Delete message and mute user for 15 minutes + */ + TEMP_MUTE, + + /** + * Delete message and mute user permanently + */ + PERM_MUTE +} + +/** + * Details about a blacklisted word + */ +export interface BadWordDetails { + /** + * The word that is blacklisted + */ + match: string; + + /** + * The severity of the word + */ + severity: Severity | 1 | 2 | 3; + + /** + * Whether or not to ignore spaces when checking for the word + */ + ignoreSpaces: boolean; + + /** + * Whether or not to ignore case when checking for the word + */ + ignoreCapitalization: boolean; + + /** + * The reason that this word is blacklisted (used for the punishment reason) + */ + reason: string; + + /** + * Whether or not the word is regex + * @default false + */ + regex: boolean; + + /** + * Whether to also check a user's status and username for the phrase + * @default false + */ + userInfo: boolean; +} + +/** + * Blacklisted words mapped to their details + */ +export interface BadWords { + [category: string]: BadWordDetails[]; +} diff --git a/lib/automod/MemberAutomod.ts b/lib/automod/MemberAutomod.ts new file mode 100644 index 0000000..6f71457 --- /dev/null +++ b/lib/automod/MemberAutomod.ts @@ -0,0 +1,72 @@ +import { stripIndent } from '#tags'; +import { EmbedBuilder, GuildMember } from 'discord.js'; +import { Automod, BadWordDetails } from './AutomodShared.js'; + +export class MemberAutomod extends Automod { + /** + * @param member The member that the automod is checking + */ + public constructor(member: GuildMember) { + super(member); + + if (member.id === member.client.user?.id) return; + + void this.handle(); + } + + protected async handle(): Promise { + if (this.member.user.bot) return; + + const badWordsRaw = Object.values(this.client.utils.getShared('badWords')).flat(); + const customAutomodPhrases = (await this.guild.getSetting('autoModPhases')) ?? []; + + const phrases = [...badWordsRaw, ...customAutomodPhrases].filter((p) => p.userInfo); + + const result: BadWordDetails[] = []; + + const str = `${this.member.user.username}${this.member.nickname ? `\n${this.member.nickname}` : ''}`; + const check = this.checkWords(phrases, str); + if (check.length > 0) { + result.push(...check); + } + + if (result.length > 0) { + const highestOffense = result.sort((a, b) => b.severity - a.severity)[0]; + await this.logMessage(highestOffense, result, str); + } + } + + /** + * Log an automod infraction to the guild's specified automod log channel + * @param highestOffense The highest severity word found in the message + * @param offenses The other offenses that were also matched in the message + */ + protected async logMessage(highestOffense: BadWordDetails, offenses: BadWordDetails[], str: string) { + void this.client.console.info( + 'MemberAutomod', + `Detected a severity <<${highestOffense.severity}>> automod phrase in <<${this.user.tag}>>'s (<<${this.user.id}>>) username or nickname in <<${this.guild.name}>>` + ); + + const color = this.logColor(highestOffense.severity); + + await this.guild.sendLogChannel('automod', { + embeds: [ + new EmbedBuilder() + .setTitle(`[Severity ${highestOffense.severity}] Automoderated User Info Detected`) + .setDescription( + stripIndent` + **User:** ${this.user} (${this.user.tag}) + **Blacklisted Words:** ${offenses.map((o) => `\`${o.match}\``).join(', ')}` + ) + .addFields({ + name: 'Info', + value: `${await this.client.utils.codeblock(str, 1024)}` + }) + .setColor(color) + .setTimestamp() + .setAuthor({ name: this.user.tag, url: this.user.displayAvatarURL() }) + ], + components: [this.buttons(this.user.id, highestOffense.reason, false)] + }); + } +} diff --git a/lib/automod/MessageAutomod.ts b/lib/automod/MessageAutomod.ts new file mode 100644 index 0000000..9673adf --- /dev/null +++ b/lib/automod/MessageAutomod.ts @@ -0,0 +1,286 @@ +import { stripIndent } from '#tags'; +import assert from 'assert/strict'; +import chalk from 'chalk'; +import { EmbedBuilder, GuildTextBasedChannel, PermissionFlagsBits, type Message } from 'discord.js'; +import { colors } from '../utils/BushConstants.js'; +import { format, formatError } from '../utils/BushUtils.js'; +import { Automod, BadWordDetails, Severity } from './AutomodShared.js'; + +/** + * Handles message auto moderation functionality. + */ +export class MessageAutomod extends Automod { + /** + * @param message The message to check and potentially perform automod actions on + */ + public constructor(private readonly message: Message) { + assert(message.member); + super(message.member); + + if (message.author.id === message.client.user?.id) return; + void this.handle(); + } + + /** + * Handles the auto moderation + */ + protected async handle() { + if (!this.message.inGuild()) return; + if (!(await this.guild.hasFeature('automod'))) return; + if (this.user.bot) return; + if (!this.message.member) return; + + traditional: { + if (this.isImmune) break traditional; + const badLinksArray = this.client.utils.getShared('badLinks'); + const badLinksSecretArray = this.client.utils.getShared('badLinksSecret'); + const badWordsRaw = this.client.utils.getShared('badWords'); + + const customAutomodPhrases = (await this.guild.getSetting('autoModPhases')) ?? []; + const uniqueLinks = [...new Set([...badLinksArray, ...badLinksSecretArray])]; + + const badLinks: BadWordDetails[] = uniqueLinks.map((link) => ({ + match: link, + severity: Severity.PERM_MUTE, + ignoreSpaces: false, + ignoreCapitalization: true, + reason: 'malicious link', + regex: false, + userInfo: false + })); + + const parsedBadWords = Object.values(badWordsRaw).flat(); + + const result = this.checkWords( + [ + ...customAutomodPhrases, + ...((await this.guild.hasFeature('excludeDefaultAutomod')) ? [] : parsedBadWords), + ...((await this.guild.hasFeature('excludeAutomodScamLinks')) ? [] : badLinks) + ], + this.message.content + ); + + if (result.length === 0) break traditional; + + const highestOffense = result.sort((a, b) => b.severity - a.severity)[0]; + + if (highestOffense.severity === undefined || highestOffense.severity === null) { + void this.guild.sendLogChannel('error', { + embeds: [ + { + title: 'AutoMod Error', + description: `Unable to find severity information for ${format.inlineCode(highestOffense.match)}`, + color: colors.error + } + ] + }); + } else { + this.punish(highestOffense); + void this.logMessage(highestOffense, result); + } + } + + other: { + if (this.isImmune) break other; + if (!this.punished && (await this.guild.hasFeature('delScamMentions'))) void this.checkScamMentions(); + } + + if (!this.punished && (await this.guild.hasFeature('perspectiveApi'))) void this.checkPerspectiveApi(); + } + + /** + * If the message contains '@everyone' or '@here' and it contains a common scam phrase, it will be deleted + * @returns + */ + protected async checkScamMentions() { + const includes = (c: string) => this.message.content.toLocaleLowerCase().includes(c); + if (!includes('@everyone') && !includes('@here')) return; + + // It would be bad if we deleted a message that actually pinged @everyone or @here + if ( + this.member.permissionsIn(this.message.channelId).has(PermissionFlagsBits.MentionEveryone) || + this.message.mentions.everyone + ) + return; + + if ( + includes('steam') || + includes('www.youtube.com') || + includes('youtu.be') || + includes('nitro') || + includes('1 month') || + includes('3 months') || + includes('personalize your profile') || + includes('even more') || + includes('xbox and discord') || + includes('left over') || + includes('check this lol') || + includes('airdrop') + ) { + const color = this.logColor(Severity.PERM_MUTE); + this.punish({ severity: Severity.TEMP_MUTE, reason: 'everyone mention and scam phrase' } as BadWordDetails); + void this.guild!.sendLogChannel('automod', { + embeds: [ + new EmbedBuilder() + .setTitle(`[Severity ${Severity.TEMP_MUTE}] Mention Scam Deleted`) + .setDescription( + stripIndent` + **User:** ${this.user} (${this.user.tag}) + **Sent From:** <#${this.message.channel.id}> [Jump to context](${this.message.url})` + ) + .addFields({ + name: 'Message Content', + value: `${await this.client.utils.codeblock(this.message.content, 1024)}` + }) + .setColor(color) + .setTimestamp() + ], + components: [this.buttons(this.user.id, 'everyone mention and scam phrase')] + }); + } + } + + protected async checkPerspectiveApi() { + return; + if (!this.client.config.isDevelopment) return; + + if (!this.message.content) return; + this.client.perspective.comments.analyze( + { + key: this.client.config.credentials.perspectiveApiKey, + resource: { + comment: { + text: this.message.content + }, + requestedAttributes: { + TOXICITY: {}, + SEVERE_TOXICITY: {}, + IDENTITY_ATTACK: {}, + INSULT: {}, + PROFANITY: {}, + THREAT: {}, + SEXUALLY_EXPLICIT: {}, + FLIRTATION: {} + } + } + }, + (err: any, response: any) => { + if (err) return console.log(err?.message); + + const normalize = (val: number, min: number, max: number) => (val - min) / (max - min); + + const color = (val: number) => { + if (val >= 0.5) { + const x = 194 - Math.round(normalize(val, 0.5, 1) * 194); + return chalk.rgb(194, x, 0)(val); + } else { + const x = Math.round(normalize(val, 0, 0.5) * 194); + return chalk.rgb(x, 194, 0)(val); + } + }; + + console.log(chalk.cyan(this.message.content)); + Object.entries(response.data.attributeScores) + .sort(([a], [b]) => a.localeCompare(b)) + .forEach(([key, value]: any[]) => console.log(chalk.white(key), color(value.summaryScore.value))); + } + ); + } + + /** + * Punishes the user based on the severity of the offense + * @param highestOffense The highest offense to punish the user for + * @returns The color of the embed that the log should, based on the severity of the offense + */ + protected punish(highestOffense: BadWordDetails) { + switch (highestOffense.severity) { + case Severity.DELETE: { + void this.message.delete().catch((e) => deleteError.bind(this, e)); + this.punished = true; + break; + } + case Severity.WARN: { + void this.message.delete().catch((e) => deleteError.bind(this, e)); + void this.member.bushWarn({ + moderator: this.guild!.members.me!, + reason: `[Automod] ${highestOffense.reason}` + }); + this.punished = true; + break; + } + case Severity.TEMP_MUTE: { + void this.message.delete().catch((e) => deleteError.bind(this, e)); + void this.member.bushMute({ + moderator: this.guild!.members.me!, + reason: `[Automod] ${highestOffense.reason}`, + duration: 900_000 // 15 minutes + }); + this.punished = true; + break; + } + case Severity.PERM_MUTE: { + void this.message.delete().catch((e) => deleteError.bind(this, e)); + void this.member.bushMute({ + moderator: this.guild!.members.me!, + reason: `[Automod] ${highestOffense.reason}`, + duration: 0 // permanent + }); + this.punished = true; + break; + } + default: { + throw new Error(`Invalid severity: ${highestOffense.severity}`); + } + } + + async function deleteError(this: MessageAutomod, e: Error | any) { + void this.guild?.sendLogChannel('error', { + embeds: [ + { + title: 'Automod Error', + description: `Unable to delete triggered message.`, + fields: [{ name: 'Error', value: await this.client.utils.codeblock(`${formatError(e)}`, 1024, 'js', true) }], + color: colors.error + } + ] + }); + } + } + + /** + * Log an automod infraction to the guild's specified automod log channel + * @param highestOffense The highest severity word found in the message + * @param offenses The other offenses that were also matched in the message + */ + protected async logMessage(highestOffense: BadWordDetails, offenses: BadWordDetails[]) { + void this.client.console.info( + 'MessageAutomod', + `Severity <<${highestOffense.severity}>> action performed on <<${this.user.tag}>> (<<${this.user.id}>>) in <<#${ + (this.message.channel as GuildTextBasedChannel).name + }>> in <<${this.guild!.name}>>` + ); + + const color = this.logColor(highestOffense.severity); + + await this.guild!.sendLogChannel('automod', { + embeds: [ + new EmbedBuilder() + .setTitle(`[Severity ${highestOffense.severity}] Automod Action Performed`) + .setDescription( + stripIndent` + **User:** ${this.user} (${this.user.tag}) + **Sent From:** <#${this.message.channel.id}> [Jump to context](${this.message.url}) + **Blacklisted Words:** ${offenses.map((o) => `\`${o.match}\``).join(', ')}` + ) + .addFields({ + name: 'Message Content', + value: `${await this.client.utils.codeblock(this.message.content, 1024)}` + }) + .setColor(color) + .setTimestamp() + .setAuthor({ name: this.user.tag, url: this.user.displayAvatarURL() }) + ], + components: highestOffense.severity >= 2 ? [this.buttons(this.user.id, highestOffense.reason)] : undefined + }); + } +} diff --git a/lib/automod/PresenceAutomod.ts b/lib/automod/PresenceAutomod.ts new file mode 100644 index 0000000..70c66d6 --- /dev/null +++ b/lib/automod/PresenceAutomod.ts @@ -0,0 +1,85 @@ +import { stripIndent } from '#tags'; +import { EmbedBuilder, Presence } from 'discord.js'; +import { Automod, BadWordDetails } from './AutomodShared.js'; + +export class PresenceAutomod extends Automod { + /** + * @param presence The presence that the automod is checking + */ + public constructor(public readonly presence: Presence) { + super(presence.member!); + + if (presence.member!.id === presence.client.user?.id) return; + + void this.handle(); + } + + protected async handle(): Promise { + if (this.presence.member!.user.bot) return; + + const badWordsRaw = Object.values(this.client.utils.getShared('badWords')).flat(); + const customAutomodPhrases = (await this.guild.getSetting('autoModPhases')) ?? []; + + const phrases = [...badWordsRaw, ...customAutomodPhrases].filter((p) => p.userInfo); + + const result: BadWordDetails[] = []; + + const strings = []; + + for (const activity of this.presence.activities) { + const str = `${activity.name}${activity.details ? `\n${activity.details}` : ''}${ + activity.buttons.length > 0 ? `\n${activity.buttons.join('\n')}` : '' + }`; + const check = this.checkWords(phrases, str); + if (check.length > 0) { + result.push(...check); + strings.push(str); + } + } + + if (result.length > 0) { + const highestOffense = result.sort((a, b) => b.severity - a.severity)[0]; + await this.logMessage(highestOffense, result, strings); + } + } + + /** + * Log an automod infraction to the guild's specified automod log channel + * @param highestOffense The highest severity word found in the message + * @param offenses The other offenses that were also matched in the message + */ + protected async logMessage(highestOffense: BadWordDetails, offenses: BadWordDetails[], strings: string[]) { + void this.client.console.info( + 'PresenceAutomod', + `Detected a severity <<${highestOffense.severity}>> automod phrase in <<${this.user.tag}>>'s (<<${this.user.id}>>) presence in <<${this.guild.name}>>` + ); + + const color = this.logColor(highestOffense.severity); + + await this.guild.sendLogChannel('automod', { + embeds: [ + new EmbedBuilder() + .setTitle(`[Severity ${highestOffense.severity}] Automoderated Status Detected`) + .setDescription( + stripIndent` + **User:** ${this.user} (${this.user.tag}) + **Blacklisted Words:** ${offenses.map((o) => `\`${o.match}\``).join(', ')}` + ) + .addFields( + ( + await Promise.all( + strings.map(async (s) => ({ + name: 'Status', + value: `${await this.client.utils.codeblock(s, 1024)}` + })) + ) + ).slice(0, 25) + ) + .setColor(color) + .setTimestamp() + .setAuthor({ name: this.user.tag, url: this.user.displayAvatarURL() }) + ], + components: [this.buttons(this.user.id, highestOffense.reason, false)] + }); + } +} diff --git a/lib/badlinks.ts b/lib/badlinks.ts new file mode 100644 index 0000000..3b4cf3b --- /dev/null +++ b/lib/badlinks.ts @@ -0,0 +1,6930 @@ +/* Links in this file are treated as severity 3 offences. + +made in part possible by https://github.com/nacrt/SkyblockClient-REPO/blob/main/files/scamlinks.json */ +export default [ + "//iscord.gift", + "100cs.ru", + "100eshopdeals.com", + "101nitro.com", + "12mon.space", + "1nitro.club", + "2021cs.net.ru", + "2021ga.xyz", + "2021liss.ru", + "2021pn.ru", + "2021y.ru", + "2022p.ru", + "2022yg.com", + "2023g.com", + "23c7481e.hbrex.cn", + "2discord.ru", + "2faceteam.ml", + "3ds-security.xyz", + "3items4rocket.com", + "4drop.ru.com", + "academynaviagg.xyz", + "accountauthorization.xyz", + "acercup.com", + "ach2x.net.ru", + "achnavi.net.ru", + "acid-tournament.ru", + "affix-cup.click", + "affix-cup.link", + "affix-cup.ru", + "affix-sport.ru", + "affixesports.ru", + "affixsport.ru", + "afkskroll.ru", + "ahijeoir.ru", + "airdrop-discord.com", + "airdrop-discord.online", + "airdrop-discord.ru", + "airdrop-nitro.com", + "airdrops.tips", + "akellasport.me", + "aladdinhub.fun", + "alexandrkost.ru", + "alexs1.ru", + "alive-lives.ru", + "allskinz.xyz", + "alm-gaming.com", + "alone18.ru", + "alonemoly.ru", + "amaterasu.pp.ua", + "ano-skinspin.xyz", + "anomalygiveaways.pro", + "anomalyknifes.xyz", + "anomalyskin.xyz", + "anomalyskinz.xyz", + "anoskinzz.xyz", + "antibot.cc", + "aoeah.promo-codes.world", + "aoeah.shop", + "api.code2gether.cf", + "api.innovations-urfu.site", + "app-discord.com", + "app-discord.ru", + "app-nitro.com", + "application-discord.com", + "appnitro-discord.com", + "appnitro-discord.ru.com", + "appnitrodiscord.ru.com", + "apps-discord.org", + "apps-nitro.com", + "arik.pp.ua", + "asprod911.com", + "asstralissport.org.ru", + "astr-teem.net.ru", + "astr-teem.org.ru", + "astralis-gg.com", + "astralis.monster", + "astralis2.net.ru", + "astralis2.org.ru", + "astralisgift.fun", + "astrallis.net.ru", + "astrallis.org.ru", + "astralliscase.org.ru", + "astralteam.org.ru", + "astresports.xyz", + "atomicstore.ru", + "attaxtrade.com", + "aucryptohubs.com", + "authnet.cf", + "autumnbot.cloud", + "avitofast.ru", + "awirabigmoneyroll.xyz", + "awirabigmoneyrolls.xyz", + "azimovcase.tk", + "badge-team.ml", + "ball-chaser.xyz", + "bandycazez.xyz", + "bangbro.ru", + "battiefy.com", + "beast-cup.ru", + "beast-dr0p.ru", + "beast-winer.ru", + "belekevskeigames.xyz", + "berrygamble.com", + "best-cup.com", + "best-cup.ru", + "bestgeeknavi.ru", + "bestshopusaoffers.com", + "bestskins.org.ru", + "beststeam.gq", + "bestwatchstyle.com", + "beta.discorder.app", + "betadiscord.com", + "bets-cup.ru", + "big.org.ru", + "big.pp.ru", + "bigcsgo.pro", + "bigesports.ru", + "bigmoneyrollawira.xyz", + "bigs.monster", + "bigsports.xyz", + "bistripudel.xyz", + "bit-skins.ru", + "bitcoingenerator.cash", + "bitknife.xyz", + "bitskeansell.ru", + "bitskines.ru", + "blockmincnain.com", + "blocknimchain.com", + "blocksilcnain.com", + "blox.land", + "bloxpromo.com", + "blustcoin.com", + "board-nitro.com", + "bondikflas.xyz", + "bonusxcase.xyz", + "books-pash.org.ru", + "boost-discord.com", + "boost-nitro.com", + "boosted-nitro.com", + "boostnitro.com", + "boostnltro.com", + "bountyweek.com", + "box-surprisebynavi.net.ru", + "boxgolg.club", + "boxnode.ru", + "br0ken-fng.xyz", + "bracesports.ru", + "bro-skiils.net.ru", + "brokenfang-csgo.com", + "brokenfangpassfree.pp.ru", + "brokenfant.org.ru", + "brokentournament.xyz", + "bruteclub.ru", + "buff-market.ru", + "buffgames.ru", + "but-three.xyz", + "buxquick.com", + "buzz-cup.ru", + "bycdu.cam", + "bycsdu.cam", + "bysellers.xyz", + "c-you-mamont.ru", + "c2bit.online", + "c2bit.su", + "case-free.com", + "case-gift.com", + "case-give.com", + "case-magic.space", + "casecs.ru", + "casefire.fun", + "casekey.ru.com", + "casesdrop.ru", + "casesdrop.xyz", + "cash.org.ru", + "cash.pp.ru", + "cashcsgo.ru", + "cashout.monster", + "cashy.monster", + "cassesoma.ru", + "cave-nitro.com", + "cawanmei.ru", + "cawanmei99.ru", + "ccomstimoon.org.ru", + "cgsell.ru", + "cgskinky.xyz", + "chainexplo.com", + "challengeme.in", + "challengeme.vip", + "challengme.ru", + "chance-stem.ru", + "chinchopa.pp.ua", + "circus-shop.ru", + "cis-fastcup.ru", + "cis-rankig.ru", + "cityofmydream.pp.ua", + "claim.robuxat.com", + "claimgifts.shop", + "clan-big.ru", + "classic-nitro.com", + "claud9.xyz", + "clck.ru", + "click-mell.pp.ru", + "cliscord-gift.ru.com", + "cllscordapp.fun", + "cloud9.ru.com", + "cloud9team.space", + "cloudeskins.com", + "cloudfox.one", + "cloudteam9.com", + "clove-nitro.com", + "cmepure.com", + "cmskillcup.com", + "cod3r0bux.pw", + "cointradebtc.com", + "comboline.xyz", + "comdiscord.com", + "come-nitro.com", + "communitytradeoffer.com.ru", + "communitytradeoffer.com", + "communltydrop.pp.ua", + "communltyguard.pp.ua", + "comsteamcommunity.com", + "contact-infoservice.com", + "contralav.ru", + "contralav.xyz", + "coolcools.xyz", + "cooldrop.monster", + "copyrightbusinessgroup.com", + "copyrightbussinessgroup.com", + "copyrighthelpbusiness.org", + "cose-lore.ru", + "counter-stricke.ru", + "counter-strlke.site", + "counterbase.ru.com", + "counterpaid.xyz", + "counterspin.top", + "counterstrik.xyz", + "counterstrikegift.xyz", + "cpanel.copyrighthelpbusiness.org", + "cpbldi.com", + "cpp-discord.com", + "crazy-soom.org.ru", + "crazypage.me", + "creack.tk", + "creditscpfree.website", + "crosflah.online", + "crustalcup.ga", + "cs-activit.xyz", + "cs-astria.xyz", + "cs-beast.xyz", + "cs-betway.xyz", + "cs-boom.org.ru", + "cs-cool.net.ru", + "cs-dark.org.ru", + "cs-dump.org.ru", + "cs-esports.link", + "cs-exeword.xyz", + "cs-fail.ru.com", + "cs-fall.ru.com", + "cs-gameis.ru", + "cs-gorun.ru.com", + "cs-grun.ru.com", + "cs-incursed.xyz", + "cs-legend.xyz", + "cs-lucky.xyz", + "cs-moneyy.ru", + "cs-navigiveaway.ru", + "cs-open.link", + "cs-pill.xyz", + "cs-play.org.ru", + "cs-prizeskins.xyz", + "cs-prizeskinz.xyz", + "cs-riptide.com", + "cs-riptide.ru", + "cs-riptide.xyz", + "cs-simpleroll.xyz", + "cs-skins.link", + "cs-skinz.xyz", + "cs-smoke.xyz", + "cs-spinz.xyz", + "cs-toom.pp.ru", + "cs-tournament.link", + "cs-victory.xyz", + "cs11go.space", + "cs4real.pp.ua", + "cs500go.com", + "csallskin.xyz", + "csbuyskins.in", + "cschanse.ru", + "cschecker.ru", + "cscoat.eu", + "cscodes.ru", + "csfair.pp.ua", + "csfix.me", + "csfreedom.me", + "csfreesklns.ru.com", + "csgameik.ru", + "csgdrop.ru", + "csgfocusa.ru", + "csggolg.ru", + "csgif.org.ru", + "csgift.fun", + "csgo-analyst.com", + "csgo-battle.ru", + "csgo-cash.eu", + "csgo-cup.ru", + "csgo-cyber.link", + "csgo-dym.ru", + "csgo-fute.net.ru", + "csgo-game-steam.ru", + "csgo-games.xyz", + "csgo-gamesteam.ru", + "csgo-gifts.com", + "csgo-lute.net.ru", + "csgo-market.ru.com", + "csgo-pell.org.ru", + "csgo-riptide.ru", + "csgo-run.info", + "csgo-run.site", + "csgo-sports.com", + "csgo-st.ru", + "csgo-steam-game.ru", + "csgo-steam-good.ru", + "csgo-steamanalyst.net", + "csgo-steamgame.ru", + "csgo-steamplay.ru", + "csgo-store-steam.ru", + "csgo-storesteam.ru", + "csgo-swapskin.com", + "csgo-trade.net", + "csgo-up.com", + "csgo-z.com", + "csgo.ghservers.cl", + "csgo2021.ru", + "csgo4cases.fun", + "csgobb.xyz", + "csgobccp.ru", + "csgobeats.com", + "csgobelieve.ru", + "csgocase.monster", + "csgocase.one", + "csgocases.monster", + "csgocashs.com", + "csgocheck.ru.com", + "csgocheck.ru", + "csgochinasteam.ru", + "csgocj-steam.work", + "csgocnfocuss.ru", + "csgocompetive.com", + "csgocup.ru", + "csgocupp.ru.com", + "csgocybersport.ru.com", + "csgodetails.info", + "csgodirect.xyz", + "csgodreamer.com", + "csgodrops.monster", + "csgodrs.com", + "csgoeasywin.ru.com", + "csgoelite.xyz", + "csgoencup.com", + "csgoevent.xyz", + "csgofast.xyz", + "csgoflash.net.ru", + "csgofocusc.xyz", + "csgogame-steam.ru", + "csgoganeak.ru", + "csgoganeik.ru", + "csgogf01.xyz", + "csgogf02.xyz", + "csgogf03.xyz", + "csgogf04.xyz", + "csgogf05.xyz", + "csgogf06.xyz", + "csgogf07.xyz", + "csgogf12.xyz", + "csgogf13.xyz", + "csgogf14.xyz", + "csgogf15.xyz", + "csgogift25.xyz", + "csgogift26.xyz", + "csgogift34.xyz", + "csgogift43.xyz", + "csgogift44.xyz", + "csgogift45.xyz", + "csgogift47.xyz", + "csgogift49.xyz", + "csgogift50.xyz", + "csgogift51.xyz", + "csgogift55.xyz", + "csgogift56.xyz", + "csgogift57.xyz", + "csgogift58.xyz", + "csgogift59.xyz", + "csgogift60.xyz", + "csgogift62.xyz", + "csgogift77.xyz", + "csgogpusk.ru", + "csgoindex.ru.com", + "csgoindex.ru", + "csgoitemdetails.com", + "csgoitemsprices.com", + "csgojs.xyz", + "csgojump.ru", + "csgoko.tk", + "csgold.monster", + "csgomarble.xyz", + "csgomarketplace.net", + "csgomarkets.net", + "csgonavi.com", + "csgoorun.ru", + "csgoprocupgo.com", + "csgorcup.com", + "csgoroll.ru", + "csgorose.com", + "csgoroulette.monster", + "csgoroyalskins1.com", + "csgorun-rubonus.ru", + "csgorun.info", + "csgorun.pro-login.ru", + "csgorun.pro-loginn.com", + "csgosell.xyz", + "csgoskill.ru", + "csgoskinprices.com", + "csgoskinsinfo.com", + "csgoskinsroll.com", + "csgosprod.com", + "csgossteam.ru", + "csgossteam.xyz", + "csgostats.fun", + "csgosteam-game.ru", + "csgosteam-play.ru", + "csgosteamanalysis.com", + "csgosteamanalyst.ru", + "csgosteamcom.ru", + "csgosteamgo.ru", + "csgoteammate.gq", + "csgothunby.com", + "csgotournaments.cf", + "csgotrades.net", + "csgotreder.com", + "csgovip.ru", + "csgowans.ru", + "csgowaycup.ru.com", + "csgowincase.xyz", + "csgoworkshops.com", + "csgoxgiveaway.ru", + "csgozone.net.in", + "csgunskins.xyz", + "cslpkmf.ru", + "csm-oney.ru", + "csmarkete.info", + "csmone-y.ru", + "csmoneyskinz.xyz", + "csmvcecup.com", + "csogamech.xyz", + "csogamecm.xyz", + "csogamee.xyz", + "csogamef.xyz", + "csogamegg.ru", + "csogameke.xyz", + "csoggskif.ru", + "csoggskif.xyz", + "csogzhnc.xyz", + "csprices.in", + "csrandom.monster", + "css500gggo.ru", + "csskill.com", + "csskillpro.xyz", + "csskins.space", + "csskinz.xyz", + "csteamskin.ru", + "cstournament.ru", + "cswanmei.ru", + "cswanmei4.ru", + "cswinterpresent.xyz", + "csxrnoney.com", + "cteamcamnynity67823535672.xyz", + "cteamcommunity.xyz", + "cubesmc.ru", + "cupcs.ru", + "cupcsgo.ru", + "cupgoo.xyz", + "cupsul.ru", + "cupwin.xyz", + "cyber-csgo.link", + "cyber-csgo.space", + "cyber-lan.com", + "cyber-roll.club", + "cyber-roll.monster", + "cyber-shok.online", + "cyber-shok.ru", + "cyber-win.ru", + "cyber-x.xyz", + "cybercsgo.link", + "cyberdex.ru", + "cyberegocscom.ru", + "cyberesports-tournaments.ru", + "cybergamearena.ru", + "cyberiaevents.ru", + "cyberlev.ru", + "cybermode.ru", + "cyberscsgo.ru", + "cyberspark.org.ru", + "d-nitro.tk", + "d.iscord.xyz", + "d.myticks.xyz", + "d1scord.xyz", + "d1scrod.site", + "d2csbox.pp.ua", + "d2cups.com", + "d2faceit.com", + "d3l3.tk", + "dac-game.xyz", + "daddsda.xyz", + "dailymegadeal.xyz", + "dawbab.xyz", + "daxrop.xyz", + "dciscord.com", + "ddiscord.com", + "deadisidddde.xyz", + "deamonbets.ru", + "def-dclss.pp.ua", + "demonbets.ru", + "denforapasi.cf", + "der-csgo.ru", + "derimonz.xyz", + "derwoood.xyz", + "desmond.ru.com", + "determined-haslett.45-138-72-103.plesk.page", + "dfiscord.com", + "diablobets.com", + "diacordapp.com", + "diascord.com", + "diccrd.com", + "dicksod.co", + "dicoapp.me", + "dicoapp.pro", + "dicord.gg", + "dicord.gift", + "dicord.site", + "dicord.space", + "dicordapp.com", + "dicordgift.ru.com", + "dicordglfts.ga", + "dicordglfts.gq", + "dicovrd.com", + "dicrod.com", + "dicscordapp.com", + "dicsocrd.com", + "dicsord-airdrop.com", + "dicsord-airdrop.ru", + "dicsord-app.com", + "dicsord-events.com", + "dicsord-gift.com", + "dicsord-gifte.ru.com", + "dicsord-gifted.ru", + "dicsord-gifts.ru", + "dicsord-give.com", + "dicsord-give.ru", + "dicsord-gives.com", + "dicsord-hypesquads.com", + "dicsord-nitro.com", + "dicsord-nitro.ru", + "dicsord-steam.com", + "dicsord-ticket.com", + "dicsord.gg", + "dicsord.gifts", + "dicsord.net", + "dicsord.pl", + "dicsord.pw", + "dicsord.ru", + "dicsord.space", + "dicsord.website", + "dicsordapp.co", + "dicsordgift.club", + "dicsordgift.com", + "dicsordgive.ru.com", + "dicsordnitro.info", + "dicsordnitro.store", + "dicsordr.xyz", + "dicsords-gift.ru", + "dicsords.ru", + "dicsrod.com", + "didiscord.com", + "didscord.com", + "diiiscrod.club", + "diisccord.club", + "diiscord-app.com", + "diiscord-gift.com", + "diiscord-nittro.ru", + "diiscord.com", + "dIiscord.com", + "diiscord.gift", + "diiscord.me", + "diiscordapp.com", + "diisscord.club", + "diisscord.online", + "dijscord.com", + "dilscord.com", + "dioscord.com", + "diqscordapp.com", + "dircode.ru", + "direct-link.net", + "dirolzz.xyz", + "dirscod.com", + "dirscod.gift", + "dirscord-gift.ru", + "dirscordapp.com", + "dis.cord.gifts", + "disbordapp.com", + "disbords.com", + "disbored.com", + "disc-ord.com", + "disc.cool", + "disc.gifts", + "disc0rd-app.ru.com", + "disc0rd-nitro.site", + "disc0rd.org", + "disc0rd.site", + "disc0rd.xyz", + "discapp.info", + "discard.gg", + "discard.gift", + "discard.xyz", + "discardapp.fun", + "disccor.com", + "disccord-apps.com", + "disccord-appss.ru", + "disccord-club.com", + "disccord-gift.com", + "disccord.gg", + "disccord.ru.com", + "disccord.ru", + "disccord.shop", + "disccord.tk", + "disccords.com", + "disccrd.gifts", + "disccrdapp.com", + "disceord.gift", + "discerd.gift", + "discford.com", + "discgrdapp.com", + "dischrd.com", + "discird.gg", + "discird.me", + "discjrd.com", + "disckord.com", + "disckordapp.com", + "disclord.com", + "disclrd.com", + "discnrd.gift", + "discnrdapp.com", + "disco.to", + "disco3d.app", + "disco9rdapp.com", + "discoapps.club", + "discoard.com", + "discocd.com", + "discocdapp.com", + "discocl.xyz", + "discoclapp.xyz", + "discocord.com", + "discocrd-gift.com", + "discocrd-gifts.com", + "discocrd-nitro.com", + "discocrd.gift", + "discocrd.gifts", + "discocrdapp.com", + "discod-hitro.xyz", + "discod-nitro.ru", + "discod.art", + "discod.fun", + "discod.gift", + "discod.gifts", + "discod.info", + "discod.tech", + "discodapp.gift", + "discodapp.net", + "discode.gift", + "discodnitro.info", + "discodnitro.ru", + "discodrd.com", + "discoed.gg", + "discoed.me", + "discoerd.com", + "discoerdapp.com", + "discofd.com", + "discokrd.com", + "discold.online", + "discold.ru", + "discolrd.com", + "discond-nitro.ru", + "discond-njtro.tech", + "discond.gift", + "discond.ru.com", + "discondapp.fun", + "disconrd.com", + "discontro.ru", + "discoogs.com", + "discoord-apps.com", + "discoord-nitro.com", + "discoord.space", + "discor-dnitro.fun", + "discor.de", + "discor.gg", + "discor.link", + "discor.me", + "discorad.com", + "discorapp.gq", + "discorapp.pw", + "discorb-nitro.ru.com", + "discorb.blog", + "discorb.co", + "discorb.com", + "discorb.gift", + "discorb.gifts", + "discorb.ru.com", + "discorc-nitro.site", + "discorcd-apps.com", + "discorcd-gift.com", + "discorcd-nitro.com", + "discorcd.click", + "discorcd.com", + "discorcd.gift", + "discorcd.gifts", + "discorcd.site", + "discorcdapp.com", + "discorci.com", + "discorcl-air.xyz", + "discorcl-app.com", + "discorcl-app.ru", + "discorcl-app.xyz", + "discorcl-boost.ru", + "discorcl-gift.org.ru", + "discorcl-gift.ru.com", + "discorcl-gift.ru", + "discorcl-gift.xyz", + "discorcl-give.site", + "discorcl-nitro.com", + "discorcl-nitro.ru.com", + "discorcl-nitro.site", + "discorcl.app", + "discorcl.art", + "discorcl.click", + "discorcl.club", + "discorcl.fun", + "discorcl.ga", + "discorcl.gift", + "discorcl.gifts", + "discorcl.info", + "discorcl.link", + "discorcl.online", + "discorcl.ru.com", + "discorcl.ru", + "discorcl.shop", + "discorcl.site", + "discorcl.store", + "discorclapp.com", + "discorclapp.fun", + "discorclgift.com", + "discorclgift.xyz", + "discorcll.com", + "discorcll.online", + "discorclnitro.ru", + "discorclsteam.com", + "discorcrd.gift", + "discorcz-booster.ru", + "discord-a.com", + "discord-accept.com", + "discord-accounts.com", + "discord-accounts.ru", + "discord-air.fun", + "discord-air.pw", + "discord-air.xyz", + "discord-airclrop.pw", + "discord-airdop.link", + "discord-airdrop.com", + "discord-airdrop.fun", + "discord-airdrop.info", + "discord-airdrop.me", + "discord-airdrop.pw", + "discord-airdrop.site", + "discord-airdrop.xyz", + "discord-airnitro.xyz", + "discord-alidrop.me", + "discord-alrdrop.com", + "discord-app.cc", + "discord-app.click", + "discord-app.club", + "discord-app.co.uk", + "discord-app.co", + "discord-app.gift", + "discord-app.gifts", + "discord-app.info", + "discord-app.io", + "discord-app.live", + "discord-app.me", + "discord-app.net", + "discord-app.ru.com", + "discord-app.shop", + "discord-app.store", + "discord-app.su", + "discord-app.top", + "discord-app.uk", + "discord-app.us", + "discord-app.xyz", + "discord-application.com", + "discord-applications.com", + "discord-apply.com", + "discord-appnitro.com", + "discord-apps.ru", + "discord-apps.site", + "discord-apps.space", + "discord-apps.xyz", + "discord-best-nitro.xyz", + "discord-bonus.ru", + "discord-boost.com", + "discord-boost.ru.com", + "discord-boost.ru", + "discord-boost.xyz", + "discord-bot.com", + "discord-bot.ru", + "discord-bugs.com", + "discord-claim.com", + "discord-claim.ru.com", + "discord-claim.ru", + "discord-clap.com", + "discord-click.shop", + "discord-club.ru", + "discord-com-free.online", + "discord-com-free.ru", + "discord-control.com", + "discord-controls.com", + "discord-cpp.com", + "discord-develop.com", + "discord-developer.com", + "discord-devs.com", + "discord-do.com", + "discord-dr0p.ru", + "discord-drop.gift", + "discord-drop.info", + "discord-drop.xyz", + "discord-drops.ru", + "discord-egift.com", + "discord-event.com", + "discord-event.info", + "discord-events.com", + "discord-exploits.tk", + "discord-faq.com", + "discord-free-nitro.ru", + "discord-free.com", + "discord-free.site", + "discord-freenitro.online", + "discord-freenitro.pw", + "discord-fun.com", + "discord-game.com", + "discord-games.cf", + "discord-generator.tk", + "discord-get.click", + "discord-get.ru", + "discord-gg.com", + "discord-gg.ru.com", + "discord-gif.xyz", + "discord-gifft.com", + "discord-gift-free-nitro.tk", + "discord-gift-nitro.site", + "discord-gift.app", + "discord-gift.info", + "discord-gift.net.ru", + "discord-gift.online", + "discord-gift.ru.com", + "discord-gift.ru", + "discord-gift.shop", + "discord-gift.site", + "discord-gift.top", + "discord-gift.us", + "discord-gifte.com", + "discord-gifte.ru", + "discord-gifte.xyz", + "discord-gifted.ru.com", + "discord-giftef.xyz", + "discord-gifteh.xyz", + "discord-giftes.com", + "discord-gifts.com.ru", + "discord-gifts.com", + "discord-gifts.me", + "discord-gifts.org", + "discord-gifts.ru.com", + "discord-gifts.shop", + "discord-gifts.site", + "discord-givaewey.ru", + "discord-give.com", + "discord-give.net", + "discord-give.org", + "discord-give.pw", + "discord-give.ru.com", + "discord-give.ru", + "discord-give.xyz", + "discord-giveaway.com", + "discord-giveaways.ru", + "discord-glft.com", + "discord-glft.ru.com", + "discord-glft.xyz", + "discord-halloween-nitro.com", + "discord-halloween.com", + "discord-halloween.link", + "discord-halloween.me", + "discord-halloween.ru.com", + "discord-halloween.ru", + "discord-hallowen.ru.com", + "discord-help.com", + "discord-helpers.com", + "discord-hse.com", + "discord-hype.com", + "discord-hypeevent.com", + "discord-hypes.com", + "discord-hypesquad.com", + "discord-hypesquad.info", + "discord-hypesquade.com", + "discord-hypesquaders.com", + "discord-hypesquads.com", + "discord-hypevent.com", + "discord-i.com", + "discord-info.com", + "discord-infoapp.xyz", + "discord-information.com", + "discord-information.ru", + "discord-informations.com", + "discord-informations.ru", + "discord-install.com", + "discord-invite-link.com", + "discord-job.com", + "discord-jobs.com", + "discord-list.cf", + "discord-load.ru", + "discord-login.cf", + "discord-mega.xyz", + "discord-mod.com", + "discord-moderation.com", + "discord-moderator.com", + "discord-moderator.us", + "discord-mods.com", + "discord-net-labs.com", + "discord-netro.ru", + "discord-news.com", + "discord-niittro.ru", + "discord-nilro.ru", + "discord-niltro.com", + "discord-niltro.ru.com", + "discord-nitr0gift.fun", + "discord-nitre.xyz", + "discord-nitro-boost.xyz", + "discord-nitro-classic.com", + "discord-nitro-free.ml", + "discord-nitro-free.ru", + "discord-nitro-free.xyz", + "discord-nitro.click", + "discord-nitro.cloud", + "discord-nitro.club", + "discord-nitro.co", + "discord-nitro.com", + "discord-nitro.eu", + "discord-nitro.gift", + "discord-nitro.gifts", + "discord-nitro.info", + "discord-nitro.it", + "discord-nitro.link", + "discord-nitro.live", + "discord-nitro.net", + "discord-nitro.online", + "discord-nitro.org", + "discord-nitro.pro", + "discord-nitro.ru.com", + "discord-nitro.services", + "discord-nitro.shop", + "discord-nitro.store", + "discord-nitro.su", + "discord-nitro.tech", + "discord-nitro.tk", + "discord-nitro.website", + "discord-nitroapp.ru", + "discord-nitroapp.xyz", + "discord-nitrodrop.xyz", + "discord-nitroe.xyz", + "discord-nitrogift.com", + "discord-nitrogift.ru", + "discord-nitrogift.xyz", + "discord-nitros.com", + "discord-nitros.ru", + "discord-nitrot.xyz", + "discord-njtro.store", + "discord-nltro.com", + "discord-nltro.fun", + "discord-nltro.info", + "discord-nltro.ru", + "discord-nudes.club", + "discord-nudes.live", + "discord-o.com", + "discord-offer.com", + "discord-partner.com", + "discord-partners.com", + "discord-premium.com", + "discord-present.ru", + "discord-promo.com", + "discord-promo.info", + "discord-promo.ru.com", + "discord-promo.site", + "discord-promo.xyz", + "discord-promotions.com", + "discord-promox.com", + "discord-report.com", + "discord-ro.tk", + "discord-ru.site", + "discord-security.com", + "discord-service.com", + "discord-sex.live", + "discord-shop.fun", + "discord-sms.eu", + "discord-soft.ru", + "discord-spooky.ru", + "discord-staff.com", + "discord-stat.com", + "discord-stats.com", + "discord-stats.org", + "discord-steam.com", + "discord-steam.ru", + "discord-steam.site", + "discord-steams.com", + "discord-stemdrop.me", + "discord-stuff.com", + "discord-sup.com", + "discord-support.com", + "discord-support.org", + "discord-support.tech", + "discord-supports.com", + "discord-team.com", + "discord-tech.com", + "discord-tester.com", + "discord-to.com", + "discord-true.com", + "discord-trustandsafety.com", + "discord-up.ru", + "discord-verif.ga", + "discord-verification.com", + "discord-verifications.com", + "discord-verify-account.ml", + "discord-verify.com", + "discord-verify.ru", + "discord-vetify.com", + "discord-web.co", + "discord-xnitro.com", + "discord.1nitro.club", + "discord.ac", + "discord.app.br", + "discord.app", + "discord.bargains", + "discord.best", + "discord.biz", + "discord.blog", + "discord.cc", + "discord.cloud", + "discord.cm", + "discord.cn.com", + "discord.co.com", + "discord.co.in", + "discord.co.za", + "discord.com.pl", + "discord.com.tw", + "discord.cool", + "discord.creditcard", + "discord.deals", + "discord.download", + "discord.es", + "discord.eu", + "discord.family", + "discord.fit", + "discord.foundation", + "discord.fyi", + "discord.gifte", + "discord.givaeway.com", + "discord.givaewey.com", + "discord.giveawey.com", + "discord.giveaweys.com", + "discord.glfte.com", + "discord.gq", + "discord.homes", + "discord.in", + "discord.istanbul", + "discord.limited", + "discord.ltd", + "discord.luxe", + "discord.marketing", + "discord.moscow", + "discord.my", + "dIscord.net", + "discord.online", + "discord.org.ru", + "discord.porn", + "discord.pp.ru", + "discord.promo", + "discord.pt", + "discord.ru.net", + "discord.shop", + "discord.si", + "discord.team", + "discord.tools", + "discord.tw", + "discord.world", + "discord2fa.com", + "discord404.com", + "discord4nitro.com", + "discordaap.com", + "discordacc2.repl.co", + "discordadp.com", + "discordadpp.com", + "discordaepp.com", + "discordalt4.repl.co", + "discordalt5.repl.co", + "discordalts293.repl.co", + "discordaoo.com", + "discordaop.com", + "discordapp.best", + "discordapp.biz", + "discordapp.click", + "discordapp.cloud", + "discordapp.co.uk", + "discordapp.eu", + "discordapp.gg", + "discordapp.help", + "discordapp.ir", + "discordapp.org", + "discordapp.pages.dev", + "discordapp.pw", + "discordapp.rip", + "discordapp.ru.com", + "discordapp.social", + "discordapp.store", + "discordapp.support", + "discordapp.top", + "discordapp.us", + "discordapp.vercel.app", + "discordapp.vip", + "discordapp.ws", + "discordappi.fun", + "discordapplication.com", + "discordapplication.xyz", + "discordapplications.com", + "discordappo.com", + "discordappp.com", + "discordappp.net", + "discordappporn.chat", + "discordapps.gift", + "discordapps.gifts", + "discordapps.tk", + "discordappss.com", + "discordaspp.com", + "discordbagequiz.cf", + "discordbeta.com", + "discordbetter.app", + "discordboost.net", + "discordbooster.com", + "discordbothost.com", + "discordbotist.com", + "discordbots.app", + "discordbugs.com", + "discordc.gift", + "discordcanary.com", + "discordcdn.sa.com", + "discordcharity.org", + "discordcheats.net", + "discordclgift.net.ru", + "discordcommunlty.com", + "discordcrasher.wtf", + "discordcreators.net", + "discordd.buzz", + "discordd.gg", + "discordd.gift", + "discorddaapp.com", + "discorddev.com", + "discorddevelopment.com", + "discorddevs.com", + "discorddiscord.com", + "discorddrop.com", + "discorde-gift.com", + "discorde-gifte.com", + "discorde-nitro.com", + "discorde.gift", + "discorde.xyz", + "discordevents.com", + "discordf.com", + "discordf.gift", + "discordfree.com", + "discordfrnitro.site", + "discordg.com.ru", + "discordg.link", + "discordgame.com", + "discordgamers.co.uk", + "discordgft.com", + "discordgg.com", + "discordgif.com", + "discordgift.app", + "discordgift.com", + "discordgift.fun", + "discordgift.info", + "discordgift.net.ru", + "discordgift.org", + "discordgift.pw", + "discordgift.ru.com", + "discordgift.ru", + "discordgift.site", + "discordgift.tk", + "discordgift.xyz", + "discordgifte.site", + "discordgifted.xyz", + "discordgiftis.ru", + "discordgifts-pay.ru.com", + "discordgifts-pay.ru", + "discordgifts.co.uk", + "discordgifts.com", + "discordgifts.fun", + "discordgifts.info", + "discordgifts.link", + "discordgifts.me", + "discordgifts.ru.com", + "discordgifts.ru", + "discordgifts.site", + "discordgifts.store", + "discordgiftss.com", + "discordgiftsteam.ru", + "discordgiftz.xyz", + "discordgive.ru.com", + "discordgive.ru", + "discordgiveaway.fun", + "discordgivenitro.com", + "discordgivenitro.ru.com", + "discordglft.com", + "discordglft.ru", + "discordglfts.com", + "discordglfts.xyz", + "discordhalloween.co.uk", + "discordhalloween.com", + "discordhalloween.gift", + "discordhalloween.uk", + "discordi.gift", + "discordiapp.fun", + "discordiatech.co.uk", + "discordicon.com", + "discordimages.com", + "discordinfo.com", + "discordinfo.ru", + "discordinvite.ml", + "discordist.com", + "discordj.gift", + "discordjob.com", + "discordjs.tech", + "discordl-steam.com", + "discordl.com", + "discordl.pw", + "discordl.site", + "discordl.xyz", + "discordlapp.fun", + "discordlgift.com", + "discordlgift.ru.com", + "discordlinks.co.uk", + "discordlist.repl.co", + "discordlive.xyz", + "discordll.gift", + "discordlogin.com", + "discordmac.com", + "discordme.me", + "discordmoderations.com", + "discordn.com", + "discordn.gift", + "discordnitro-gift.com", + "discordnitro-steam.ru", + "discordnitro.altervista.org", + "discordnitro.biz", + "discordnitro.cc", + "discordnitro.click", + "discordnitro.club", + "discordnitro.com", + "dIscordnitro.com", + "discordnitro.fun", + "discordnitro.gift", + "discordnitro.info", + "discordnitro.link", + "discordnitro.ru.com", + "discordnitro.space", + "discordnitro.store", + "discordnitro.su", + "discordnitro9.repl.co", + "discordnitroapp.ru.com", + "discordnitroevent.info", + "discordnitrofree.com", + "discordnitrofree.xyz", + "discordnitrogenerator.com", + "discordnitrogift.com", + "discordnitrogift.ru", + "discordnitrogifts.pl", + "discordnitrolink.tk", + "discordnitropromo.site", + "discordnitros.gifts", + "discordnitros.xyz", + "discordnitrosteam.com", + "discordnltro.com", + "discordobs.com", + "discordp.com", + "discordp.ml", + "discordpap.com", + "discordpp.com", + "discordprize.xyz", + "discordpromo.site", + "discordq.com", + "discordqapp.com", + "discordqpp.com", + "discordqr.com", + "discordre.store", + "discordresearch.com", + "discordrgift.com", + "discordrgift.online", + "discordrgift.ru", + "discords-accounts.ru", + "discords-app.com", + "discords-dev.ga", + "discords-developers.com", + "discords-events.com", + "discords-gift.com", + "discords-gift.ru", + "discords-gifte.ru", + "discords-gifts.club", + "discords-gifts.ru", + "discords-glft.com", + "discords-hypes.com", + "discords-hypesquad.com", + "discords-hypesquads.com", + "discords-moderation.com", + "discords-moderator.com", + "discords-nitro.com", + "discords-nitro.site", + "discords-nitro.xyz", + "discords-nitroapp.xyz", + "discords-nitros.fun", + "discords-nitros.shop", + "discords-premium.com", + "discords-premium.site", + "discords-steam.com", + "discords-support.com", + "discords-teams.com", + "discords.biz", + "discords.co.uk", + "discords.company", + "discords.gifts", + "discords.net", + "discords.ru.com", + "discords.ru", + "discords.us", + "discordsapi.com", + "discordsapp.fun", + "discordsapp.xyz", + "discordsapplication.info", + "discordsatus.com", + "discordsearch.co", + "discordservice.com", + "discordsex.live", + "discordsgift.com", + "discordsgift.info", + "discordshort.ga", + "discordsite.repl.co", + "discordsnitro.com", + "discordsnitro.store", + "discordsnitros.one", + "discordspp.com", + "discordss.ru", + "discordstaff.xyz", + "discordstat.com", + "discordsteam.com", + "discordsteam.ru", + "discordsteams.com", + "discordsub.com", + "discordsupport.gg", + "discordt.gift", + "discordtest.xyz", + "discordtesters.com", + "discordtext.com", + "discordtoken.com", + "discordtokens.shop", + "discordtokens2.repl.co", + "discordtos.com", + "discordtotal.com", + "discordtotal.net", + "discordtts.com", + "discordtw.com", + "discordu.gift", + "discordup.ru", + "discordx.link", + "discordx.ml", + "discordxgift.xyz", + "discordxnitro.xyz", + "discordxsteam.com", + "discoredapp.com", + "discorfd.com", + "discorg.gg", + "discorgift.online", + "discorgift.xyz", + "discorid.gift", + "discoril.com", + "discorl.com", + "discorld-gift.site", + "discorld.com", + "discorld.site", + "discorlgifts.store", + "discorll.com", + "discornd.com", + "discorrd.com", + "discorrd.gift", + "discorrd.link", + "discorrd.ru", + "discorrd.site", + "discorrdapp.com", + "discorrl.com", + "discorsd.com", + "discorsd.gifts", + "discort-nitro.com", + "discort.com", + "discort.site", + "discortnitosteam.online", + "discortnitostem.online", + "discosd.com", + "discosrd.com", + "discotdapp.com", + "discourd.com", + "discourd.info", + "discourd.site", + "discourdapp.com", + "discovd.com", + "discpordapp.com", + "discprd.com", + "discqorcl.com", + "discrd.co", + "discrd.gg", + "discrdapp.cf", + "discrdapp.com", + "discrds.gift", + "discrdspp.com", + "discrocl.xyz", + "discrod-app.com", + "discrod-app.ru", + "discrod-app.site", + "discrod-apps.ru", + "discrod-gift.com", + "discrod-gifte.com", + "discrod-gifts.club", + "discrod-glfts.com", + "discrod-nitro.fun", + "discrod-nitro.info", + "discrod-up.ru", + "discrod.gg", + "discrod.gift", + "discrod.gifts", + "discrod.pw", + "discrod.ru", + "discrodapp.ru", + "discrodapp.site", + "discrodapp.xyz", + "discrode-app.club", + "discrode-app.com", + "discrode-gift.club", + "discrode-gift.com", + "discrode-gifte.club", + "discrode.gift", + "discrodnitro.org", + "discrodnitro.ru", + "discrods.gift", + "discrods.site", + "discrodsteam.online", + "discrodsteam.ru", + "discrodup.ru", + "discrord.com", + "discrordapp.com", + "discsord.com", + "discsrdapp.com", + "discurcd.com", + "discurd.js.org", + "discvordapp.com", + "discxordapp.com", + "disdrop.com.br", + "disinfo.org.ru", + "disiscord.com", + "diskord.gg", + "diskord.org.ru", + "diskord.ru.com", + "dislcord.com", + "disocordapp.com", + "disocr.com", + "disocrd-gift.com", + "disocrd-gift.ru", + "disocrd.co", + "disocrd.codes", + "disocrd.gg", + "disocrd.gifts", + "disocrd.me", + "disocrd.org", + "disocrd.ru", + "disocrd.tk", + "disocrdapp.com", + "disocrde.gift", + "disocrds.gift", + "disorc.com", + "disord.co", + "disord.codes", + "disord.fun", + "disord.gift", + "disord.gifts", + "disordapp.gift", + "disordapp.gifts", + "disorde.gift", + "disordgift.codes", + "disordgifts.com", + "disordglft.com", + "disordnitros.gifts", + "disordnitros.xyz", + "disordnltro.xyz", + "disordnltros.com", + "disordnltros.com", + "disordnltros.gifts", + "disords.gift", + "disordsnitro.gifts", + "disordsnitros.gifts", + "disrcod.com", + "disrcod.gift", + "disrcod.gifts", + "disrcord.com", + "disscord.com", + "disscord.gift", + "disscord.online", + "disscord.ru", + "disscords.club", + "dissord.com", + "dissord.gift", + "dissord.ru", + "diswcord.com", + "disxcord.com", + "disxord.com", + "diszcord.com", + "diszcordapp.com", + "diucord.js.org", + "diuscordapp.com", + "divinegardens.xyx", + "diwcord.com", + "dixcord.com", + "dixscord.com", + "dizcord.app", + "dizcord.com", + "dizcord.gift", + "dizscord.com", + "djiscord.com", + "djscord.com", + "dkscord.com", + "dlcord.gift", + "dlcsorcl.com", + "dlcsorcl.ru", + "dlcsord-airdrop.com", + "dlcsord-gift.com", + "dlicord-glfts.site", + "dlicsord.ru", + "dliscord-gift.com", + "dliscord-gift.ru.com", + "dliscord-gifts.com", + "dliscord-giveaway.ru", + "dliscord-glft.ru.com", + "dliscord-nitro.com", + "dliscord.com", + "dliscord.gift", + "dliscord.us", + "dliscordl.com", + "dliscordnltro.com", + "dliscords.com", + "dliscrd.one", + "dlisocrd.ru", + "dllscord.online", + "dlscard.ru", + "dlsccord-app.club", + "dlsccord-apps.club", + "dlsccrd.com", + "dlscocrd.club", + "dlscocrd.com", + "dlscocrdapp.com", + "dlscorcl-apps.com", + "dlscorcl.gift", + "dlscorcl.info", + "dlscorcl.ru.com", + "dlscorcl.ru", + "dlscorcl.shop", + "dlscorcl.xyz", + "dlscorclapp.fun", + "dlscord-alirdrop.com", + "dlscord-alirdrop.site", + "dlscord-app.com", + "dlscord-app.info", + "dlscord-app.net", + "dlscord-app.ru.com", + "dlscord-app.ru", + "dlscord-app.su", + "dlscord-app.xyz", + "dlscord-apps.com", + "dlscord-boost.fun", + "dlscord-claim.com", + "dlscord-developer.com", + "dlscord-game.com", + "dlscord-gift.com", + "dlscord-gift.one", + "dlscord-gift.ru.com", + "dlscord-gift.xyz", + "dlscord-gifts.com", + "dlscord-gifts.xyz", + "dlscord-glft.pw", + "dlscord-glft.ru.com", + "dlscord-glft.xyz", + "dlscord-glfts.xyz", + "dlscord-halloween.ru", + "dlscord-hypesquad.com", + "dlscord-hypesquads.com", + "dlscord-inventory.fun", + "dlscord-nitro.click", + "dlscord-nitro.fun", + "dlscord-nitro.info", + "dlscord-nitro.link", + "dlscord-nitro.ru.com", + "dlscord-nitro.space", + "dlscord-nitro.store", + "dlscord-nltro.com", + "dlscord-nltro.ru", + "dlscord-nltro.xyz", + "dlscord-promo.xyz", + "dlscord-spooky.ru", + "dlscord-steam.com", + "dlscord-stime-2021.ru", + "dlscord-store.club", + "dlscord-support.com", + "dlscord.app", + "dlscord.art", + "dlscord.blog", + "dlscord.cc", + "dlscord.click", + "dlscord.cloud", + "dlscord.fr", + "dlscord.gg", + "dlscord.gifts", + "dlscord.in", + "dlscord.info", + "dlscord.ink", + "dlscord.live", + "dlscord.net", + "dlscord.online", + "dlscord.org", + "dlscord.press", + "dlscord.pro", + "dlscord.rocks", + "dlscord.ru.com", + "dlscord.shop", + "dlscord.site", + "dlscord.space", + "dlscord.store", + "dlscord.support", + "dlscord.team", + "dlscord.tech", + "dlscord.tips", + "dlscord.wiki", + "dlscord.world", + "dlscordapp.codes", + "dlscordapp.com", + "dlscordapp.fun", + "dlscordapp.info", + "dlscordapp.pw", + "dlscordapp.ru", + "dlscordapp.store", + "dlscordapps.com", + "dlscordboost.com", + "dlscordd.ru", + "dlscordfull.ru", + "dlscordgift.com", + "dlscordgift.shop", + "dlscordgived.xyz", + "dlscordglft.xyz", + "dlscordglfts.xyz", + "dlscordniltro.com", + "dlscordnitro.com", + "dlscordnitro.info", + "dlscordnitro.ru.com", + "dlscordnitro.ru", + "dlscordnitro.store", + "dlscordnitro.us", + "dlscordnitrofree.com", + "dlscordnitros.gifts", + "dlscordnltro.gifts", + "dlscordnltro.online", + "dlscordnltro.ru", + "dlscordrglft.xyz", + "dlscords.gifts", + "dlscords.site", + "dlscordsgift.xyz", + "dlscordsglfts.xyz", + "dlscordsream.pp.ua", + "dlscordsteam.com", + "dlscorldnitro.store", + "dlscorp.com", + "dlscors.gift", + "dlscourd.info", + "dlscrod-app.xyz", + "dlscrod-game.ru", + "dlscrod-gift.com", + "dlscrod.ru.com", + "dlscrodapp.ru", + "dlsordnitro.gifts", + "dlsordnltros.gifts", + "dmarkef.com", + "dmarket-place.pp.ua", + "dmcordsteamnitro.de", + "dnitrogive.com", + "doatgiveaway.top", + "does-small.ru.com", + "dogewarrior-giveaway.info", + "dola.pp.ua", + "domineer.pp.ua", + "dominosllc.com", + "dominospizza-nl.com", + "dominospizzanl.com", + "dopeskins.com", + "doscord.com", + "doscordapp.com", + "dota2fight.net", + "dota2fight.ru", + "dota2giveaway.top", + "dota2giveaways.top", + "dotacommunitu.xyz", + "dotafights.vip", + "dotagift01.xyz", + "dotagift07.xyz", + "dotagift11.xyz", + "dotagift12.xyz", + "dotagift13.xyz", + "dotagift14.xyz", + "dotagift15.xyz", + "dotagiveaway.win", + "douyutv.ru", + "dragon-black.net.ru", + "dragon-up.online", + "dragonary-giveaway.info", + "dreamhacks-fort.site", + "dripa-discord.com", + "driscord.ru.com", + "driscord.ru", + "dro-coad.ru", + "drop-key.ru", + "drop-nitro.com", + "drop-nitro.fun", + "drop-pro.com", + "drop.net.ru", + "drop.org.ru", + "drop.pp.ru", + "dropkeygood.ml", + "drops4all.pp.ru", + "dropskey.com", + "dropskey.ru", + "dropskin.monster", + "drumairabubakar.com", + "ds-nitr.xyz", + "ds-nitro.com", + "ds-nitro.site", + "dscord-generaot.store", + "dscord.gifts", + "dscord.me", + "dscord.nl", + "dscord.xyz", + "dscordapp.com", + "dscordnitro.xyz", + "dscrd.club", + "dsctnitro.site", + "dsicord.gift", + "dsicrod.com", + "dsiscord.com", + "dsnitro.xyz", + "duiscord.com", + "dumdumdum.ru", + "duscord.com", + "duscord.js.org", + "dwaynejon.xyz", + "dwny.org", + "dxiscord.com", + "dzscord.js.org", + "e-giftpremium.com", + "ea-case.com", + "ea-drop.com", + "each-tel.xyz", + "earnskinz.xyz", + "easy-box.site", + "easycases.pw", + "easyopeningpay.online", + "easyopeningpay.ru", + "eazy-game.online", + "eazy-game.ru", + "eazydrop.monster", + "ecnhasports.ru", + "ecyber-tournament.ru", + "ecyber-versus.ru", + "egamerscup.club", + "emeraldbets.ru", + "en-roblox.com", + "ence.net.ru", + "encebrand.xyz", + "encecsport.me", + "encegun.xyz", + "encesports.xyz", + "enceteam.me", + "enceteam.org.ru", + "encewatch.ru", + "epic-request.xyz", + "epicfriendis.xyz", + "epicfriennd.xyz", + "epicgamees.xyz", + "epicgamesnitro.com", + "epicgamess.xyz", + "epicgammes.xyz", + "epicgamnes.xyz", + "epicganmes.xyz", + "epicggames.site", + "epicggames.xyz", + "epicinvite.xyz", + "epicjames.xyz", + "epickgames.xyz", + "epicqames.xyz", + "epicqannes.xyz", + "epicservic.xyz", + "epicservise.xyz", + "epilcgames.xyz", + "epiqgames.xyz", + "eplcgames.xyz", + "eplcups.com", + "eplicgames.xyz", + "eqiccames.xyz", + "eqicgames.xyz", + "esea-mdl.com", + "esl-2020.com", + "esl-drop.com", + "esl-eu.com", + "esl-gamingnetwork.com", + "esl-gamingseries.com", + "esl-lv.com", + "esl-pl.com", + "esl-playglobal.net", + "esl-pro-legue.xyz", + "esl-proleague.net", + "eslcup.xyz", + "eslgamescommunity.com", + "eslgamesworldwide.com", + "eslgaming-play.com", + "eslgaming-world.com", + "eslgamingnetworks.com", + "eslgamingopen.com", + "eslgamingworldwide.net", + "eslhub.xyz", + "eslhubgaming.com", + "eslplaynetworks.com", + "eslplayoneleague.com", + "eslplayworlds.com", + "eslpro.ru", + "eslquickseries.com", + "eslsports.ru", + "eslworldwideplay.com", + "esportgaming.ru", + "esportgift.ru", + "esportpoinl.xyz", + "esportpoint.xyz", + "esports-2go.pp.ua", + "esports-csgo.ru", + "esports-sale.ru", + "esports-trade.net.ru", + "esportscase.online", + "esportscase.ru", + "esportsfast.pp.ua", + "esportsgvay.xyz", + "esportsi.xyz", + "espots-csgo.xyz", + "essenseglow.com", + "etsdrop.monster", + "etssdrop.monster", + "event-discord.com", + "event-games4roll.com", + "events-discord.com", + "evmcups.ru", + "ewqdsa.xyz", + "exaltedbot.xyz", + "exchangeuritems.gq", + "explorerblocks.com", + "extraskinscs.xyz", + "ez-tasty.cyou", + "ezcase.xyz", + "ezclrop.ru", + "ezdiscord.xyz", + "ezdrop.net.ru", + "ezdropss.net.ru", + "ezdrp.ru", + "ezopen.site", + "ezpudge.pp.ua", + "ezwin24.ru", + "ezwithcounter.xyz", + "ezzrun.pp.ua", + "facecup.fun", + "facedrop.one", + "faceit-premium.com", + "faceiteasyleague.ru", + "faceiten.info", + "facepunch-award.com", + "facepunch-gifts.org.ru", + "facepunch-llc.com", + "facepunch-ltd.com", + "facepunch-reward.com", + "facepunch-studio.com", + "facepunch-studio.us", + "facepunch-twitch.com", + "facepunchltd.com", + "facepunchs.com", + "facepunchskins.com", + "facepunchstudio.com", + "facerit.com", + "faceuinuu.com", + "faceuinuz.com", + "faceuinuz.org.ru", + "faceuinuz.ru.com", + "fai-ceite.info", + "faiceit.ru.com", + "fall500.ru", + "fang-operation.ru", + "fannykey.ru", + "farestonpw.ru.com", + "faritkoko.ru", + "farkimagix.xyz", + "fartik.net.ru", + "fasdf.pp.ua", + "fast-cup.site", + "fastcup.ru.com", + "fastcups.xyz", + "fastdrop.win", + "fastgotournaments.xyz", + "fastlucky.ru.com", + "fastlucky.ru", + "fastskins.ru", + "fasttake.space", + "fatown.net", + "fdiscord.com", + "ff.soul-ns.xyz", + "fineleague.fun", + "fineplay.xyz", + "fireopencase.com", + "firtonesroll.ru.com", + "fiscord.com", + "fivetown.net", + "flyes-coin.com", + "fnaatic.org.ru", + "fnatcas.org.ru", + "fnatic-2021.ru", + "fnatic-drop.com", + "fnatic-gg.fun", + "fnatic-go.fun", + "fnatic-ro1ls.ru.com", + "fnatic-s.fun", + "fnatic-team.ru", + "fnatic-time.ru", + "fnatic.pp.ru", + "fnatic.team", + "fnatic1.org.ru", + "fnatic2.org.ru", + "fnaticez.me", + "fnaticforyou.xyz", + "fnaticgit.xyz", + "fnaticteam.org.ru", + "fnnatic.org.ru", + "fnnaticc.org.ru", + "fntc-bd.pp.ua", + "follow-ask.xyz", + "forcedope.xyz", + "forest-host.ru", + "formulaprize.com", + "fornite.best", + "forse-pash.pp.ru", + "forse-wash.pp.ru", + "forsportss.pp.ua", + "fortnight.space", + "fortnite-newswapper.fun", + "fortnite.sswapper.com", + "fortnitebuy.com", + "fortnitecrew.ru.com", + "fortniteswapper.fun", + "fortuneroll.tk", + "fowephwo.ru", + "foxycyber.ru", + "fozzytournaments.fun", + "fplgo.ru", + "fps-booster.pw", + "fr33item.xyz", + "free-discord.ru", + "free-dislcordnitrlos.ru", + "free-niltross.ru", + "free-nitlross.ru", + "free-nitro-sus.pages.dev", + "free-nitro.ru", + "free-nitroi.ru", + "free-nitros.ru", + "free-skins.ru", + "freediscord-nitro.cf", + "freediscordnitro.ru", + "freediscrodnitro.org", + "freediskord-nitro.xyz", + "freedrop0.xyz", + "freefireclaim.club", + "freeinstagramfollowersonline.com", + "freenetflix.io", + "freenitro.ru", + "freenitrogenerator.cf", + "freenitrogenerator.tk", + "freenitroi.ru", + "freenitrol.ru", + "freenitros.com", + "freenitros.ru", + "freenitros.tk", + "freenltro.ru", + "freerobloxgenerator.tk", + "freeskins.online", + "freeskinsfree.pp.ua", + "freespoty.com", + "from-eliasae.ru.com", + "from-puste.xyz", + "from-sparsei.ru.com", + "from-surenseds.xyz", + "ftp.celerone.cf", + "ftp.copyrighthelpbusiness.org", + "ftp.def-dclss.pp.ua", + "ftp.domineer.pp.ua", + "ftp.fasdf.pp.ua", + "ftp.ghostgame.ru", + "ftp.gooditems.pp.ua", + "ftp.greatdrops.pp.ua", + "ftp.legasytour.it", + "ftp.navieslproleagueseason13.pp.ua", + "ftp.ogevtop.ru", + "ftp.scogtopru.pp.ua", + "ftp.steamcommunlty.it", + "ftp.topeasyllucky.pp.ua", + "ftp.versuscsgoplay.pp.ua", + "fulldiscord.com", + "funchest.fun", + "fundro0p.site", + "funjet1.ru.com", + "funnydrop.store", + "furtivhqqc.com", + "furyesports.xyz", + "furyleage.xyz", + "fustcup.ru", + "g-games.store", + "g1veaway-nav1.site", + "g2-cybersport.net", + "g2-cybersport.ru", + "g2-cybersports.net", + "g2-esports.moscow", + "g2-game.ru", + "g2-give.info", + "g2-give.ru", + "g2-pro.shop", + "g2a.ru.com", + "g2cyber-espots.top", + "g2cybergame.fun", + "g2eref.ru", + "g2ezports.xyz", + "g2team-give.top", + "g2team.org", + "g2teams.com", + "g2teamss.ru", + "gaben-seller.pp.ua", + "gamaloft.xyz", + "gambit-cs.com", + "gambit.net.ru", + "gambit.org.ru", + "gambitesports.me", + "gambling1.ru.com", + "gambling1.ru", + "gamdom.ru", + "game-case.ru", + "game-csgo-steam.ru", + "game-csgosteam.ru", + "game-sense.space", + "game-steam-csgo.ru", + "game-steamcsgo.ru", + "game-tournaments.net.ru", + "game-tournaments.ru.com", + "game.schweitzer.io", + "game4roll.com", + "gameb-platform.com", + "gamecsgo-steam.ru", + "gamegowin.xyz", + "gamekere.net.ru", + "gamekor.net.ru", + "gameluck.ru", + "gamemaker.net.ru", + "gamepromo.net.ru", + "gamerich.xyz", + "gameroli.net.ru", + "gamerolls.net.ru", + "games-code.ru.com", + "games-roll.ga", + "games-roll.ml", + "games-roll.ru", + "gamesbuy.net.ru", + "gamesfree.org.ru", + "gamespol.net.ru", + "gamzc-topz.xyz", + "gamzgss-top.org.ru", + "gamzgss-top.xyz", + "garstel.github.io", + "gave-nitro.com", + "gavenitro.com", + "gbauthorization.com", + "gdiscord.com", + "gdr-op.ru.com", + "generator.discordnitrogift.com", + "get-discord.fun", + "get-gamesroll.xyz", + "get-my-nitro.com", + "get-nitro.com", + "get-nitro.fun", + "get-nitro.net", + "get-traded.xyz", + "get.sendmesamples.com", + "getautomendpro.com", + "getcach.monster", + "getfitnos.com", + "getfreediscordnitro.ml", + "getnaturetonics.com", + "getnitro.xyz", + "getnitrogen.org", + "getproviamax.com", + "getriptide.live", + "getskins.monster", + "getstratuswatch.com", + "getv-bucks.site", + "getyouritems.pp.ua", + "gfrtwgfkgc.xyz", + "gg-dr0p.ru", + "ggbolt.ru", + "ggboom.ru", + "ggdrop-gg.xyz", + "ggdrop.org.ru", + "ggdrop.pp.ru", + "ggdrop.space", + "ggdrop1.net.ru", + "ggdrops.net.ru", + "ggdrops.ru.com", + "ggexpert.online", + "ggexpert.ru", + "ggfail.xyz", + "gglootgood.xyz", + "ggnatus.com", + "ggnavincere.xyz", + "ggtour.ru", + "ghostgame.ru", + "gif-discord.com", + "gife-discorde.com", + "gift-discord.online", + "gift-discord.ru", + "gift-discord.shop", + "gift-discord.xyz", + "gift-discords.com", + "gift-g2.online", + "gift-g2.ru", + "gift-nitro.store", + "gift4keys.com", + "giftc-s.ru", + "giftcsogg.ru", + "giftdiscord.info", + "giftdiscord.online", + "giftes-discord.com", + "giftnitro.space", + "giftsdiscord.com", + "giftsdiscord.fun", + "giftsdiscord.online", + "giftsdiscord.ru", + "giftsdiscord.site", + "givaeway.com", + "givaewey.com", + "giveavvay.com", + "giveaway-fpl-navi.net.ru", + "giveaway-fpl.net.ru", + "giveawaynitro.com", + "giveawayskin.com", + "giveaweys.com", + "giveeawayscin.me", + "givenatus.site", + "giveprize.ru", + "giveweay.com", + "givrayawards.xyz", + "glaem.su", + "gleam.su", + "glets-nitro.com", + "glft-discord.com", + "glob21.online", + "globacs.monster", + "global-skins.gq", + "globalcs.monster", + "globalcss.monster", + "globalcsskins.xyz", + "globalmoestro.ru", + "globalskins.tk", + "gnswebservice.com", + "go-cs.ru.com", + "go-cups.ru", + "go.rancah.com", + "go.thefreedailyraffle.com", + "go2-rush.pp.ua", + "go4you.ru", + "gocs8.ru.com", + "gocs8q.ru", + "gocs8v.ru.com", + "gocsx.ru", + "gocsx8.ru", + "gocups.ru", + "godssale.ru", + "goldendota.com", + "goman.ru.com", + "good-csgo-steam.ru", + "gooditems.pp.ua", + "goodskins.gq", + "gool-lex.org.ru", + "gosteamanalyst.com", + "great-drop.xyz", + "greatdrops.pp.ua", + "greatgreat.xyz", + "greenwisedebtrelief.com", + "gtakey.ru", + "gtwoesport-battle.ru", + "guardian-angel.xyz", + "guns-slot.tk", + "halitaoz.cam", + "hallowen-nitro.com", + "haste.monster", + "hdiscord.com", + "hdiscordapp.com", + "hellcase.net.ru", + "hellgiveaway.trade", + "hellstorecoin.site", + "hellstores.xyz", + "help-center-portal.tk", + "help.usabenefitsguide.com", + "help.usalegalguide.com", + "help.verified-badgeform.tk", + "heroic-esports.ru", + "hjoiaeoj.ru", + "hltvcsgo.com", + "hltvgames.net", + "holyawards.xyz", + "hope-nitro.com", + "horizon-up.org.ru", + "horizonup.ru", + "hornetesports.xyz", + "host322.ru", + "howl.monster", + "howls.monster", + "httpdlscordnitro.ru.com", + "humanlifeof.xyz", + "humnchck.co", + "hunts.monster", + "huracancsgo.tk", + "huyatv.ru", + "hydra2018.ru", + "hype-chat.ru", + "hyper-tournament.xyz", + "hypercups.ru", + "hypertracked.com", + "hyperz.monster", + "id-374749.ru", + "idchecker.xyz", + "idealexplore.com", + "idiscord.pro", + "iemcup.com", + "imvu37.blogspot.com", + "in-gives.ru.com", + "indereyn.ru.com", + "information-discord.com", + "inteledirect.com", + "intimki.com", + "into-nitro.com", + "inventtop.com", + "isp3.queryhost.ovh", + "itemcloud.one", + "iwinner.ru.com", + "jet-crash.xyz", + "jetcase.fun", + "jetcase.ru.com", + "jetscup.ru", + "jjdiscord.com", + "joewfpwg.ru", + "jokedrop.ru", + "jope-nitro.com", + "joyskins.xyz", + "juct-case.ru", + "just-roll.ru", + "justcase.net.ru", + "justcause.fun", + "justdior.com", + "justwins.ru", + "kahiotifa.ru", + "kambol-go.ru", + "kaspi-capital.com", + "katowice.ru", + "katowlce.ru", + "kaysdrop.ru", + "key-dr0b.com", + "key-dr0p.com", + "key-drcp.com", + "key-drop-free.com", + "key-dropo.com", + "keydoppler.one", + "keydorp.me", + "keydrop.guru", + "keydrop.org.ru", + "keydrop.ru.com", + "keydropp.one", + "keydrops.xyz", + "keydrup.ru", + "keys-dropes.com", + "keys-loot.com", + "keysdropes.com", + "kievskiyrosdachy-ua.ru", + "kingofqueens2021.github.io", + "kirakiooi.xyz", + "kkgdrops.monster", + "knife-eazy.pp.ua", + "knifespin.top", + "knifespin.xyz", + "knifespins.xyz", + "knifex.ru.com", + "knifez-roll.xyz", + "knifez-win.xyz", + "knmirjdf.ru", + "konicpirg.com", + "kr1ks0w.ru", + "kredo-capital.com", + "ksgogift.pp.ua", + "ksodkcvm.ru", + "l0d4b860.justinstalledpanel.com", + "l1568586.justinstalledpanel.com", + "l23682ce.justinstalledpanel.com", + "l3a32c23.justinstalledpanel.com", + "l4a13998.justinstalledpanel.com", + "l4bbc943.justinstalledpanel.com", + "l95614b0.justinstalledpanel.com", + "l9f009d3.justinstalledpanel.com", + "la622566.justinstalledpanel.com", + "la76c010.justinstalledpanel.com", + "labfbb02.justinstalledpanel.com", + "lakskuns.xyz", + "lan-pro.fun", + "lan-pro.link", + "lan-pro.ru", + "lan-pro.xyz", + "lb4b95f8.justinstalledpanel.com", + "lb6469d3.justinstalledpanel.com", + "lb9d00fb.justinstalledpanel.com", + "lbd74bef.justinstalledpanel.com", + "lc995e52.justinstalledpanel.com", + "lcb2f337.justinstalledpanel.com", + "ld54d414.justinstalledpanel.com", + "ldb9f474.justinstalledpanel.com", + "ldiscord.gift", + "ldiscordapp.com", + "le491879.justinstalledpanel.com", + "league-csgo.com", + "legasytour.it", + "lehatop-01.ru", + "lemesports.ru", + "lf4d4257.justinstalledpanel.com", + "lf5d73bb.justinstalledpanel.com", + "lfa90cb7.justinstalledpanel.com", + "lfd0d93c.justinstalledpanel.com", + "lifegg.xyz", + "linktrade.pp.ua", + "listycommunity.ru", + "litenavi.xyz", + "lkdiscord.com", + "loginprofile.xyz", + "loginrun.info", + "longxrun.online", + "loot-conveyor.com", + "loot-item.xyz", + "loot-rust.com", + "loot.net.ru", + "loot.pp.ru", + "loot4fun.ru", + "lootmake.com", + "lootship.ga", + "lootshunt.org.ru", + "lootsrow.com", + "lootxmarket.com", + "loungeztrade.com", + "low-cups.ru", + "lozt.pp.ua", + "luancort.com", + "lucky-skins.xyz", + "luckycrush.ga", + "luckydrop.site", + "luckyfast.ru.com", + "luckyfast.ru", + "luckygift.net.ru", + "luckygift.space", + "luckygo.ru.com", + "luckygo.ru", + "luckyiwin.ml", + "luckyiwin.tk", + "luxace.ru.com", + "luxerkils.xyz", + "m-discord.pw", + "m.setampowered.com", + "m90694rb.beget.tech", + "made-nitro.com", + "madessk.pp.ua", + "maggicdrop.xyz", + "magic-delfy.net.ru", + "magicdropgift.ru", + "magicdropnew.xyz", + "magicrollslg.com.ru", + "magicrollslw.com.ru", + "magicroulete.ru", + "magicrun.site", + "magictop.ru.com", + "magifcrolrlc.xyz", + "magifcrolrlh.xyz", + "magifrolbiq.xyz", + "magifrolbit.xyz", + "magik-dr0p.fun", + "magikbrop.xyz", + "magnaviroll.xyz", + "magnavirolls.xyz", + "magnavirollz.xyz", + "mail.celerone.cf", + "mail.csgoroll.ru", + "mail.dicsord-airdrop.ru", + "mail.explorerblocks.com", + "mail.fasdf.pp.ua", + "mail.ghostgame.ru", + "mail.gooditems.pp.ua", + "mail.ogevtop.ru", + "mail.scogtopru.pp.ua", + "mail.streamcomuniity.pp.ua", + "mail.versuscsgoplay.pp.ua", + "majestictips.com", + "major-2021.ru", + "makson-gta.ru", + "malibones.buzz", + "marke-tcgo.ru.com", + "marke-tgo.ru.com", + "market-csgo.ru", + "market-subito.site", + "marketsleam.xyz", + "marketsm.pp.ua", + "markt-csgo.ru.com", + "markt-csru.info", + "marktcsgo.ru.com", + "mars-cup.ru", + "master-up.ru", + "maxskins.xyz", + "mcdaonlds.com", + "mcdelivery-offer.com", + "mcdelivery-sale.com", + "mcdelivery24.com", + "mcdonalds-iloveit.com", + "mcdonalds-saudiarabia.com", + "mcdonaldsau.info", + "mdiscord.com", + "medpatrik.ru", + "megacase.monster", + "mekaverse-minting.com", + "mekaversecollection.com", + "mekaversenft.net", + "microsup.net", + "minea.club", + "moderationacademy-exams.com", + "mol4a.pp.ua", + "money.fastcreditmatch.com", + "money.usacashfinder.com", + "mvcsgo.com", + "mvpcup.ru", + "mvptournament.com", + "my-trade-link.ru", + "my-tradelink.ru", + "myccgo.xyz", + "mychaelknight.com", + "mycsgoo.ru", + "mydrop.monster", + "myfast.ru", + "mygames4roll.com", + "myjustcase.ru", + "myrolls.monster", + "myrollz.com", + "mythic-esports.xyz", + "mythiccups.xyz", + "mythicleagues.xyz", + "mythicups.xyz", + "myticks.xyz", + "mytrade-link.ru.com", + "mytradelink.pp.ua", + "mytradelink.ru.com", + "mytradeoffers.ru.com", + "nacybersportvi.ru", + "nagipen.ru", + "nagiver.ru", + "naturespashowerpurifier.com", + "natus-lootbox.net.ru", + "natus-lootbox.org.ru", + "natus-open.net.ru", + "natus-open.org.ru", + "natus-open.pp.ru", + "natus-rolls.xyz", + "natus-space.ru", + "natus-spot.net.ru", + "natus-spot.pp.ru", + "natus-vincere.ru", + "natus-vincere.space", + "natus-vincere.xyz", + "natus-vincery-majors.ru.com", + "natus-vincerygive.xyz", + "natus-vincerygivess.xyz", + "natus-vincerygivesz.xyz", + "natus-vincerygivex.xyz", + "natus-vincerygivezc.xyz", + "natus-vincerygivezr.ru", + "natus-vincerygivezz.xyz", + "natus-win.net.ru", + "natus-win.org.ru", + "natus-win.pp.ru", + "natusforyou.pp.ua", + "natusspot.pp.ru", + "natustop.net.ru", + "natustop.org.ru", + "natusvincerbestmarket.work", + "natusvinceredrop.ru", + "natuswin.org.ru", + "nav-s1.ru", + "navi-21.ru", + "navi-bp.com", + "navi-cis.net.ru", + "navi-cs.com", + "navi-drop.net", + "navi-drop2020.com", + "navi-es.ru", + "navi-esl.ru.com", + "navi-esports.net", + "navi-eu.ru", + "navi-ez.com", + "navi-freedrop.xyz", + "navi-freeskins.com", + "navi-give.net.ru", + "navi-giveaway-simple.net.ru", + "navi-giveaway.net", + "navi-giveaway.xyz", + "navi-gs.com", + "navi-gt.com", + "navi-gv.com", + "navi-hawai.net.ru", + "navi-io.com", + "navi-keep.net.ru", + "navi-lix.xyz", + "navi-ls.com", + "navi-lzx.ru", + "navi-off.us", + "navi-ol.com", + "navi-q.com", + "navi-rt.com", + "navi-russia.ru", + "navi-share.pp.ru", + "navi-skins.org.ru", + "navi-skins.pp.ru", + "navi-sp.com", + "navi-tm.com", + "navi-tm.ru", + "navi-up.com", + "navi-up.ru", + "navi-winners.org.ru", + "navi-wins-skiins.org.ru", + "navi-x.ru", + "navi-youtube.net.ru", + "navi.pp.ru", + "navi2021.net.ru", + "naviback.ru", + "navibase.net.ru", + "navibase.org.ru", + "navibase.pp.ru", + "navicase-2020.org.ru", + "navicase.org", + "navicsg.ru", + "navidonative.ru", + "naviend.xyz", + "navieslproleagueseason13.pp.ua", + "naviesport.net", + "naviesportsgiveaways.pro", + "navifree.ru", + "navifreeskins.ru", + "navifun.me", + "navigg.org.ru", + "navigg.ru", + "naviggcoronagiveaway.ru", + "navigiveaway.ru", + "navign.me", + "navigs.ru", + "navileague.xyz", + "navination.site", + "navipodarok.ru", + "navipresent.xyz", + "naviqq.org.ru", + "navirolls.org.ru", + "navishare.net.ru", + "navishare.pp.ru", + "naviskins.xyz", + "naviteam.net.ru", + "naviteamway.net.ru", + "navitm.ru", + "navvigg.site", + "navviigg.ru", + "navy-freecases.ru", + "navy-loot.xyz", + "nawegate.com", + "nawi-gw.ru", + "nawibest.ru.com", + "nawigiveavay.xyz", + "netfllix-de.com", + "new-collects.xyz", + "new-drop.net.ru", + "new-offer.trade", + "new-steamcommunlty.xyz", + "new.mychaelknight.com", + "newdiscord.online", + "nice-haesh-info.ru", + "nicegg.ru", + "night-skins.com", + "nightz.monster", + "nise-cell.net.ru", + "nise-gell.org.ru", + "nise-well.org.ru", + "nise-win.xyz", + "nitrlooss-free.ru", + "nitro-airdrop.org", + "nitro-all.xyz", + "nitro-app.com", + "nitro-app.fun", + "nitro-discord.fun", + "nitro-discord.info", + "nitro-discord.me", + "nitro-discord.org", + "nitro-discord.ru.com", + "nitro-discordapp.com", + "nitro-discords.com", + "nitro-drop.com", + "nitro-ds.xyz", + "nitro-for-free.com", + "nitro-from-steam.com", + "nitro-gift.ru.com", + "nitro-gift.ru", + "nitro-gift.site", + "nitro-gift.space", + "nitro-gift.store", + "nitro-gift.xyz", + "nitro-give.site", + "nitro-up.com", + "nitro.gift", + "nitroairdrop.com", + "nitroappstore.com", + "nitrochallange.com", + "nitrodiscord.org", + "nitrodlscordl.xyz", + "nitrodlscordx.xyz", + "nitrofgift.xyz", + "nitrofrees.ru", + "nitrogeneral.ru", + "nitrogift.xyz", + "nitrogive.com", + "nitroos-frieie.ru", + "nitroosfree.ru", + "nitropussy.com", + "nitros-gift.com", + "nitrostore.org", + "nitrotypehack.club", + "nltro.site", + "ns1.dns-soul.wtf", + "ns1.dropc.me", + "ns1.navitry.me", + "ns1.peektournament.me", + "ns2.dropc.me", + "ns2.helpform-center.ml", + "nur-electro-05.ml", + "nv-pick.com", + "nvcontest.xyz", + "nwgwroqr.ru", + "offerdealstop.com", + "official-nitro.com", + "official-nitro.fun", + "ogevtop.ru", + "ogfefieibio.ru", + "okdiscord.com", + "oligarph.club", + "onehave.xyz", + "open-case.work", + "opencase.space", + "operation-broken.xyz", + "operation-pass.ru.com", + "operation-riptide.link", + "operation-riptide.ru.com", + "operation-riptide.xyz", + "operationbroken.xyz", + "operationreptide.com", + "operationriptide.tk", + "opinionshareresearch.com", + "order-40.com", + "order-78.com", + "order-87.com", + "order-96.com", + "orderpropods.com", + "ornenaui.ru", + "out-want.xyz", + "output-nitro.com", + "overdrivsa.xyz", + "ovshau.club", + "ownerbets.com", + "p.t67.me", + "paayar.info", + "pandakey.ru", + "pandaskin.ru.com", + "pandaskins.ru.com", + "pandemidestekpaket.cf", + "passjoz.net.ru", + "path.shareyourfreebies.com", + "path.topsurveystoday.com", + "patrool.net.ru", + "pay-18.info", + "payeaer.xyz", + "payear.xyz", + "payeer.life", + "payeer.live", + "payeer.vip", + "pingagency.ru", + "pizzaeria-papajohns.com", + "playcsgo-steam.ru", + "playerskinz.xyz", + "playeslseries.com", + "please.net.ru", + "pltw.com", + "pluswin.ru", + "pluswsports.ru", + "poloname.net.ru", + "pop.ghostgame.ru", + "pop.ogevtop.ru", + "pose1dwin.ru", + "poste.xyz", + "power-sk1n.net.ru", + "ppayeer.ru.com", + "ppayeer.ru", + "prajyoth-reddy-mothi.github.io", + "prajyoth.me", + "prefix.net.ru", + "premium-discord.com", + "premium-discords.com", + "premium-faceit.com", + "premiums-discord.com", + "price-claim.xyz", + "prime-drop.xyz", + "privatexplore.com", + "privatkeyblok.com", + "prizee-good.com", + "profile-2994292.ru", + "profile-442572242.online", + "profiles-7685291049068.me", + "promo-codes.world", + "promo-discord.com", + "promo-discord.site", + "proz.monster", + "psyonix-trade.online", + "psyonix.website", + "psyonlxcodes.com", + "ptbdiscord.com", + "pubg-asia.xyz", + "pubg-steamcommunityyz.top", + "pubg.network", + "pubg.new-collects.xyz", + "pubgclaims.com", + "pubge21.xyz", + "pubgfree77.com", + "pubgfreedownload.org", + "pubgfreeeus.cf", + "pubggf01.xyz", + "pubggf02.xyz", + "pubggf03.xyz", + "pubggf04.xyz", + "pubggf05.xyz", + "pubggf06.xyz", + "pubggf10.xyz", + "pubggf15.xyz", + "pubggf16.xyz", + "pubggf17.xyz", + "pubggf18.xyz", + "pubggf19.xyz", + "pubggf20.xyz", + "pubggf21.xyz", + "pubggf22.xyz", + "pubggf23.xyz", + "pubggf24.xyz", + "pubggf25.xyz", + "pubggf26.xyz", + "pubggf27.xyz", + "pubggf28.xyz", + "pubggf29.xyz", + "pubggf30.xyz", + "pubggf31.xyz", + "pubggf32.xyz", + "pubggf33.xyz", + "pubggf34.xyz", + "pubggf35.xyz", + "pubggf36.xyz", + "pubggf37.xyz", + "pubggf38.xyz", + "pubggf39.xyz", + "pubggf40.xyz", + "pubggf41.xyz", + "pubggf42.xyz", + "pubggift100.xyz", + "pubggift101.xyz", + "pubggift102.xyz", + "pubggift31.xyz", + "pubggift32.xyz", + "pubggift48.xyz", + "pubggift56.xyz", + "pubggift58.xyz", + "pubggift59.xyz", + "pubggift60.xyz", + "pubggift61.xyz", + "pubggift62.xyz", + "pubggift63.xyz", + "pubggift64.xyz", + "pubggift65.xyz", + "pubggift66.xyz", + "pubggift67.xyz", + "pubggift68.xyz", + "pubggift69.xyz", + "pubggift70.xyz", + "pubggift71.xyz", + "pubggift87.xyz", + "pubggift91.xyz", + "pubggift92.xyz", + "pubggift93.xyz", + "pubggift94.xyz", + "pubggift95.xyz", + "pubggift96.xyz", + "pubggift97.xyz", + "pubggift98.xyz", + "pubggift99.xyz", + "pubgmcheats.com", + "pubgmobile2019ucfreeeee.tk", + "pubgmobile365.com", + "pubgmobile365.giftcodehot.net", + "pubgmobile737373.ml", + "pubgmobileskin2020.com", + "pubgmobilespro.my.id", + "pubgmobileuc2020free.cf", + "pubgofficielbcseller.online", + "pubgtoken.io", + "pubguccmobilefree.cf", + "qbt-giveaway.info", + "qcold.club", + "qcoldteam.life", + "qtteddybear.com", + "quantumtac.co", + "quick-cup.xyz", + "quickrobux.net", + "r-andomfloat.ru", + "rainorshine.ru", + "ran-getto.org.ru", + "rangskins.com", + "rave-clup.ru", + "rave-new.ru", + "rblxcorp.work", + "rbux88.com", + "rbux88go.com", + "rdr2code.ru", + "realskins.xyz", + "realtorg.xyz", + "redirectednet.xyz", + "redizzz.xyz", + "rednance.com", + "redskin.monster", + "reports.noodlesawp.ru", + "reslike.net", + "rewardbuddy.me", + "rewards-rl.com", + "rewardsavenue.net", + "rewardspremium-nitro.gq", + "rien.xyz", + "rip-tide.ru", + "ripetide.ru", + "riptid-operation.ru", + "riptide-cs.com", + "riptide-cs.ru", + "riptide-csgo.ru", + "riptide-free-pass.net.ru", + "riptide-free-pass.org.ru", + "riptide-free-pass.pp.ru", + "riptide-gaming.ru", + "riptide-operation.com", + "riptide-operation.ru.com", + "riptide-operation.ru", + "riptide-operation.xyz", + "riptide-operations.ru", + "riptide-pass.org.ru", + "riptide-take.ru", + "riptide-valve.ru", + "riptidefree.ru", + "riptiden.ru", + "riptideoffer.ru", + "riptideoperation.xyz", + "riptidepass.net.ru", + "riptidepass.ru", + "rl-activate.com", + "rl-award.com", + "rl-bounce.com", + "rl-change.ru", + "rl-chaser.com", + "rl-code.com", + "rl-diamond.com", + "rl-epic.com", + "rl-fandrops.com", + "rl-fanprize.com", + "rl-fast.com", + "rl-fastrading.com", + "rl-garage.info", + "rl-garage.online", + "rl-garage.space", + "rl-give.ru.com", + "rl-insidergift.com", + "rl-performance.com", + "rl-positive.com", + "rl-promocode.com", + "rl-promos.com", + "rl-purple.com", + "rl-retail.fun", + "rl-rewards.ru.com", + "rl-tracking.pro", + "rl-traders.com", + "rlatracker.com", + "rlatracker.pro", + "rldrop-gifts.com", + "rldrop.gifts", + "rlexcihnage.com", + "rlgarages.com", + "rlgifts.org", + "rlgtracker.zone", + "rlq-trading.com", + "rlqtrading.com", + "rlshop.fun", + "rlstracker.com", + "rltracken.ru", + "rltrackings.com", + "rlv-trading.com", + "rlz-trading.com", + "robfan.work", + "roblox-collect.com", + "roblox-login.com", + "roblox-porn.com", + "roblox-robux.de", + "roblox.com.so", + "roblox.free.robux.page", + "roblox.help", + "roblox.link.club", + "robloxbing.com", + "robloxdownload.org", + "robloxgamecode.com", + "robloxgiftcardz.com", + "robloxpasssword.com", + "robloxromania.com", + "robloxs.land", + "robloxsecure.com", + "robloxstore.co.uk", + "robloxux.com", + "robloxwheelspin.com", + "robloxxhacks.co", + "robuux1.club", + "robux-codes.ga", + "robux.claimgifts.shop", + "robux20.club", + "robux247.win", + "robux4sex.tk", + "robuxat.com", + "robuxfiends.com", + "robuxfree.us", + "robuxgen.site", + "robuxhach.com", + "robuxhelp.com", + "robuxhelpers.com", + "robuxhelps.com", + "robuxprofiles.com", + "robuxtools.me", + "robuxx.work", + "robx.pw", + "rocket-dealer.com", + "rocket-item.com", + "rocket-leag.com", + "rocket-league.info", + "rocket-retailer.fun", + "rocket-tournament.fun", + "rocket-trader.fun", + "rocket-traders.store", + "rocket-trades.store", + "rocket-trading.site", + "rocket-trading.space", + "rocket-trading.store", + "rocket-tradings.com", + "rocket2pass.com", + "rocketleague-drops.com", + "rocketleagues.site", + "rocketleaque.info", + "rocketradings.com", + "rockets-garages.com", + "rockets-item.com", + "rockets-items.com", + "rockets-sale.com", + "rockets-sales.com", + "rockets-trade.com", + "roleum.buzz", + "roll-gift.fun", + "roll-skins.ga", + "roll-skins.ru", + "roll-skins.tk", + "roll-statedrop.ru", + "roll4knife.xyz", + "roll4tune.com", + "rollcas.ru.com", + "rollgame.net.ru", + "rollkey.ru.com", + "rollknfez.xyz", + "rollskin-simple.xyz", + "rollskin.ru", + "rollskins.monster", + "rollskins.ru", + "rool-skins.xyz", + "roposp12.design", + "roposp14.design", + "ropost15.xyz", + "roulette-prizes.ru.com", + "roulettebk.ru", + "royalegive.pp.ua", + "run2go.ru", + "runwebsite.ru", + "rushbskins.xyz", + "rushskillz.net.ru", + "rushskins.xyz", + "rust-award.com", + "rust-boom.xyz", + "rust-charge.com", + "rust-chest.com", + "rust-code.com", + "rust-code.ru.com", + "rust-codes.com", + "rust-drop.ru.com", + "rust-get.com", + "rust-gitfs.ru", + "rust-giveaways.xyz", + "rust-kit.com", + "rust-llc.com", + "rust-ltd.com", + "rust-reward.com", + "rust-satchel.com", + "rust-skin.com", + "rust.facepunchs.com", + "rustarea.me", + "rustg1ft.com", + "rustg1fts.online", + "rustg1fts.ru", + "rustgame-servers.com", + "rustprize.com", + "rustygift.site", + "rustyit-ems.xyz", + "s-steame.ru", + "s-teame.ru", + "s1cases.site", + "s1cses.site", + "s1mple-give-away.pp.ua", + "s1mple-spin.xyz", + "s1mplesun.design", + "s92673tu.beget.tech", + "sa-mcdonalds.com", + "safe-funds.site", + "said-home.ru.com", + "sakuralive.ru.com", + "sale-steampowered.com", + "savage-growplus.com", + "scale-navi.pp.ru", + "scl-online.ru", + "sclt.xyz", + "scltourments.xyz", + "scogtopru.pp.ua", + "scteamcommunity.com", + "scwanmei.ru", + "sdiscord.com", + "seamcommunity.com", + "seamconmunity.xyz", + "seancommunity.com", + "seancommunlty.ru", + "secure-instagram.ru", + "secure.yourreadytogoproduct.surf", + "seed-nitro.com", + "services.runescape.rs-tt.xyz", + "services.runescape.rs-ui.xyz", + "setamcommunity.com", + "shadowmarket.xyz", + "shadowpay.pp.ru", + "share.nowblox.com", + "shattereddrop.xyz", + "shib.events", + "shimermsc.ru", + "shopy-nitro.tk", + "shroud-cs.com", + "sieamcommunity.net.ru", + "sieamcommunity.org.ru", + "simple-knifez.xyz", + "simple-win.xyz", + "simplegamepro.ru", + "simplegif.ru", + "simpleroll-cs.xyz", + "simplespinz.xyz", + "simplewinz.xyz", + "siriusturnier.pp.ua", + "sitemap.onedrrive.com", + "skill-toom.pp.ru", + "skin-index.com", + "skin888trade.com", + "skincs-spin.top", + "skincs-spin.xyz", + "skincsggtl.xyz", + "skindeyyes.ru", + "skingstgg.ru", + "skingstgo.ru", + "skini-lords.net.ru", + "skinkeens.xyz", + "skinmarkets.net", + "skinnprojet.ru", + "skinpowcs.ru", + "skinpowst.ru", + "skinroll.ru.com", + "skinroll.ru", + "skins-drop.ru", + "skins-hub.top", + "skins-info.net", + "skins-jungle.xyz", + "skins-navi.pp.ru", + "skins.net.ru", + "skins.org.ru", + "skins.pp.ru", + "skins1wallet.xyz", + "skinsbon.com", + "skinsboost.ru", + "skinscsanalyst.ru", + "skinsdatabse.com", + "skinsgo.monster", + "skinsind.com", + "skinslit.com", + "skinsmedia.com", + "skinsmind.ru", + "skinspace.ru", + "skinsplane.com", + "skinsplanes.com", + "skinsplanets.com", + "skinstradehub.com", + "skinsup.monster", + "skinup.monster", + "skinxinfo.net", + "skinxmarket.site", + "skinz-spin.top", + "skinz-spin.xyz", + "skinzjar.ru", + "skinzprize.xyz", + "skinzspin-cs.xyz", + "skinzspinz.xyz", + "sklinsbaron.net", + "sl1pyymyacc.ru", + "slaaeamcrommunity.com.profiles-7685291049068.me", + "sleam-trade.net.ru", + "sleam-trade.org.ru", + "sleam-trade.pp.ru", + "sleamcominnuty.ru", + "sleamcommiinuty.ru", + "sleamcomminity.ru", + "sleamcomminutiycom.ru.com", + "sleamcommmunily.xyz", + "sleamcommmunitiy.ru", + "sleamcommmunity.com", + "sleamcommmuntiy.ru", + "sleamcommnnity.com", + "sleamcommnunity.net", + "sleamcommuiliy.ru.com", + "sleamcommuinity.xyz", + "sleamcommuintiy.ru.com", + "sleamcommuinty.store", + "sleamcommuity.com", + "sleamcommunety.ru", + "sleamcommuniitey.ru.com", + "sleamcommuniity.me", + "sleamcommuniity.ru.com", + "sleamcommuniity.xyz", + "sleamcommuniiy.ru", + "sleamcommunilly.me", + "sleamcommunilly.ru", + "sleamcommunily.net", + "sleamcommunily.org", + "sleamcommunily.ru.com", + "sleamcommuninty.com", + "sleamcommuninty.ru", + "sleamcommuniry.ru", + "sleamcommunitey.com", + "sleamcommuniti.ru", + "sleamcommuniti.xyz", + "sleamcommunitiy.com", + "sleamcommunitty.xyz", + "sleamcommunittyy.me", + "sleamcommunitu.net.ru", + "sleamcommunitu.ru", + "sleamcommunituy.com", + "sleamcommunity.me", + "sleamcommunity.net", + "sleamcommunity.org.ru", + "sleamcommunity.org", + "sleamcommunity.pp.ru", + "sleamcommunityprofiles76561199056426944.ru", + "sleamcommunityy.me", + "sleamcommunlity.xyz", + "sleamcommunlty.net.ru", + "sleamcommunlty.net", + "sleamcommunlty.ru.com", + "sleamcommunlty.space", + "sleamcommunlty.xyz", + "sleamcommunnitu.com", + "sleamcommunnity.net", + "sleamcommunnity.org", + "sleamcommunnity.ru", + "sleamcommuntiny.ru", + "sleamcommuntity.ru", + "sleamcommuntiy.com", + "sleamcommuntly.ru", + "sleamcommunty.com", + "sleamcommunyti.ru", + "sleamcommunytu.ru", + "sleamcommutiny.com", + "sleamcommuunity.com", + "sleamcommynilu.online", + "sleamcommynitu.ru", + "sleamcommynity.ru", + "sleamcommyunity.com", + "sleamcomnnuniity.ru", + "sleamcomnnuniliy.site", + "sleamcomnnunily.site", + "sleamcomnnunily.website", + "sleamcomnnunitiy.ru", + "sleamcomnnunity.ru", + "sleamcomnnunty.website", + "sleamcomnumity.com", + "sleamcomnunily.ru", + "sleamcomnunity.net.ru", + "sleamcomnunity.xyz", + "sleamcomnunlty.me", + "sleamcomrnunity.com", + "sleamcomuniity.ru", + "sleamcomunitly.co", + "sleamcomunity.me", + "sleamcomunity.net.ru", + "sleamcomunity.ru.com", + "sleamcomunuty.ru", + "sleamconmumity.com", + "sleamconmunity.ru", + "sleamconmunity.xyz", + "sleamconmunlity.com", + "sleamconmunnity.com", + "sleamconnmunitiy.com", + "sleamconnunity.net.ru", + "sleamconnunity.net", + "sleamcoommunilty.com", + "sleamcoommunily.com", + "sleamcoommunity.com", + "sleamcoommunlilty.com", + "sleamcoommunlity.com", + "sleamcoomnnunity.xyz", + "sleamcoomunity.com", + "sleamcoomuuntty.xyz", + "sleamcornmunuity.me", + "sleamcornmunyti.ru", + "sleamcornrnunity.host", + "sleamcornrnunity.ru", + "sleamcummunity.me", + "sleammcommunity.ru", + "sleammcommunnity.ru", + "sleampowered.com", + "sleampowereed.ru", + "sleamscommunity.com", + "sleamtrade-offer.xyz", + "sleancommunlty.xyz", + "sleancomninity.xyz", + "sleanmconmunltiy.ru", + "slearncommunity.store", + "sleemcomnuniti.xyz", + "sleepbuster.xyz", + "slemcamunity.ru", + "slemcommunity.com", + "slemommunity.com", + "sleramconnummitti.org", + "slreamcommumnlty.com", + "slreamcommunntiy.org", + "slreamcomnuitly.xyz", + "slreamcomunity.ru", + "slreamcomunntiy.org", + "slteamcommuinity.com", + "slteamcommunity.com", + "slteamconmuniity.com", + "slum-trade.org.ru", + "smartcommunity.net", + "smeacommunity.com.au", + "smitecommunity.org", + "smtp.ghostgame.ru", + "smtp.ogevtop.ru", + "softhack.ru", + "some-other.ru.com", + "sometheir.xyz", + "sp708431.sitebeat.site", + "spacegivewayzr.xyz", + "spacegivewayzw.xyz", + "special4u.xyz", + "speedtrkzone.com", + "spin-games.com", + "spin4skinzcs.top", + "spin4skinzcs.xyz", + "spinforskin.ml", + "spiritsport.xyz", + "sponsored-simple.xyz", + "sports-liquid.com", + "spt-night.ru", + "sreamcomminity.ru", + "sreamcommuniity.com", + "sreamcommunity.com", + "sreamcommunity.net.ru", + "sreamcommunity.org.ru", + "sreamcommunty.com", + "sreammcommuunntileiy.xyz", + "sreampowered.com", + "sreancomunllty.xyz", + "srtreamcomuninitiy.xyz", + "ssteamcommunitry.com", + "ssteamcommunity.com", + "ssteamcommunity.ru.com", + "ssteampowered.com", + "st-csgo.ru", + "st-eam.ru", + "staamcommunity.com", + "staeaemcornmunite.me", + "staeamcomunnityu.me", + "staeamconmuninty.me", + "staeamconnunitly.online", + "staeamconnunitly.ru", + "staeamcromnuninty.com.profiles-76582109509.me", + "staem-communitu.info", + "staemcammunity.com", + "staemcammunity.me", + "staemcammynlty.ru", + "staemccommunnity.net.ru", + "staemcomcommunlty.ru.com", + "staemcomcommunlty.ru", + "staemcomconmunlty.ru.com", + "staemcommintu.ru", + "staemcomminuty.online", + "staemcomminuty.ru", + "staemcommmunity.com", + "staemcommmunity.online", + "staemcommmunity.ru", + "staemcommnity.ru", + "staemcommnuniti.com", + "staemcommnunity.ru.com", + "staemcommnutiy.ru", + "staemcommueneity.com", + "staemcommuinity.com", + "staemcommuneaity.com", + "staemcommunety.com", + "staemcommuneuity.com", + "staemcommuniity.com", + "staemcommunility.com", + "staemcommunily.com", + "staemcommunily.ru.com", + "staemcommuninity.org.ru", + "staemcommuninty.me", + "staemcommunitey.com", + "staemcommunitiy.com", + "staemcommunitu.com", + "staemcommunitu.ru", + "staemcommunity.click", + "staemcommunity.com.ru", + "staemcommunity.info", + "staemcommunity.org", + "staemcommunity.ru", + "staemcommunityi.com", + "staemcommunityu.ru.com", + "staemcommuniunity.com", + "staemcommunlty.com", + "staemcommunlty.fun", + "staemcommunlty.ru", + "staemcommunlty.us", + "staemcommunninty.com", + "staemcommunnity.club", + "staemcommunnity.com", + "staemcommunnity.ru", + "staemcommunniuty.com", + "staemcommunnlty.ru", + "staemcommuntiy.com", + "staemcommuntiy.ru", + "staemcommuntly.ru", + "staemcommunty.com", + "staemcommunty.ru", + "staemcommuntyi.ru", + "staemcommunulty.ru", + "staemcommunyti.ru.com", + "staemcommynity.xyz", + "staemcomnrnunitiy.ru.com", + "staemcomnuinty.ru", + "staemcomnumity.ru", + "staemcomnunity.fun", + "staemcomnunity.org", + "staemcomnunlty.ru", + "staemcomnunyti.club", + "staemcomnunyti.ru", + "staemcomnunyti.xyz", + "staemcomrnunity.ru.com", + "staemcomrnunity.ru", + "staemcomrnunity.store", + "staemcomrrunity.com", + "staemcomumity.com", + "staemcomunetys.ru.com", + "staemcomunitly.xyz", + "staemcomunity.com", + "staemcomunity.ru", + "staemcomunnity.com", + "staemcomunyti.ru", + "staemconmuilty.com", + "staemconmunilty.com", + "staemconmunity.com", + "staemconmunity.ru.com", + "staemconmunity.ru", + "staemconmunity.xyz", + "staemconmunlty.ru", + "staemcoommnunity.ru", + "staemcoommnuty.ru", + "staemcoommunity.ru", + "staemcoommunlty.ru", + "staemcoommuntiy.ru", + "staemcoommunty.ru", + "staemcoomnunlty.ru", + "staemcoomnunty.ru", + "staemcoomunity.ru", + "staemcoomuntiy.ru", + "staemcoomuunity.ru", + "staemcoomuunity.xyz", + "staemcoomuunty.ru", + "staemcormurnity.com", + "staemcornmunity.com", + "staemcornmunity.online", + "staemcornmunity.ru.com", + "staemcornmunity.ru", + "staemcornmunity.xyz", + "staemcornmuntiy.ru", + "staemcorrmunity.com", + "staemcrommuninty.com.profiles-76577258786.ml", + "staemcrommuninty.com", + "staemcrommunity.com.profiles-768590190751377476483.me", + "staemcrornmmunity.com.profiles-75921098086.me", + "staemcummunity.ru.com", + "staemcummunlty.com", + "staemmcommunity.ru", + "staemncrommunity.store", + "staempawered.xyz", + "staemporewed.xyz", + "staempovered.com", + "staempowered.space", + "staempowered.xyz", + "staermcormmunity.com", + "staermcrommunity.me", + "staermcrommunty.me", + "staermnconnumti.com", + "staerncoinunitiy.me", + "staerncormmunity.com", + "staerncornmunity.co", + "staerncornmunity.com", + "staffcups.ru", + "staffstatsgo.com", + "stamcomunnity.pp.ua", + "stamconnunnity.xyz", + "stammcommunity.com", + "stammcornunity.xyz", + "stampowered.com", + "starmcommunity.net", + "starrygamble.com", + "stat-csgo.ru", + "stats-cs.ru", + "stayempowered.org", + "stceamcomminity.com", + "stcommunity.xyz", + "ste-trade.ru.com", + "ste.amcommunity.com", + "stea-me.ru", + "stea-sgplay.ru", + "steaamcammunitiy.com", + "steaamcamunity.com", + "steaamcommmunity.com", + "steaamcommunity.club", + "steaamcommunnity.co", + "steaamcommunnity.com", + "steaamcommunnity.ru.com", + "steaamcomunity.com", + "steaamcomunity.net", + "steaamcomunity.ru.com", + "steaamconnmunlty.com", + "steaamcorrrmunity.com", + "steacmommunity.com", + "steacommnunity.com", + "steacommunilty.ru.com", + "steacommunity.com", + "steacommunity.net.ru", + "steacommunity.org.ru", + "steacommunity.ru.com", + "steacommunity.site", + "steacommunnity.com", + "steacommunty.ru", + "steacomnmunify.fun", + "steacomnmunity.com", + "steacomnunity.ru.com", + "steaemcamunity.xyz", + "steaemcommunity.pp.ru", + "steaemcommunity.ru.com", + "steaemcomunity.com", + "steaimcoimmunity.com", + "steaimcomminnity.ru", + "steaimcommnunity.com", + "steaimcommumitiy.com", + "steaimcommuniity.com", + "steaimcommunitiy.com", + "steaimcommunytiu.com", + "steaimecommintliy.com", + "steaimecommuninitiy.com", + "steaimecommunytiu.com", + "steaimecommunytu.com", + "steaimeecommunity.com", + "stealcommuniti.ru", + "stealcommunity.com", + "stealcommunlti.com", + "stealmcommulnitycom.xyz", + "stealmcommunity.ru", + "steam-account.ru.com", + "steam-account.ru", + "steam-account.site", + "steam-accounts.com", + "steam-analyst.ru", + "steam-announcements1.xyz", + "steam-auth.com", + "steam-auth.ru", + "steam-cammuneti.com", + "steam-communiity.ru", + "steam-community.net.ru", + "steam-community.org.ru", + "steam-community.ru.com", + "steam-community.xyz", + "steam-community1.xyz", + "steam-communitygifts.xyz", + "steam-communitygifts1.xyz", + "steam-communitysource.xyz", + "steam-communitysource1.xyz", + "steam-communitytrade.xyz", + "steam-comunity.me", + "steam-cs-good.ru", + "steam-cs.ru", + "steam-csgo-game.ru", + "steam-csgo-good.ru", + "steam-csgo-store.ru", + "steam-csgo.ru", + "steam-csgocom.ru", + "steam-csgogame.ru", + "steam-csgoplay.ru", + "steam-discord.com", + "steam-discord.ru", + "steam-discords.com", + "steam-dlscord.com", + "steam-free-nitro.ru", + "steam-g5chanaquyufuli.ru", + "steam-game-csgo.ru", + "steam-gametrade.xyz", + "steam-historyoffer.xyz", + "steam-hometrade.xyz", + "steam-hometrades.xyz", + "steam-hype.com", + "steam-login.ru", + "steam-login1.xyz", + "steam-nitro.com", + "steam-nitro.ru", + "steam-nitro.store", + "steam-nitros.com", + "steam-nitros.ru", + "steam-nltro.com", + "steam-nltro.ru", + "steam-nltros.ru", + "steam-offer.com", + "steam-offersgames.xyz", + "steam-offersofficial.xyz", + "steam-offerstore.xyz", + "steam-officialtrade.xyz", + "steam-play-csgo.ru", + "steam-povered.xyz", + "steam-power.xyz", + "steam-power1.xyz", + "steam-powered-games.com", + "steam-powered.xyz", + "steam-powered1.xyz", + "steam-poweredexchange.xyz", + "steam-poweredoffer.xyz", + "steam-poweredoffers.xyz", + "steam-poweredtrades.xyz", + "steam-profile.com", + "steam-promo-page.ml", + "steam-rep.com", + "steam-ru.ru", + "steam-service.ru", + "steam-servicedeals.xyz", + "steam-servicedeals1.xyz", + "steam-site.ru", + "steam-sourcecommunity.xyz", + "steam-sourcecommunity1.xyz", + "steam-storetrade.xyz", + "steam-storetrade1.xyz", + "steam-support.xyz", + "steam-trade.xyz", + "steam-tradegame.xyz", + "steam-tradehome.xyz", + "steam-tradeoffer.com", + "steam-tradeoffer.xyz", + "steam-trades.icu", + "steam-tradeshome.xyz", + "steam-tradestore.xyz", + "steam-tradestore1.xyz", + "steam.99box.com", + "steam.cards", + "steam.cash", + "steam.cheap", + "steam.codes", + "steam.communty.com", + "steam.communyty.worldhosts.ru", + "steam.comnunity.com", + "steam.luancort.com", + "steam.mmosvc.com", + "steam4you.online", + "steamaccount.xyz", + "steamaccountgenerator.ru.com", + "steamaccounts.net", + "steamaccounts.org", + "steamacommunity.com", + "steamanalysts.com", + "steambrowser.xyz", + "steamc0mmunity.com", + "steamc0munnity.site", + "steamcamiutity.com", + "steamcammiuniltty.com", + "steamcammmunity.ru", + "steamcammnuity.com", + "steamcammuinity.com", + "steamcammuniety.com", + "steamcammunitey.com", + "steamcammuniti.ru", + "steamcammunitu.com", + "steamcammunitu.ru.com", + "steamcammunity-profile.ru", + "steamcammunity.net", + "steamcammunity.top", + "steamcammunlty.ru", + "steamcammuntiy.com", + "steamcammunty.com", + "steamcammunuty.com", + "steamcammunyty.fun", + "steamcammunyty.ru", + "steamcamnunity.com.ru", + "steamcamnunity.ru", + "steamcamunite.com", + "steamcamunitey.com", + "steamcamunitu.com", + "steamcamunitu.xyz", + "steamcamunity-profile.ru", + "steamcamunity.com", + "steamcamunity.ru", + "steamcamunity.top", + "steamcamunity.xyz", + "steamcamunlty.com", + "steamcamunnity.xyz", + "steamcannunlty.com", + "steamcard.me", + "steamccommuniity.com", + "steamccommunity.com", + "steamccommunity.net", + "steamccommunity.ru.com", + "steamccommunityy.ru", + "steamccommunyty.ru", + "steamccommurity.ru", + "steamccommyunity.com", + "steamccomunnity.ru.com", + "steamcconmmuunity.co", + "steamchinacsgo.ru", + "steamcmmunuti.ru", + "steamcmmunyti.ru", + "steamcmunity.com", + "steamco.mmunity.com", + "steamco.ru", + "steamcoarnmmnunity.ru.com", + "steamcodesgen.com", + "steamcokmunity.com", + "steamcomannlty.xyz", + "steamcombain.com", + "steamcomcmunlty.com", + "steamcomcunity.ru", + "steamcominity.ru", + "steamcominuty.ru", + "steamcomity.com", + "steamcomiunity.com", + "steamcomiunity.xyz", + "steamcomiynuytiy.net.ru", + "steamcommenitry.ru", + "steamcommenity.ru", + "steamcommeunity.com", + "steamcommhnity.com", + "steamcomminiity.site", + "steamcomminiti.ru", + "steamcomminity.com", + "steamcomminity.ru.com", + "steamcomminity.ru", + "steamcomminnty.com", + "steamcommintty.com", + "steamcomminty.ru", + "steamcomminulty.ru", + "steamcomminuly.com", + "steamcomminuly.ru", + "steamcomminutiiu.ru", + "steamcomminutiu.ru", + "steamcomminutiy.ru", + "steamcomminutty.ru", + "steamcomminuty-offer.ru.com", + "steamcomminuty.click", + "steamcomminuty.com", + "steamcomminuty.link", + "steamcomminuty.me", + "steamcomminuty.nl", + "steamcomminuty.repl.co", + "steamcomminuty.ru.com", + "steamcomminuty.ru", + "steamcomminuty.xyz", + "steamcomminyti.ru", + "steamcomminytiu.com", + "steamcomminytiu.ru", + "steamcomminytiy.ru", + "steamcomminytu.click", + "steamcomminytu.com", + "steamcomminytu.link", + "steamcomminytu.ru", + "steamcomminyty.ru.com", + "steamcommiuinity.com", + "steamcommiunitiy.pp.ru", + "steamcommiunitty.ru", + "steamcommiunity.pp.ru", + "steamcommiunity.ru", + "steamcommiunniutty.net.ru", + "steamcommiunty.ru", + "steamcommiynitiy.net.ru", + "steamcommllty.com", + "steamcommlnuty.com", + "steamcommlunity.com", + "steamcommmuiniity.ru", + "steamcommmunitty.site", + "steamcommmunity.xyz", + "steamcommmunlity.com", + "steamcommmunnity.com", + "steamcommmunty.com", + "steamcommninty.com", + "steamcommnity.com.ru", + "steamcommnity.com", + "steamcommnity.ru", + "steamcommnity.store", + "steamcommnlty.com", + "steamcommnlty.xyz", + "steamcommnmunity.ru", + "steamcommnnity.net.ru", + "steamcommnnunity.ru", + "steamcommnnunnity.world", + "steamcommntiy.xyz", + "steamcommnuitly.com", + "steamcommnuitty.com", + "steamcommnultiy.ru", + "steamcommnulty.com", + "steamcommnulty.store", + "steamcommnunily.com", + "steamcommnunily.xyz", + "steamcommnuninty.com", + "steamcommnuninty.ru.com", + "steamcommnunitlu.com", + "steamcommnunitu.com", + "steamcommnunity.com", + "steamcommnunity.org.ru", + "steamcommnunity.ru.com", + "steamcommnunlty.com", + "steamcommnunlty.icu", + "steamcommnunlty.ru", + "steamcommnunlty.xyz", + "steamcommnunmity.com", + "steamcommnunniiy.net.ru", + "steamcommnuntiy.com", + "steamcommnunty.ru", + "steamcommnunylti.com", + "steamcommnunyti.com", + "steamcommnunytl.com", + "steamcommnutly.ru.com", + "steamcommnutry.com", + "steamcommnutry.ru", + "steamcommnuty.site", + "steamcommnuuntiy.com", + "steamcommonitey.com", + "steamcommonnnity.ru.com", + "steamcommqnity.com", + "steamcommrnunity.com", + "steamcommrunitly.com", + "steamcommrutiny.ru", + "steamcommtity.com", + "steamcommuanity.ru.com", + "steamcommuenity.com", + "steamcommuhity.ru", + "steamcommuhuity.com", + "steamcommuilty.ru", + "steamcommuinilty.com", + "steamcommuininty.com", + "steamcommuinitiycom.ru", + "steamcommuinity.com", + "steamcommuinity.ru", + "steamcommuinty.com.ru", + "steamcommuinuity.com", + "steamcommuiti.ru", + "steamcommuitliy.com", + "steamcommuitly.ru", + "steamcommuity.com", + "steamcommuity.ru", + "steamcommulity.ru", + "steamcommulltty.com", + "steamcommullty.ru", + "steamcommulnity.com", + "steamcommulnt.ru.com", + "steamcommulnty.ru", + "steamcommulty.ru", + "steamcommumilty.com", + "steamcommumitiy.com", + "steamcommumituy.com", + "steamcommumity.biz", + "steamcommumity.net", + "steamcommumiuty.com", + "steamcommumlity.com", + "steamcommumnity.com", + "steamcommumtiy.com", + "steamcommun1ty.ru", + "steamcommunely.ru", + "steamcommuneteiy.com", + "steamcommunetiy.com", + "steamcommunetiy.ru", + "steamcommunetiyi.com", + "steamcommunetiyy.xyz", + "steamcommunetu.com", + "steamcommunety.com", + "steamcommunety.net.ru", + "steamcommunety.online", + "steamcommunety.org.ru", + "steamcommunety.ru", + "steamcommunety1i.com", + "steamcommunetyei.com", + "steamcommuneuity.ru", + "steamcommunhity.com", + "steamcommuni.com", + "steamcommunicty.com", + "steamcommunicty.ru.com", + "steamcommunidy.com", + "steamcommunieityi.com", + "steamcommunieti.ru", + "steamcommunietiy.com", + "steamcommuniety.com", + "steamcommuniety.ru", + "steamcommunifly.ru.com", + "steamcommunify.com", + "steamcommunify.ru", + "steamcommunihty.com", + "steamcommuniiity.com", + "steamcommuniilty.ru", + "steamcommuniitu.site", + "steamcommuniity.com.ru", + "steamcommuniiy.online", + "steamcommuniiy.ru", + "steamcommunikkty.net.ru", + "steamcommunili.xyz", + "steamcommunility.com", + "steamcommunillty.com", + "steamcommunillty.net.ru", + "steamcommunillty.ru.com", + "steamcommunillty.ru", + "steamcommunilly.com", + "steamcommuniltily.ru.com", + "steamcommuniltiy.online", + "steamcommuniltiy.ru", + "steamcommuniltly.com", + "steamcommunilty.buzz", + "steamcommunilty.it", + "steamcommunilty.ru.com", + "steamcommunilty.us", + "steamcommunilty.xyz", + "steamcommuniltys.com", + "steamcommunilv.com", + "steamcommunily.buzz", + "steamcommunily.org", + "steamcommunily.uno", + "steamcommunimty.ru.com", + "steamcommuninity.ru.com", + "steamcommuninthy.com", + "steamcommuninty.ru.com", + "steamcommuninunty.com", + "steamcommunirtly.ru.com", + "steamcommunirty.com", + "steamcommunirty.ru.com", + "steamcommuniry.com", + "steamcommuniry.net.ru", + "steamcommuniry.ru", + "steamcommunit.org.ru", + "steamcommunit.ru.com", + "steamcommunit.ru", + "steamcommunitcy.ru.com", + "steamcommunite.com", + "steamcommunite.ru", + "steamcommunitey.com", + "steamcommunitey.ru", + "steamcommuniteypowered.com", + "steamcommunitfy.com", + "steamcommunitfy.ru.com", + "steamcommunithy.com", + "steamcommuniti.com.ru", + "steamcommuniti.org.ru", + "steamcommuniti.org", + "steamcommuniti.ru.com", + "steamcommunitie.net", + "steamcommunitie.ru.com", + "steamcommunitie.ru", + "steamcommunitie.site", + "steamcommunities.biz", + "steamcommunitii.xyz", + "steamcommunitily.com", + "steamcommunitity.com", + "steamcommunitiu.ru", + "steamcommunitiv.com", + "steamcommunitiy.ru", + "steamcommunitiycom.ru", + "steamcommunitiyu.com", + "steamcommunitiyy.com", + "steamcommunitj.buzz", + "steamcommunitl.com", + "steamcommunitl.net.ru", + "steamcommunitli.ru", + "steamcommunitlil.ru", + "steamcommunitliy.ru.com", + "steamcommunitlly.com", + "steamcommunitlly.net", + "steamcommunitlly.ru.com", + "steamcommunitlu.com", + "steamcommunitluy.com", + "steamcommunitly.com", + "steamcommunitly.me", + "steamcommunitmy.ru.com", + "steamcommunitry.com", + "steamcommunitry.ru", + "steamcommunitte.com", + "steamcommunitte.ru", + "steamcommunittey.com", + "steamcommunittrade.xyz", + "steamcommunittru.co", + "steamcommunittry.xyz", + "steamcommunitty.com.ru", + "steamcommunitty.esplay.eu", + "steamcommunitty.net", + "steamcommunitty.site", + "steamcommunitty.top", + "steamcommunitu.com-profile-poka.biz", + "steamcommunitu.com-profiles-mellenouz.trade", + "steamcommunitu.icu", + "steamcommunitu.net", + "steamcommunitu.ru.com", + "steamcommunitv.ru", + "steamcommunitvs.com", + "steamcommunitx.ru.com", + "steamcommunity-com.xyz", + "steamcommunity-comtradeoffer.ru", + "steamcommunity-gifts.xyz", + "steamcommunity-gifts1.xyz", + "steamcommunity-nitro.ru", + "steamcommunity-nitrogeneral.ru", + "steamcommunity-profile.net", + "steamcommunity-profiles.ru.com", + "steamcommunity-source.xyz", + "steamcommunity-source1.xyz", + "steamcommunity-trade.xyz", + "steamcommunity-tradeoffer.com", + "steamcommunity-tradeoffer.ru.com", + "steamcommunity-tradeoffer4510426522.ru", + "steamcommunity-tradeoffers.com", + "steamcommunity-user.me", + "steamcommunity-xpubg.xyz", + "steamcommunity.at", + "steamcommunity.best", + "steamcommunity.biz", + "steamcommunity.ca", + "steamcommunity.click", + "steamcommunity.cloud", + "steamcommunity.cn", + "steamcommunity.co.ua", + "steamcommunity.com-id-k4tushatwitchbabydota.ru", + "steamcommunity.com.ru", + "steamcommunity.comlappl251490lrust.ru", + "steamcommunity.de", + "steamcommunity.digital", + "steamcommunity.eu", + "steamcommunity.in", + "steamcommunity.link", + "steamcommunity.live", + "steamcommunity.llc", + "steamcommunity.mobi", + "steamcommunity.moscow", + "steamcommunity.net.in", + "steamcommunity.pl", + "steamcommunity.pp.ru", + "steamcommunity.rest", + "steamcommunity.ru.net", + "steamcommunity.ru", + "steamcommunity.site", + "steamcommunity.steams.ga", + "steamcommunity.support", + "steamcommunity.team", + "steamcommunity.trade", + "steamcommunity.us", + "steamcommunity1.com", + "steamcommunitya.com", + "steamcommunityc.com", + "steamcommunityc.ru", + "steamcommunitycom.ru.com", + "steamcommunitycomoffernewpartner989791155tokenjbhldtj6.trade", + "steamcommunitycomtradeoffer.ru.com", + "steamcommunitygames.com", + "steamcommunitygifts.xyz", + "steamcommunitygifts1.xyz", + "steamcommunityi.com", + "steamcommunityi.ru.com", + "steamcommunityi.ru", + "steamcommunityid.ru", + "steamcommunitylink.xyz", + "steamcommunitym.com", + "steamcommunitym.ru", + "steamcommunitynow.com", + "steamcommunityo.com", + "steamcommunityoff.com", + "steamcommunityoffers.org", + "steamcommunitypubg.com", + "steamcommunityr.com.ru", + "steamcommunityru.tk", + "steamcommunityshop.com", + "steamcommunitysource.xyz", + "steamcommunitysource1.xyz", + "steamcommunitytradeofer.com", + "steamcommunitytradeoffer.com", + "steamcommunitytradeoffer.ru", + "steamcommunitytradeoffter.com", + "steamcommunitytradeofter.com", + "steamcommunitytredeoffer.com", + "steamcommunityu.com", + "steamcommunityu.ru", + "steamcommunityw.com", + "steamcommunityw.net.ru", + "steamcommunityw.org.ru", + "steamcommunitywork.com", + "steamcommunitywork.ml", + "steamcommunityx.com", + "steamcommunityy.online", + "steamcommunityy.ru", + "steamcommunityz.com", + "steamcommunityzbn.top", + "steamcommunityzbo.top", + "steamcommunityzbq.top", + "steamcommunityzbr.top", + "steamcommunityzcd.top", + "steamcommunityzce.top", + "steamcommunityzci.top", + "steamcommunityzda.top", + "steamcommunityzdb.top", + "steamcommunityzdd.top", + "steamcommunityzdl.top", + "steamcommunityzdp.top", + "steamcommunityzdq.top", + "steamcommunityzdr.top", + "steamcommunityzds.top", + "steamcommunityzdt.top", + "steamcommuniuity.com", + "steamcommuniutiiy.com", + "steamcommuniutiy.ru", + "steamcommuniuty.ru", + "steamcommuniy.com", + "steamcommuniyt.com", + "steamcommuniytu.com", + "steamcommuniyty.ru", + "steamcommunjti.com", + "steamcommunjtv.xyz", + "steamcommunjty.net", + "steamcommunjty.ru", + "steamcommunlilty.ru.com", + "steamcommunlite.com", + "steamcommunlitily.ru.com", + "steamcommunlitly.ru", + "steamcommunlitty.ru.com", + "steamcommunlitty.ru", + "steamcommunlity.net", + "steamcommunlity.ru.com", + "steamcommunlity.ru", + "steamcommunlityl.ru", + "steamcommunliu.com", + "steamcommunlky.net.ru", + "steamcommunllity.ru.com", + "steamcommunllty.com", + "steamcommunllty.ru", + "steamcommunlte.ru", + "steamcommunltiy.club", + "steamcommunltiy.com", + "steamcommunltty.com", + "steamcommunltu.com", + "steamcommunltuy.com", + "steamcommunltv.buzz", + "steamcommunlty-proflle.com.ru", + "steamcommunlty.biz", + "steamcommunlty.business", + "steamcommunlty.cloud", + "steamcommunlty.company", + "steamcommunlty.info", + "steamcommunlty.link", + "steamcommunlty.pro", + "steamcommunlty.shop", + "steamcommunlty.site", + "steamcommunlty.store", + "steamcommunlty.top", + "steamcommunltyu.ru", + "steamcommunltyy.com", + "steamcommunly.com", + "steamcommunly.net.ru", + "steamcommunmity.com.ru", + "steamcommunniittly.ru", + "steamcommunniitty.com", + "steamcommunniity.com", + "steamcommunniity.net", + "steamcommunniity.ru", + "steamcommunnilty.com", + "steamcommunnilty.ru", + "steamcommunnitey.com", + "steamcommunnitlly.ru", + "steamcommunnitty.ru", + "steamcommunnity.co", + "steamcommunnity.com.ru", + "steamcommunnity.ml", + "steamcommunnity.net", + "steamcommunnity.ru.com", + "steamcommunnity.ru", + "steamcommunnjty.com", + "steamcommunnlity.ru", + "steamcommunnlty.com.ru", + "steamcommunnty.ru", + "steamcommunnuty.ru", + "steamcommunrinty.ru.com", + "steamcommunrity.com", + "steamcommunrity.ru.com", + "steamcommunrlity.com", + "steamcommunrrity.com", + "steamcommunti.com", + "steamcommuntily.ru.com", + "steamcommuntily.ru", + "steamcommuntity.com", + "steamcommuntity.ru.com", + "steamcommuntiv.com", + "steamcommuntiy.com", + "steamcommuntli.ru", + "steamcommuntliy.ru", + "steamcommuntly.com", + "steamcommuntry.com", + "steamcommunty.buzz", + "steamcommunty.com.ru", + "steamcommunty.com", + "steamcommunty.net", + "steamcommunty.pw", + "steamcommunty.ru.com", + "steamcommuntyy.ru", + "steamcommunuaity.xyz", + "steamcommunuety.ru", + "steamcommunuity.net", + "steamcommunuity.ru", + "steamcommununty-con.ru", + "steamcommununty.ru", + "steamcommunury.ru", + "steamcommunute.com", + "steamcommunuti.co", + "steamcommunuti.ru", + "steamcommunutii.ru", + "steamcommunutiy.com", + "steamcommunutry.com", + "steamcommunutry.ru", + "steamcommunutty.com", + "steamcommunutty.ru", + "steamcommunutuy.com", + "steamcommunuty.buzz", + "steamcommunuty.co", + "steamcommunuty.link", + "steamcommunuty.org.ru", + "steamcommunuty.ru", + "steamcommunutyu.com", + "steamcommunvti.ru", + "steamcommunyity.ru", + "steamcommunylty.ru", + "steamcommunyte.com", + "steamcommunyti.com", + "steamcommunyti.info", + "steamcommunytitradeoffer.com", + "steamcommunytiu.com", + "steamcommunytiu.ru", + "steamcommunytiy.ru", + "steamcommunytiy.tk", + "steamcommunytu.ru", + "steamcommunyty.com", + "steamcommunyty.ru.com", + "steamcommunyty.xyz", + "steamcommunytytradeofferphobos.ru", + "steamcommuriity.com", + "steamcommurity.ru", + "steamcommurjty.com", + "steamcommurlity.com", + "steamcommurlty.com", + "steamcommurnity.com", + "steamcommurnuity.com", + "steamcommutinny.ru.com", + "steamcommutiny.com", + "steamcommutiny.ru.com", + "steamcommutiny.ru", + "steamcommutiny.xyz", + "steamcommutry.ru", + "steamcommuty.com", + "steamcommutyniu.com", + "steamcommutyniy.com", + "steamcommuuity.net.ru", + "steamcommuulty.com", + "steamcommuunitey.com", + "steamcommuunitty.ru.com", + "steamcommuunity.net.ru", + "steamcommuunity.pp.ru", + "steamcommuunity.ru.com", + "steamcommuunity.ru", + "steamcommuunjty.com", + "steamcommuunlity.com", + "steamcommuunlty.com", + "steamcommuwunity.com", + "steamcommuynity.ru.com", + "steamcommyinuty.ru", + "steamcommymity.ru", + "steamcommynite.com", + "steamcommyniti.ru", + "steamcommyniti.xyz", + "steamcommynitiu.com", + "steamcommynitry.ru", + "steamcommynitu.com", + "steamcommynitu.net.ru", + "steamcommynitu.ru.com", + "steamcommynitu.ru", + "steamcommynitu.xyz", + "steamcommynituy.com", + "steamcommynity.icu", + "steamcommynity.ru", + "steamcommynity.space", + "steamcommynityprofile.ru", + "steamcommynltu.com", + "steamcommynlty.com", + "steamcommynlty.ru", + "steamcommynnityy.com", + "steamcommynuti.ru", + "steamcommynutiy.ru", + "steamcommynutu.ru", + "steamcommynuty.ru.com", + "steamcommynyti.ru", + "steamcommynyti.site", + "steamcommytiny.com", + "steamcommytuniu.com", + "steamcommyuinity.net.ru", + "steamcommyunity.com", + "steamcomnenity.ru.com", + "steamcomninuty.ru.com", + "steamcomninytiu.com", + "steamcomniunity.com", + "steamcomnmnuty.ru", + "steamcomnmrunity.online", + "steamcomnmrunity.ru", + "steamcomnmufly.ru.com", + "steamcomnmuituy.com", + "steamcomnmuity.ru", + "steamcomnmunity.com.ru", + "steamcomnmunlty.com", + "steamcomnmuntiy.ru.com", + "steamcomnmutly.ru.com", + "steamcomnmuunity.ru.com", + "steamcomnmynitu.com", + "steamcomnnity.net.ru", + "steamcomnnlty.com", + "steamcomnnuity.com", + "steamcomnnunilty.com", + "steamcomnnunity.co", + "steamcomnnunity.ru.com", + "steamcomnnunity.ru", + "steamcomnnunlty.ru", + "steamcomnnunty.ru", + "steamcomnnuty.ru", + "steamcomnnynlty.com", + "steamcomnuenuity.com", + "steamcomnuhity.com", + "steamcomnuiti.xyz", + "steamcomnulty.com", + "steamcomnumilty.com", + "steamcomnumily.com", + "steamcomnumity.com", + "steamcomnumity.org.ru", + "steamcomnumity.ru.com", + "steamcomnumity.ru", + "steamcomnumity.xyz", + "steamcomnumlity.com", + "steamcomnumlty.com", + "steamcomnumlty.ru", + "steamcomnumnity.com", + "steamcomnumty.ru", + "steamcomnuniity.com.ru", + "steamcomnuniity.pp.ru", + "steamcomnuniity.ru.com", + "steamcomnunilty.com", + "steamcomnunilty.ru.com", + "steamcomnunily.co", + "steamcomnunirty.ru", + "steamcomnuniti.com", + "steamcomnunitiy.com", + "steamcomnunitiy.ru", + "steamcomnunitly.com", + "steamcomnunitly.tk", + "steamcomnunitry.ru", + "steamcomnunitty.com", + "steamcomnunity.com", + "steamcomnunity.net", + "steamcomnunity.org.ru", + "steamcomnunity.ru", + "steamcomnunity.site", + "steamcomnunityprofile.ru.com", + "steamcomnunlity.com", + "steamcomnunlity.ru", + "steamcomnunllty.com", + "steamcomnunllty.net", + "steamcomnunlty.ru.com", + "steamcomnunlty.ru", + "steamcomnunluty.ru", + "steamcomnunmity.com", + "steamcomnunnirty.ru", + "steamcomnunniry.ru", + "steamcomnunnity.com", + "steamcomnunnity.net.ru", + "steamcomnunnity.net", + "steamcomnunnlty.ru", + "steamcomnuntiy.com", + "steamcomnuntty.ru.com", + "steamcomnunutiy.ru", + "steamcomnunuty.com", + "steamcomnunuty.ru", + "steamcomnunytu.ru", + "steamcomnurity.com", + "steamcomnurity.xyz", + "steamcomnutiny.online", + "steamcomnutiny.ru.com", + "steamcomnutiny.ru", + "steamcomnuty.com", + "steamcomnuunlty.com", + "steamcomnynlity.ru", + "steamcomonity.com", + "steamcomrmunity.ru.com", + "steamcomrmunnuity.ru.com", + "steamcomrneuneity.com", + "steamcomrninuty.link", + "steamcomrninuty.ru", + "steamcomrninuty.site", + "steamcomrnity.xyz", + "steamcomrnlnuty.site", + "steamcomrnumity.com", + "steamcomrnunite.com", + "steamcomrnuniti.ru.com", + "steamcomrnunitu.ru.com", + "steamcomrnunitu.ru", + "steamcomrnunity.com.ru", + "steamcomrnunity.online", + "steamcomrnunity.ru.com", + "steamcomrnunity.ru", + "steamcomrnunity.site", + "steamcomrnunity.su", + "steamcomrnunity.xyz", + "steamcomrnunlty.com", + "steamcomrnunlty.ru", + "steamcomrnunuity.ru.com", + "steamcomrnyniti.ru.com", + "steamcomrnyniti.ru", + "steamcomrrnunity.com", + "steamcomrrnunity.net.ru", + "steamcomrrnunity.ru", + "steamcomrunily.com", + "steamcomrunity.com", + "steamcomueniity.ru", + "steamcomumity.com", + "steamcomumunty.com", + "steamcomunety.com", + "steamcomunety.ru", + "steamcomuniety.ru", + "steamcomuniiity.com", + "steamcomuniitly.ru.com", + "steamcomuniity.ru.com", + "steamcomunillty.ru.com", + "steamcomuniltu.xyz", + "steamcomunilty.com", + "steamcomunily.ru.com", + "steamcomuninruty.ru", + "steamcomuniti.com", + "steamcomuniti.ru", + "steamcomuniti.xyz", + "steamcomunitly.pp.ru", + "steamcomunitly.ru", + "steamcomunitty.ru.com", + "steamcomunitu.com", + "steamcomunitu.net.ru", + "steamcomunitu.ru", + "steamcomunituy.com", + "steamcomunity-comid12121212123244465.ru", + "steamcomunity-nitro-free.ru", + "steamcomunity.com.ru", + "steamcomunity.com", + "steamcomunity.me", + "steamcomunity.net.ru", + "steamcomunity.org.ru", + "steamcomunity.ru", + "steamcomunity.us", + "steamcomunityo.com", + "steamcomunitytrades.xyz", + "steamcomunityy.com", + "steamcomunlitly.ru.com", + "steamcomunlty.ru.com", + "steamcomunmity.ru.com", + "steamcomunniity.ru", + "steamcomunninuty.com", + "steamcomunnitly.ru.com", + "steamcomunnitu.xyz", + "steamcomunnity.fun", + "steamcomunnity.ru.com", + "steamcomunnity.site", + "steamcomunnity.xyz", + "steamcomunnlty.com", + "steamcomunnuity.com", + "steamcomunnuty.com", + "steamcomunnyti.ru", + "steamcomuntty.com", + "steamcomunty.org.ru", + "steamcomunuty.com", + "steamcomunuty.ru", + "steamcomunyiti.ru.com", + "steamcomunyti.com", + "steamcomunytiu.com", + "steamcomuuniity.com", + "steamcomuunity.com", + "steamcomuunity.ru.com", + "steamcomyniti.xyz", + "steamcomynitu.ru", + "steamcomynity.ru", + "steamcomynlty.com", + "steamcomynnitu.net.ru", + "steamconimmunity.com", + "steamconminuty.ru", + "steamconmiunity.ru", + "steamconmmuntiy.com", + "steamconmnmnunity.ru", + "steamconmnmunity.ru", + "steamconmnunitiy.ru.com", + "steamconmnunitiy.ru", + "steamconmnunity.co", + "steamconmnunity.com", + "steamconmnunity.ru", + "steamconmnunuty.ru.com", + "steamconmnutiny.ru", + "steamconmuhlty.com", + "steamconmumity.com.ru", + "steamconmumity.com", + "steamconmumity.ru.com", + "steamconmumity.ru", + "steamconmumltu.com.ru", + "steamconmummity.ru", + "steamconmumnity.com", + "steamconmuniti.ru", + "steamconmunitly.com", + "steamconmunitty.com", + "steamconmunity.co", + "steamconmunity.com.ru", + "steamconmunity.pp.ru", + "steamconmunity.xyz", + "steamconmunjty.com", + "steamconmunlly.com", + "steamconmunlty.com.ru", + "steamconmunlty.com", + "steamconmunlty.ru", + "steamconmunnitry.ru", + "steamconmunnlty.ru", + "steamconmunuty.ru", + "steamconmunyty.com", + "steamconmunyty.ru", + "steamconnmuhity.com", + "steamconnmunitu.net.ru", + "steamconnmunity.ru", + "steamconnmunlty.com", + "steamconnmunlty.ru.com", + "steamconnmunlty.ru", + "steamconnnnunity.net.ru", + "steamconnnnunity.org.ru", + "steamconnumity.ru.com", + "steamconnummity.ru", + "steamconnumuty.com", + "steamconnuniitty.tk", + "steamconnunirty.ru", + "steamconnunitiy.com", + "steamconnunity.com.ru", + "steamconnunity.com", + "steamconnunity.de", + "steamconnunity.fun", + "steamconnunity.net", + "steamconnunity.pp.ru", + "steamconnunity.ru.com", + "steamconnunlty.com", + "steamconummity.ru", + "steamconunity.cf", + "steamconunity.ru", + "steamconunity.tk", + "steamconunlty.ru", + "steamconynuyty.net.ru", + "steamconynuyty.org.ru", + "steamcoominuty.site", + "steamcoomminuty.site", + "steamcoommunety.com", + "steamcoommuniity.link", + "steamcoommuniity.ru", + "steamcoommunilty.com", + "steamcoommunity.pp.ru", + "steamcoommunity.ru.com", + "steamcoommunllty.com", + "steamcoommunlty.ru", + "steamcoommunuity.com", + "steamcoommunuty.com", + "steamcoomrnmunity.ml", + "steamcoomunity-nitro.site", + "steamcoomunitye.com", + "steamcoomunjty.com", + "steamcoomunlty.com", + "steamcoomunlty.net", + "steamcoomunlty.ru", + "steamcoomunnity.com", + "steamcoomunnity.ru", + "steamcoomynity.ru", + "steamcoonmuntiy.ru", + "steamcoormmunity.com", + "steamcormmmunity.com", + "steamcormmunity.com", + "steamcormmunity.net.ru", + "steamcormmunity.ru.com", + "steamcormmuntiy.com", + "steamcormmuuity.ru", + "steamcormrunity.com", + "steamcormunity.ru", + "steamcormunity.xyz", + "steamcormurnity.com", + "steamcornminity.ru.com", + "steamcornminty.xyz", + "steamcornminuty.com", + "steamcornmmunity.com", + "steamcornmnitu.ru.com", + "steamcornmnuity.com", + "steamcornmunety.com", + "steamcornmunify.ru.com", + "steamcornmuniity.net.ru", + "steamcornmunily.ru", + "steamcornmunit.ru.com", + "steamcornmunite.com", + "steamcornmunity.fun", + "steamcornmunity.net.ru", + "steamcornmunity.org", + "steamcornmunty.com", + "steamcornmunyti.ru", + "steamcornmynitu.ru", + "steamcornmynity.ru", + "steamcornrnuity.com", + "steamcornrnunity.com.ru", + "steamcornrnunity.fun", + "steamcornrrnunity.com", + "steamcorrmunity.com", + "steamcorrnmunity.ru", + "steamcorrnunity.org", + "steamcoummunitiy.com", + "steamcoummunity.com", + "steamcrommunlty.me", + "steamcromnmunity-com.profiles-7685981598976.me", + "steamcronnmmuniry.me", + "steamcsgo-game.ru", + "steamcsgo-play.ru", + "steamcsgo.ru", + "steamcsgoplay.ru", + "steamcummunity.com.ru", + "steamcummunity.com", + "steamcummunity.ru.com", + "steamcummunity.ru", + "steamcummunityy.pp.ua", + "steamcummunnity.com", + "steamcumumunity.com.ru", + "steamdesksupport.com", + "steamdiscord.com", + "steamdiscord.ru", + "steamdiscordi.com", + "steamdiscordj.com", + "steamdiscords.com", + "steamdiscrod.ru", + "steamdlscord.com", + "steamdlscords.com", + "steamdocs.xyz", + "steamdomain.online", + "steamdomain.ru", + "steamdommunity.com", + "steamecommuinty.com", + "steamecommunitiiy.com", + "steamecommunitiy.com", + "steamecommunituiy.com", + "steamecommunity.net", + "steamecommunity.org", + "steamecommunity.pp.ua", + "steamecommunity.ru.com", + "steamecommuniuty.com", + "steamecommunlty.com.ru", + "steamecommunlty.com", + "steamecommunytu.com", + "steamecomunity.com.ru", + "steamedpowered.com", + "steamepowered.com", + "steamescommunity.com", + "steamgame-csgo.ru", + "steamgame-trade.xyz", + "steamgame.net.ru", + "steamgamepowered.net", + "steamgames.net.ru", + "steamgamesroll.ru", + "steamgametrade.xyz", + "steamgiftcards.cf", + "steamgifts.net.ru", + "steamgiveaway.cc", + "steamgiveawayfree.ru", + "steamgivenitro.com", + "steamglft.ru", + "steamguard.ir", + "steamhelp.net", + "steamhome-trade.xyz", + "steamhome-trades.xyz", + "steamhometrade.xyz", + "steamhometrades.xyz", + "steamicommunnity.com", + "steamid.ru", + "steamitem.xyz", + "steamkey.ru", + "steamkommunity.net.ru", + "steamkommunity.org.ru", + "steamlcommunity.net.ru", + "steamlcommunity.org.ru", + "steamlcommunity.ru.com", + "steamm.store", + "steammatily.online", + "steammatily.ru", + "steammcamunitu.com", + "steammcamunity.com", + "steammcamunity.ru.com", + "steammcomminity.ru", + "steammcomminuty.ru", + "steammcommmunlty.pp.ua", + "steammcommunety.com", + "steammcommuniity.ru", + "steammcommunily.net.ru", + "steammcommunitey.com", + "steammcommunitly.ru", + "steammcommunity-trade.xyz", + "steammcommunity.com", + "steammcommunity.ru.com", + "steammcommunity.ru", + "steammcommunnity.ru", + "steammcommunyti.ru", + "steammcommuunityy.ru.com", + "steammcomtradeoff.com", + "steammcomunit.ru", + "steammcomunity.ru", + "steammcomunlty.ru", + "steammcomunnity.com", + "steammcounity.ru.com", + "steammecommunity.com", + "steammncommunty.ru.com", + "steamncommnunity.ru", + "steamncommnunty.ru", + "steamncommuinity.com", + "steamncommumity.ru", + "steamncommuniity.com", + "steamncommunitiy.com", + "steamncommunitu.co", + "steamncommunity.com", + "steamncommunity.pp.ru", + "steamncommunity.ru", + "steamncommunity.xyz", + "steamncommunytu.ru", + "steamncomnunlty.com.ru", + "steamncomunitity.com", + "steamncomunity.com", + "steamncomunity.xyz", + "steamnconmunity.com", + "steamnconmunity.ru.com", + "steamnconmunity.work", + "steamnconnmunity.com", + "steamnitro.com", + "steamnitrol.com", + "steamnitros.com", + "steamnitros.ru", + "steamnitrro.com", + "steamnltro.com", + "steamnltros.com", + "steamnltros.ru", + "steamnmcomunnity.co", + "steamocmmunity.me", + "steamoemmunity.com", + "steamoffer-store.xyz", + "steamoffered.trade", + "steamoffergames.xyz", + "steamommunity.com", + "steamoowered.com", + "steamowered.com", + "steampawared.club", + "steampawered.store", + "steampcwered.com", + "steampewared.com", + "steampewered.com", + "steampiwered.com", + "steampoeer.com", + "steampoeerd.com", + "steampoewred.com", + "steampoiwered.com", + "steampoowered.com", + "steampowaered.com", + "steampoward.com", + "steampowder.com", + "steampowed.com", + "steampoweded.com", + "steampoweeed.com", + "steampowened.ru.com", + "steampower.co", + "steampower.de", + "steampower.space", + "steampowerco.com", + "steampowerd.com", + "steampowerd.net", + "steampowerde.com", + "steampowerded.com", + "steampowerdwallet.com", + "steampowere.com", + "steampoweread.com", + "steampowerec.com", + "steampowered-offer.xyz", + "steampowered-offers.xyz", + "steampowered-swap.xyz", + "steampowered-swap1.xyz", + "steampowered-trades.xyz", + "steampowered.company", + "steampowered.de", + "steampowered.freeskins.ru.com", + "steampowered.help", + "steampowered.irl.com.pk", + "steampowered.jcharante.com", + "steampowered.org", + "steampowered.tw", + "steampowered.us", + "steampowered.xyz", + "steampoweredcinema.com", + "steampoweredcommunity.com", + "steampoweredexchange.xyz", + "steampoweredexchanges.xyz", + "steampoweredkey.com", + "steampoweredmarketing.com", + "steampoweredoffer.xyz", + "steampoweredoffers.xyz", + "steampoweredpoetry.com", + "steampoweredshow.com", + "steampoweredswap.xyz", + "steampoweredtrades.xyz", + "steampowereed.com", + "steampowererd.com", + "steampowerered.com", + "steampowerewd.com", + "steampowerred.com", + "steampowers.com", + "steampowers.org", + "steampowerwd.com", + "steampowerwed.com", + "steampowoereid.com", + "steampowored.com", + "steampowrd.com", + "steampowred.ru", + "steampowwered.com", + "steampowwred.com", + "steamppwrred.com", + "steampromo.net.ru", + "steamproxy.net", + "steampunch-twitch.co", + "steampwered.com", + "steampwoered.com", + "steamrccommunity.com", + "steamrcommuniity.com", + "steamrcommunity.ru", + "steamroll.org.ru", + "steamrolll.net.ru", + "steamrolls.net.ru", + "steamrolls.pp.ru", + "steamrommunily.com", + "steamrommunity.org.ru", + "steamru.org", + "steams-community.ru", + "steams-discord.ru", + "steamscommmunity.com", + "steamscommunitey.com", + "steamscommunity.com", + "steamscommunity.pro", + "steamscommunity.ru", + "steamscommunyti.com", + "steamscommynitu.co", + "steamscomnunity.com", + "steamscomnunyti.com", + "steamsconmunity.com", + "steamsdiscord.com", + "steamservice-deals.xyz", + "steamservice-deals1.xyz", + "steamservicedeals.xyz", + "steamservicedeals1.xyz", + "steamshensu.top", + "steamskincs.ru", + "steamsnitro.ru", + "steamsoftware.info", + "steamsommunity.com", + "steamsommunity.ru", + "steamsomunity.com", + "steamsourcecommunity.xyz", + "steamsourcecommunity1.xyz", + "steamstore.map2.ssl.hwcdn.net", + "steamstore.site", + "steamstorecsgo.com", + "steamstorepowered.com", + "steamstoretrade1.xyz", + "steamstradecommunity.xyz", + "steamsupportpowered.icu", + "steamswap.xyz", + "steamtrade-game.xyz", + "steamtrade-home.xyz", + "steamtrade-store.xyz", + "steamtrade-store1.xyz", + "steamtradecommunity.fun", + "steamtradehome.xyz", + "steamtradeoffeer.com", + "steamtradeoffer.net", + "steamtradeprofile.com", + "steamtrades-home.xyz", + "steamtrades-store.xyz", + "steamtrades.com", + "steamtradeshome.xyz", + "steamtradesofer.com", + "steamtradestore.xyz", + "steamtradestore1.xyz", + "steamunlocked.online", + "steamunlocked.pro", + "steamunpowered.com", + "steamuppowered.com", + "steamuserimages-a.akamaid.net", + "steamwalletbd.com", + "steamwalletcodes.net", + "steamwanmeics.ru", + "steamwcommunity.com", + "steamwcommunity.net", + "steamworkspace.com", + "steamzcommunity.com", + "steanammunuty.ml", + "steancammunity.com", + "steancammunity.ru", + "steancammunlte.com", + "steancammunlty.com", + "steancammunyti.com", + "steanccommunity.ru", + "steancimnunity.ru", + "steancommanty.ru.com", + "steancommeuniliy.ru.com", + "steancomminity.com", + "steancomminity.ru", + "steancomminyty.com", + "steancomminyty.ru.com", + "steancommiuniliy.ru.com", + "steancommiunity.com", + "steancommmunity.com", + "steancommnnity.com", + "steancommnuitty.com", + "steancommnuity.com", + "steancommnulty.com", + "steancommnunity.ru", + "steancommnunitytradeoffer.xyz", + "steancommnunlty.ru", + "steancommounity.com", + "steancommrnity.com", + "steancommueniliy.ru.com", + "steancommuhity.com", + "steancommuhity.ru", + "steancommuineliy.ru.com", + "steancommuiniliy.ru.com", + "steancommuinty.ru", + "steancommuinuty.ru", + "steancommuity.com", + "steancommuity.ru", + "steancommumity.com", + "steancommumity.net", + "steancommumlty.com", + "steancommuncity.ru", + "steancommunety.com", + "steancommunety.ru", + "steancommunify.com", + "steancommuniiity.com", + "steancommuniiliy.ru.com", + "steancommuniit.ru.com", + "steancommuniite-xuz.ru", + "steancommuniite.xyz", + "steancommuniitty.com", + "steancommuniity.com", + "steancommuniity.fun", + "steancommuniity.ru", + "steancommunilly.com", + "steancommunilty.com", + "steancommunilty.ru", + "steancommunily.ru", + "steancommunite.site", + "steancommuniti.com.ru", + "steancommuniti.site", + "steancommunitiy.com.ru", + "steancommunitiy.ru", + "steancommunitry.ru", + "steancommunitty.com", + "steancommunitty.xyz", + "steancommunitv.com", + "steancommunity.cc", + "steancommunity.click", + "steancommunity.host", + "steancommunity.link", + "steancommunity.net.ru", + "steancommunity.pw", + "steancommunity.ru.com", + "steancommunity.ru", + "steancommunitytradeaffer.xyz", + "steancommunlity.ru.com", + "steancommunllty.com", + "steancommunlty.business", + "steancommunlty.com", + "steancommunlty.ru.com", + "steancommunlty.ru", + "steancommunmilty.com", + "steancommunniitly.ru", + "steancommunniity.ru", + "steancommunnilty.ru", + "steancommunnily.ru", + "steancommunnitl.ru", + "steancommunnitlly.ru", + "steancommunnity.co", + "steancommunnity.site", + "steancommunnliity.ru", + "steancommunnlity.ru", + "steancommunnlty.com", + "steancommunnlty.ru", + "steancommunnty.com", + "steancommunnuly.me", + "steancommuntiy.ru.com", + "steancommuntly.com", + "steancommunuity.ru", + "steancommunuty.com", + "steancommunyti.com", + "steancommunyti.ru.com", + "steancommurily.xyz", + "steancommutiny.ru", + "steancommuuity.com", + "steancommuuniliiy.ru.com", + "steancommuuniliy.ru.com", + "steancommuunity.com", + "steancommuvity.com", + "steancommynitu.com", + "steancommynity.org.ru", + "steancommynity.ru.com", + "steancommynuti.ru", + "steancommynyty.ru.com", + "steancomnmunity.ru", + "steancomnnunity.com", + "steancomnnunnity.ru", + "steancomnuilty.ru.com", + "steancomnuity.com", + "steancomnumity.com", + "steancomnumlty.com", + "steancomnumlty.ru", + "steancomnuniiity.ru", + "steancomnuniity.com", + "steancomnunilty.ru", + "steancomnunity.com", + "steancomnunity.ru", + "steancomnunitys.ru", + "steancomnunlty.ru", + "steancomnunnity.xyz", + "steancomnunyti.ru.com", + "steancomnunytu.ru.com", + "steancomnunytu.ru", + "steancomnurity.one", + "steancomnurity.xyz", + "steancomnuuniliy.ru.com", + "steancomrnunitiy.com", + "steancomrnunity.com", + "steancomrnunity.ru", + "steancomrnunuty.ru", + "steancomuniiity.com", + "steancomuniite-xuz.ru", + "steancomuniity.com", + "steancomunite-xuz.ru", + "steancomunitiy.ru.com", + "steancomunitly.ru", + "steancomunity.ru.com", + "steancomunitytradeffer.xyz", + "steancomunnity.ru", + "steancomunnity.tk", + "steancomunnlty.me", + "steancomunnlty.ru.com", + "steancomunyiti.ru", + "steancomunyti.ru.com", + "steancomuunity.com", + "steanconmnuity.com", + "steanconmumity.com", + "steanconmumlty.com", + "steanconmunitiy.co", + "steanconmunitly.ru", + "steanconmunity.ru", + "steanconmunlly.ru", + "steanconmunlty.com", + "steanconmunlty.ru", + "steanconmunuty.ru", + "steanconmunuty.xyz", + "steanconmunyti.ru.com", + "steanconmunyti.ru", + "steanconmynmuti.com", + "steanconnunitly.xyz", + "steanconnunity.com", + "steanconnunlty.com", + "steancoommuniity.xyz", + "steancoommunity.com", + "steancoommunity.xyz", + "steancoommunitytradeofferr.com", + "steancoommunnity.com", + "steancoomnuity.com", + "steancoomnunity.com", + "steancoomunnity.com", + "steancornminuty.com", + "steancornmunuty.ru", + "steancouminnuty.org", + "steanecommunlty.site", + "steanfocuak.ru", + "steanfocusd.xyz", + "steanfocusi.ru", + "steanfocusk.ru", + "steanfocusse.ru", + "steanfocussi.ru", + "steanmcommuniitiy.ru", + "steanmcommunily.ru", + "steanmcommunity.com", + "steanmcommunity.ru.com", + "steanmcommunity.ru", + "steanmcommuniuty.ru.com", + "steanmcommunlty.ru", + "steanmcommunlty.xyz", + "steanmcommzunity.ru", + "steanmcomnuinmty.com", + "steanmcomnuity.com", + "steanmcomnumntiy.com", + "steanmcomnumty.com", + "steanmcomnunitiy.com", + "steanmcomnunity.com", + "steanmcomnynuytiy.org.ru", + "steanmcomrninuty.xyz", + "steanmcomumnity.xyz", + "steanmcomunitly.ru", + "steanmconmunity.com", + "steanmconmunnity.ru", + "steanmconnynuytiy.net.ru", + "steanmconynnuytiy.net.ru", + "steanmconynnuytiy.org.ru", + "steanmecommunity.com", + "steanmncommunity.com", + "steanmncomnunity.com", + "steanncammunlte.com", + "steanncammunlte.ru", + "steanncmmunytiy.ru", + "steanncomminity.ru.com", + "steanncommity.co", + "steanncommiuty.com", + "steanncommnunyti.com", + "steanncommuiniuty.com", + "steanncommunily.com", + "steanncommunitv.com", + "steanncommunity.com", + "steanncommuniuity.com", + "steanncommunlty.com", + "steanncomnmunity.com", + "steanncomnuniity.com", + "steanncomnuniity.online", + "steanncomnuniity.ru", + "steanncomnuniity.xyz", + "steanncomnunity.xyz", + "steanncomunitiy.ru.com", + "steanncomunitli.ru.com", + "steanncomunitly.co", + "steanncomunitly.ru.com", + "steanncomunitly.ru", + "steanncomunitty.site", + "steanncomunity.com", + "steanncomunnity.ru", + "steannconmunity.com", + "steannconnmunity.com", + "steannconnnnunity.net.ru", + "steannconnnunity.com", + "steannconnunynity.ru", + "steannecomunlty.com", + "steanpowered.net.ru", + "steanpowered.xyz", + "steanrcommunitiy.com", + "steapowered.com", + "steappowered.com", + "stearamcomminnity.net", + "stearamcomnunitu.xyz", + "stearcommity.com", + "stearcommuity.com", + "stearcommunitly.com", + "stearmcammunity.com", + "stearmcommnity.com", + "stearmcommnumity.com", + "stearmcommnunity.com", + "stearmcommnunnity.org", + "stearmcommrunity.com", + "stearmcommuniity.com", + "stearmcommuniity.ru.com", + "stearmcommuninty.com", + "stearmcommunitly.ru", + "stearmcommunitry.cf", + "stearmcommunitty.ru.com", + "stearmcommunity.com", + "stearmcommunity.one", + "stearmcommunity.ru.com", + "stearmcommunltly.com", + "stearmcommunnitty.online", + "stearmcommunnity.ru.com", + "stearmcommuunity.ru.com", + "stearmcommuunity.ru", + "stearmcommuunnity.ru", + "stearmcommynity.fun", + "stearmcomrmunity.co", + "stearmcomrmunity.com", + "stearmcomrnunitiy.com", + "stearmcomrnunity.com", + "stearmconmmunity.com", + "stearmconmunity.ru", + "stearmconmunnity.com", + "stearmconnrnunity.com", + "stearmcormmunity.com", + "stearmcornmunitiy.com", + "stearmcornmunity.ru", + "stearmcornmunlty.com", + "stearmcornnnunity.com", + "stearmmcommuniity.ru", + "stearmmcomunitty.ru", + "stearmmcomunity.ru", + "stearmmcomuunity.ru", + "stearncomiunity.ru", + "stearncomminhty.com", + "stearncomminutiu.ru", + "stearncomminuty.click", + "stearncomminuty.com", + "stearncomminuty.link", + "stearncomminuty.ru.com", + "stearncomminuty.ru", + "stearncomminytu.com", + "stearncommiunity.com", + "stearncommiuty.co", + "stearncommmnuity.xyz", + "stearncommmunity.online", + "stearncommmunity.ru", + "stearncommninuty.com", + "stearncommnniity.com", + "stearncommnniity.ru", + "stearncommnnity.co.uk", + "stearncommnnity.com", + "stearncommnuinty.com", + "stearncommnuity.ru.com", + "stearncommnunity.ru.com", + "stearncommonity.ru", + "stearncommrunity.com", + "stearncommubity.com", + "stearncommuinuty.co", + "stearncommumitly.com", + "stearncommumity.com", + "stearncommumlty.com", + "stearncommunety.com", + "stearncommunety.ru", + "stearncommungty.com", + "stearncommunhty.com", + "stearncommunigy.com", + "stearncommuniitty.xyz", + "stearncommuniity.click", + "stearncommuniity.ru", + "stearncommuniity.site", + "stearncommuniityt.click", + "stearncommunilly.site", + "stearncommunilty.ru", + "stearncommunilty.site", + "stearncommunily.ru", + "stearncommunily.website", + "stearncommuninity.com", + "stearncommuniry.com", + "stearncommunite.com", + "stearncommunitey.com", + "stearncommunitey.ru", + "stearncommunitly.ru", + "stearncommunitly.website", + "stearncommunitly.xyz", + "stearncommunity.click", + "stearncommunity.link", + "stearncommunity.net.ru", + "stearncommunity.ru", + "stearncommunivy.com", + "stearncommunjty.com", + "stearncommunlity.com", + "stearncommunlty.ru", + "stearncommunlty.site", + "stearncommunlty.store", + "stearncommunnitty.xyz", + "stearncommunnity.ru", + "stearncommunnity.xyz", + "stearncommunrty.com", + "stearncommuntity.com", + "stearncommuntiy.com", + "stearncommuntty.com", + "stearncommunuitiy.com", + "stearncommunuity.net.ru", + "stearncommunutiy.com", + "stearncommunyti.ru", + "stearncommunytiy.ru", + "stearncommunytiyu.ru", + "stearncommurity.ru", + "stearncommutiny.online", + "stearncommutiny.ru", + "stearncommuty.com", + "stearncommynitu.ru.com", + "stearncommynity.fun", + "stearncommynity.ru.com", + "stearncomnmunity.com", + "stearncomnnunity.fun", + "stearncomnnunity.site", + "stearncomnnunity.website", + "stearncomnnunty.com.ru", + "stearncomnumity.com", + "stearncomnunily.com", + "stearncomnunitu.ru", + "stearncomnunitv.ru.com", + "stearncomnunity.com", + "stearncomnunity.org", + "stearncomnunity.ru.com", + "stearncomnunnity.ru", + "stearncomrmunity.co", + "stearncomrmunity.com", + "stearncomrmynity.fun", + "stearncomrninuty.ru", + "stearncomrninuty.xyz", + "stearncomrnrunity.ru.com", + "stearncomrnrunity.ru", + "stearncomrnunety.com", + "stearncomrnunitly.site", + "stearncomrnunitly.xyz", + "stearncomrnunity.com", + "stearncomrnunity.ru", + "stearncomrnunity.store", + "stearncomrnunlity.ru", + "stearncomrnunlty.site", + "stearncomrnunyti.ru", + "stearncomrrnunity.com", + "stearncomrrunity.com", + "stearncomrunity.ru.com", + "stearncomrunity.ru", + "stearncomunitu.ru", + "stearncomunlty.ru.com", + "stearncomynity.ru", + "stearnconmumity.com", + "stearnconmunity.com", + "stearnconmunity.me", + "stearnconmunity.net", + "stearnconmuntiy.ru", + "stearnconmuuity.com", + "stearnconmuulty.ru", + "stearnconnrnunity.xyz", + "stearnconrmunity.com", + "stearncormmunity.com", + "stearncormmunity.ru", + "stearncormunity.ru", + "stearncormunniti.org", + "stearncornminuty.com", + "stearncornminuty.ru", + "stearncornmnuity.ru", + "stearncornmrunity.ru.com", + "stearncornmunitiy.com", + "stearncornmunitly.com", + "stearncornmunity.com", + "stearncornmunity.net", + "stearncornmunity.ru.com", + "stearncornmunity.ru", + "stearncornmunlty.ru", + "stearncornmunuty.ru", + "stearncornmurnity.ru.com", + "stearncornnumyty.com", + "stearncornnunity.ru", + "stearncornrnnity.ru.com", + "stearncornrnuity.com", + "stearncornrnunity.com", + "stearncornrnunity.ru.com", + "stearncornunity.ru", + "stearncornunity.xyz", + "stearncornurniity.xyz", + "stearncorrmunity.com", + "stearncurnmunity.com", + "stearnmcommunnity.com", + "stearnmcomunity.com", + "stearnncomrnunitiy.com", + "stearnncomrnunity.com", + "stearnporewed.ru.com", + "stearnpovvered.com", + "stearnpowered.online", + "stearnpowered.xyz", + "steasmpowered.com", + "steawcammunity.xyz", + "steawcommunity.com", + "steawcommunity.net", + "steawcomunity.net", + "steawconnunity.xyz", + "steawmcommunity.net", + "steawmcomnunnity.ru", + "steawmcomuunity.ru", + "steawmcowmunnity.ru", + "steawmpowered.com", + "steawncomnunity.ru", + "steawpowered.com", + "steawscommunity.net", + "steaxmcommity.com", + "steeaamcomunity.xyz", + "steeacmcommumitiy.com", + "steeamcommmunety.com", + "steeamcommmunitty.site", + "steeamcommmunity.com", + "steeamcommuinitty.com", + "steeamcommunity.me", + "steeamcommunity.ml", + "steeamcommunity.ru.com", + "steeamcommunlity.com", + "steeamcommunlity.ru", + "steeamcommunllty.xyz", + "steeamcommunlty.com", + "steeamcommunnity.ru.com", + "steeamcommunnity.ru", + "steeamcommunnlty.ru", + "steeamcommunnuity.ru.com", + "steeamcommunyti.com", + "steeamcomnnunity.com", + "steeamcomuneety.com", + "steeamcomunitty.com", + "steeamcomunity.net", + "steeamcomunlty.ru.com", + "steeamcomunlty.ru", + "steeamcomunnlty.com", + "steeamcoommunity.ru", + "steeammcomunity.com", + "steeammcomunlty.com", + "steeampowered.tk", + "steeamwins.xyz", + "steemacommunity.com", + "steemcammunllty.com", + "steemcammunlly.com", + "steemcammunlty.com", + "steemcommmunety.com", + "steemcommmunity.com", + "steemcommnnity.com", + "steemcommnunity.ru", + "steemcommnunnity.ru.com", + "steemcommuinty.com", + "steemcommuniity.com", + "steemcommunily.ru.com", + "steemcommuninity.org.ru", + "steemcommuniry.com", + "steemcommunitey.com", + "steemcommuniti.com", + "steemcommunitry.com", + "steemcommunity.co", + "steemcommunity.com", + "steemcommunity.ru.com", + "steemcommunityy.com", + "steemcommuniy.com", + "steemcommunllty.com", + "steemcommunlty.com", + "steemcommunly.com", + "steemcommunnity.co", + "steemcommunnity.net", + "steemcommuntiy.ru.com", + "steemcommuntiy.ru", + "steemcommunty.net.ru", + "steemcommunty.org.ru", + "steemcommunty.pp.ru", + "steemcommunty.ru", + "steemcommuunity.com", + "steemcommynity.ru", + "steemcomnmunity.com", + "steemcomnrunity.com", + "steemcomrnunity.co", + "steemcomrnunity.com", + "steemcomrunity.ru", + "steemcomunatlytradeoffer40034231.ru", + "steemcomuniti.com", + "steemcomuniti.ru", + "steemcomunity.me", + "steemcomunity.net.ru", + "steemcomunity.org.ru", + "steemcomunity.pp.ru", + "steemcomunnity.com", + "steemconnunity.com", + "steemcoommunity.com", + "steemcoommunity.ru", + "steemcoommunlty.ru", + "steemcoommuntiy.ru", + "steemcoommunty.ru", + "steemcoomnunty.ru", + "steemcoomunity.xyz", + "steemcoomuntiy.ru", + "steemcoomuunity.ru", + "steemcoonmuntiy.ru", + "steemcowwunity.xyz", + "steempowerd.ru", + "steempowered.com", + "steemurl.com", + "steencommunilty.com", + "steencommunityy.xyz", + "steiamcommuinity.com", + "steiamcommunityi.com", + "steimcomnunnity.ru.com", + "stemacommunity.net", + "stemacommunlty.com", + "stemacomunity.com", + "stemapowered.com", + "stemcammuniety.ru", + "stemcammuniity.com", + "stemcammuniity.ru", + "stemcamnunity.com", + "stemcamnunity.ru", + "stemccomnmunity.com", + "stemcomiunity.ru", + "stemcomminity.com", + "stemcomminuty.ru", + "stemcommlunity.com", + "stemcommnuity.ru.com", + "stemcommnunity.com", + "stemcommnunity.ru.com", + "stemcommnunlty.ru", + "stemcommnunnity.com", + "stemcommnunulty.com", + "stemcommnuunity.com", + "stemcommouniity.com", + "stemcommounilty.com", + "stemcommounity.ru.com", + "stemcommuinty.ru", + "stemcommuniby.com", + "stemcommuniety.com", + "stemcommuniity.com", + "stemcommuniity.ru", + "stemcommunilty.com", + "stemcommunilty.ru", + "stemcommunite.pp.ru", + "stemcommuniti.ru", + "stemcommunitiy.com", + "stemcommunitly.com", + "stemcommunitty.com", + "stemcommunitty.ru.com", + "stemcommunity.com.ru", + "stemcommunity.ru.com", + "stemcommunity.ru", + "stemcommunitytraade.xyz", + "stemcommunitytrade.com", + "stemcommunitytrade.fun", + "stemcommunjty.com", + "stemcommunlitly.com", + "stemcommunlity.ru", + "stemcommunlty.com", + "stemcommunlty.ru.com", + "stemcommunlty.space", + "stemcommunniity.com", + "stemcommunnilty.com", + "stemcommunnitiy.net.ru", + "stemcommunnity.com.ru", + "stemcommunnity.com", + "stemcommunuity.com", + "stemcommununity.com", + "stemcommuty.ru", + "stemcommuunity.com.ru", + "stemcommynity.ru.com", + "stemcommyunity.ru", + "stemcomnmnnunity.com", + "stemcomnmnunity.com", + "stemcomnmounity.com", + "stemcomnmuity.com", + "stemcomnmuniity.com", + "stemcomnmuniity.ru.com", + "stemcomnmunity.com.ru", + "stemcomnmunity.ru.com", + "stemcomnmunity.ru", + "stemcomnmunniity.com", + "stemcomnmunnity.com", + "stemcomnmunuity.com", + "stemcomnmununity.com", + "stemcomnmuunity.com", + "stemcomnmuunity.ru.com", + "stemcomnnmunity.com", + "stemcomnnmunnity.com", + "stemcomnnmuunity.ru", + "stemcomnuniti.ru", + "stemcomnunity.com", + "stemcomnunity.ru.com", + "stemcomnunity.ru", + "stemcomnunyti.ru.com", + "stemcomrnmunity.com", + "stemcomrnuniity.ru", + "stemcomuniti.ru", + "stemcomunitiy.com", + "stemcomunity.com", + "stemcomunity.net", + "stemcomunity.ru.com", + "stemcomunnity.com.ru", + "stemcomunnity.com", + "stemcomunnity.ru.com", + "stemconmmnunity.com", + "stemconmmunity.com", + "stemconmmunnity.com", + "stemconmmuunnity.com", + "stemconmnmuunity.com", + "stemconmuite.xyz", + "stemconmumity.ru", + "stemcoominuty-alirdrop.xyz", + "stemcoommounity.com", + "stemcoommuniity.com", + "stemcoommunity.com", + "stemcoommuunnity.com", + "stemcoomnmnunity.com", + "stemcoomnmounity.com", + "stemcoomnmuniity.com", + "stemcoomnmunity.com", + "stemcoomnmunity.ru.com", + "stemcoomnmunnity.com", + "stemcoomnnunity.com", + "stemcormmunity.com", + "stemcormmunlty.ru.com", + "stemcornmunitly.ru.com", + "stemcornmunity.com", + "stemcornmunity.ru.com", + "stemcornmunity.ru", + "stemcornmunlty.xyz", + "stemcummnuity.ru.com", + "stemcummnunity.ru.com", + "stemcummunity.com.ru", + "stemcummunity.ru.com", + "stemcummunnity.com.ru", + "stemcummunnity.ru.com", + "stemcumnmunity.com.ru", + "stemcumnmunity.com", + "stemcumnmunity.ru.com", + "stemcumunnity.ru.com", + "stemecommunlty.com", + "stemmcomunity.xyz", + "stemmcomunnityy.xyz", + "stemncornmunity.com", + "stemsell.ml", + "stencommunity.com", + "stenmcommunilty.ru.com", + "stenmcommunitly.ru.com", + "stenncornmuniy.com", + "stennicommuitun.com", + "steomcommunitey.com", + "steomcommunito.con", + "steomcommunity.com", + "steomcommunity.ru", + "steomcommunlty.ml", + "steomcomnunity.ru.com", + "steomconmunity.com", + "steomcoommynity.ru.com", + "stepmscononnity.com", + "steqmcommunity.com", + "steqmpowered.com", + "steramconmunity.com", + "sterampowered.com", + "stermccommunitty.ru", + "stermcommuniity.com", + "stermcommunilty.ru.com", + "stermcommunity.com", + "stermcommunity.ru.com", + "stermcommunityy.ru", + "stermcommunlity.ru.com", + "stermcommunnitty.ru", + "stermcomunitte.xyz", + "stermcomunniity.ru", + "stermconmmunity.com", + "stermmcomuniity.ru", + "stermncommunity.com", + "sterncommunilty.ru.com", + "sterncommunilty.site", + "sterncommunnity.ru", + "sterncommynuty.ru", + "sterncomnurity.one", + "sternconmunity.ru", + "sterncornmunity.ru", + "sternmcommunity.com", + "sternmconmunity.com", + "sternmcornmmunity.com", + "sternmcornnunity.com", + "sterumcommunity.com", + "stetrncommity.com", + "steumcommunity.com", + "steumcommunity.ru", + "steumcornmunity.com", + "steurmcommunity.com", + "steurmconmunity.com", + "stewie2k-giveaway-150days.pro", + "stewmpowered.com", + "stfriendprofile.ru", + "stg.steamcpowered.com", + "stheamcommnitiy.ru", + "stheamcommuniti.com", + "stheamcommunity.ru", + "stheamcommunutiy.ru", + "stheamcommunutly.ru", + "stheamcomunitly.ru", + "stheamcomunutly.ru", + "stheamconmuniity.com", + "stheamconnmunutly.ru", + "stheamcornmunitiy.ru", + "stiamcammunieti.com", + "stiamcommunitly.xyz", + "stiamcommunity.com", + "stiamcommyunlty.ru.com", + "stiamcomunity.xyz", + "stiamcomunlty.ru", + "stiamcomynity.com", + "stieamcommuinity.com", + "stieamcommuniity.com", + "stieamcommuniity.ru", + "stieamcommunitey.ru", + "stieamcommunitiy.com", + "stieamcommunity.com", + "stieamcommunity.org.ru", + "stieamcommunity.pp.ru", + "stieamcommuunitey.us", + "stieamcommynituy.com", + "stieamcomnnunity.com", + "stieamcomuniiti.ru", + "stieamcomunity.com", + "stieamconmuniity.com", + "stieamconnmunity.com", + "stieamcormnynity.ru.com", + "stiemcommunitty.ru", + "stiemconnumity.xyz", + "stimcommunity.ru", + "stimcommunlty.ru", + "stimiache.ru", + "stjeamcoimmunity.com", + "stjeamcommunity.ru", + "stjeamcomnuminiti.ru", + "stjeamcomnunitiy.ru", + "stjeamcomnunity.ru", + "stjeamcomuniity.ru", + "stjeamconmunnitii.com", + "stleaamcommunity.com", + "stleam-communithy.com", + "stleamcommiunity.ru.com", + "stleamcommiynitu.ru", + "stleamcommiynitu.xyz", + "stleamcommiynity.xyz", + "stleamcommnunity.ru", + "stleamcommulnity.xyz", + "stleamcommulnitycom.xyz", + "stleamcommuneety.com", + "stleamcommuniity.com", + "stleamcommuniity.net", + "stleamcommunilty.com", + "stleamcommunithy.com", + "stleamcommunitiy.com", + "stleamcommunitly.com", + "stleamcommunitty.com", + "stleamcommunity.com", + "stleamcommunity.net", + "stleamcommunlty.com", + "stleamcommunlty.xyz", + "stleamcomnmunity.ru.com", + "stleamcomnunity.ru.com", + "stleamcomunity.com", + "stleamconminity.online", + "stleamconminity.ru", + "stleamconmmunity.ru.com", + "stleamconmmunlty.net.ru", + "stleamconmunity.com", + "stleamconnunlty-tyztradeoffernewpartnhr15902271.xyz", + "stleamcormmunity.ru.com", + "stleamcormmynity.ru.com", + "stleamcormunity.ru.com", + "stleamcornmmunity.ru.com", + "stleammcomnnunitycom.buzz", + "stleamncommunity.ru", + "stleancommunity.ru", + "stleanmcommunity.ru", + "stleaomcoommynity.ru.com", + "stlemamcornmunty.me", + "stmawards.xyz", + "stmcornnunnitty.xyz", + "stmcornumnunitty.xyz", + "stmeacomunnitty.ru", + "stmemcomyunity.com", + "stmencommunity.ru", + "stmtrdoffer.xyz", + "stoacommunity.codes", + "stoemcommunity.com", + "stopify.com", + "store-communitiy.com", + "store-discord.com", + "store-steam-csgo.ru", + "store-steamcomminuty.ru.com", + "store-steamcommunity.xyz", + "store-steamcomnunity", + "store-steampoweered.ru", + "store-steampowereb.com", + "store-steampowered.ru", + "store-stempowered.com", + "store-streampowered.me", + "store.stampowered.com", + "store.stempowerd.com", + "storeesteampowered.ru.com", + "storeesteampowereed.ru.com", + "stores-steampowered.com", + "storesleampowecommunity.store", + "storesteam-csgo.ru", + "straemcommonlity.com", + "straemcomunnitry.ru", + "straemcummonilty.com", + "straemcummonity.com", + "stramconmunity.com", + "strcomnunnitly.xyz", + "streaalcommuunnitu.ru", + "streaemcrommunlty.com.ru", + "stream-conmunlty.ru", + "streamc0mmunnlty.xyz", + "streamcammunitly.com", + "streamccomunilty.com", + "streamcolmnty.xyz", + "streamcomlutitly.me", + "streamcomminuty.pw", + "streamcomminuty.ru.com", + "streamcommiumity.com", + "streamcommiunity.com", + "streamcommiunnity.com", + "streamcommlunity.ru.com", + "streamcommmumnity.ru.com", + "streamcommmunify.ru.com", + "streamcommmunitty.ru.com", + "streamcommmunity.com", + "streamcommmunjty.ru.com", + "streamcommmunlty.ru.com", + "streamcommmunnlty.ru.com", + "streamcommnnity.com", + "streamcommnnuity.com", + "streamcommnnutiy.com", + "streamcommnuity.com", + "streamcommnuity.ru", + "streamcommnunilty.com", + "streamcommnunitly.com", + "streamcommnunity.ru", + "streamcommnunlity.ru", + "streamcommnunnity.ml", + "streamcommnunuty.ru.com", + "streamcommnunuty.ru", + "streamcommonlty.ru.com", + "streamcommounity.com", + "streamcommuinity.com", + "streamcommuinty.com", + "streamcommuiny.ru", + "streamcommulinty.com", + "streamcommulnty.com", + "streamcommumity.ru.com", + "streamcommumninty.com", + "streamcommumnity.com", + "streamcommumtiy.ru", + "streamcommunaly.com", + "streamcommunaty.com", + "streamcommuneiley.net", + "streamcommunetly.com", + "streamcommunety.ru", + "streamcommunicate.ru", + "streamcommunication.com", + "streamcommunify.com", + "streamcommuniiley.net.ru", + "streamcommuniiley.net", + "streamcommuniily.com", + "streamcommuniitty.com", + "streamcommuniitu.com", + "streamcommuniity.org", + "streamcommuniity.ru.com", + "streamcommuniity.ru", + "streamcommuniityy.me", + "streamcommuniley.net.ru", + "streamcommuniley.net", + "streamcommuniliey.net.ru", + "streamcommuniliey.xyz", + "streamcommuniliiey.net.ru", + "streamcommuniliiey.org.ru", + "streamcommuniliiey.pp.ru", + "streamcommuniliiy.org.ru", + "streamcommuniliiy.pp.ru", + "streamcommunillty.com", + "streamcommunilly.com", + "streamcommunilty.com", + "streamcommunilty.xyz", + "streamcommunily.cc", + "streamcommunily.co", + "streamcommunily.com", + "streamcommunily.icu", + "streamcommunily.me", + "streamcommunily.net", + "streamcommunily.ru.com", + "streamcommunimty.com", + "streamcommuninllty.com", + "streamcommuninnity.com", + "streamcommuninnuity.com", + "streamcommuninty.com", + "streamcommuninty.me", + "streamcommuninuty.store", + "streamcommunit.com", + "streamcommunit.ru.com", + "streamcommunite.com", + "streamcommunite.ru.com", + "streamcommunitey.com", + "streamcommuniti.ru", + "streamcommuniti.xyz", + "streamcommunitily.com", + "streamcommunitiy.com", + "streamcommunitiy.net", + "streamcommunitiy.ru.com", + "streamcommunitiy.ru", + "streamcommunitly.net", + "streamcommunitly.ru", + "streamcommunitly.xyz", + "streamcommunitry.ru", + "streamcommunitty.ru.com", + "streamcommunitu.com", + "streamcommunitv.me", + "streamcommunitv.net", + "streamcommunity-user.me", + "streamcommunity.com.ru", + "streamcommunity.me", + "streamcommunity.net.ru", + "streamcommunity.one", + "streamcommunity.org.ru", + "streamcommunity.pl", + "streamcommunity.ru.com", + "streamcommunityi.ru", + "streamcommunityy.me", + "streamcommuniunity.com", + "streamcommuniuty.ru.com", + "streamcommuniuty.store", + "streamcommuniy.ru", + "streamcommunjty.com", + "streamcommunjty.ru.com", + "streamcommunlity.ru", + "streamcommunliy.com", + "streamcommunlte.ru", + "streamcommunltiy.com", + "streamcommunlty.net", + "streamcommunly.com", + "streamcommunly.me", + "streamcommunly.net", + "streamcommunly.ru", + "streamcommunminty.com", + "streamcommunmity.com", + "streamcommunniity.com", + "streamcommunnilty.com", + "streamcommunnitty.com", + "streamcommunnity.org", + "streamcommunnty.com", + "streamcommunnty.me", + "streamcommunnuitty.com", + "streamcommuntiiy.org", + "streamcommuntiy.com", + "streamcommuntly.com", + "streamcommuntly.net.ru", + "streamcommuntly.org.ru", + "streamcommuntly.pp.ru", + "streamcommunttly.com", + "streamcommunty.co", + "streamcommunty.me", + "streamcommunty.ru", + "streamcommunuitty.com", + "streamcommunuity.net", + "streamcommununty.com", + "streamcommuny.ru", + "streamcommunyty.com", + "streamcommutiny.net", + "streamcommuuniity.com", + "streamcommuunilty.ru.com", + "streamcommuunity.com", + "streamcommuunniity.com", + "streamcommuunnity.com", + "streamcommuunnity.net", + "streamcommuuty.ru", + "streamcommynitu.com", + "streamcommynuty.com", + "streamcomninuty.xyz", + "streamcomnmunity.ru.com", + "streamcomnmunnity.ru.com", + "streamcomnnunity.net", + "streamcomnnunity.website", + "streamcomnnunity.xyz", + "streamcomnnunlty.com", + "streamcomnnunuty.com", + "streamcomnully.net.ru", + "streamcomnully.org.ru", + "streamcomnullyty.net.ru", + "streamcomnullyty.org.ru", + "streamcomnullyty.pp.ru", + "streamcomnultyy.net.ru", + "streamcomnultyy.org.ru", + "streamcomnumity.ru", + "streamcomnumnity.ru.com", + "streamcomnunely.com", + "streamcomnunetiy.com", + "streamcomnuniity.com", + "streamcomnuniity.net", + "streamcomnunitiy.ru", + "streamcomnunitly.ru", + "streamcomnunitry.ru", + "streamcomnunitty.com", + "streamcomnunity.ru", + "streamcomnunity.site", + "streamcomnuniuty.com", + "streamcomnunlity.com", + "streamcomnunlty.ru", + "streamcomnunnity.ru", + "streamcomnunuty.com", + "streamcomnunuty.ru", + "streamcomnunyti.xyz", + "streamcomrnunitiy.ru", + "streamcomrnunity.com", + "streamcomrnunity.online", + "streamcomrnunity.ru", + "streamcomulty.net.ru", + "streamcomulty.org.ru", + "streamcomuniitty.ru.com", + "streamcomuniity.cf", + "streamcomuniity.com", + "streamcomuniity.net", + "streamcomuniity.pp.ua", + "streamcomunilty.net.ru", + "streamcomunilty.org.ru", + "streamcomunily.net.ru", + "streamcomunily.org.ru", + "streamcomunily.pp.ru", + "streamcomunitly.com", + "streamcomunitly.net.ru", + "streamcomunitly.net", + "streamcomunitly.ru", + "streamcomunitry.com", + "streamcomunitty.net", + "streamcomunitu.ru", + "streamcomunity.com", + "streamcomunity.fun", + "streamcomunity.net", + "streamcomunity.org", + "streamcomunity.ru.com", + "streamcomunlty.net.ru", + "streamcomunlty.org.ru", + "streamcomunlty.pp.ru", + "streamcomunltyy.org.ru", + "streamcomunltyy.pp.ru", + "streamcomunniity.net.ru", + "streamcomunnity.pp.ua", + "streamcomunnity.ru.com", + "streamcomunnity.xyz", + "streamcomuuniltyy.org.ru", + "streamcomuuniltyy.pp.ru", + "streamcomuunltyy.net.ru", + "streamcomuunltyy.org.ru", + "streamcomuunltyy.pp.ru", + "streamcomynity.com", + "streamcomynity.ru.com", + "streamconmmunity.com", + "streamconmmunity.ru.com", + "streamconmumuty.xyz", + "streamconmunilty.com", + "streamconmunitly.com", + "streamconmunitly.ru", + "streamconmunity.com", + "streamconmunlity.com", + "streamconmunlty.ru", + "streamconmunyti.com", + "streamconnmunity.com", + "streamconnuity.com", + "streamconnumity.com", + "streamconnunitly.com", + "streamconnunity.net.ru", + "streamconnunity.ru", + "streamconnunity.site", + "streamconnunity.us", + "streamconunity.net.ru", + "streamcoommounity.com", + "streamcoommuniity.xyz", + "streamcoommunity.com", + "streamcoommunity.net", + "streamcoommunity.xyz", + "streamcormmunity.com", + "streamcormmunity.ru.com", + "streamcormmunlty.ru.com", + "streamcormmunnity.ru.com", + "streamcormmyniity.ru.com", + "streamcormnmunity.ru.com", + "streamcormunnity.ru.com", + "streamcornnunitly.co", + "streamcornnunitly.com", + "streamcoumunniity.org", + "streamcoumunnity.org", + "streamcrommunify.me", + "streamcummonity.ru.com", + "streamcummunity.ru.com", + "streamcummunlty.com", + "streamcummunlty.xyz", + "streamecommuniity.com", + "streamecommunity.com", + "streammcommunity.ru", + "streammcomunittty.ru", + "streammcomunity.com", + "streammcomunnity.ru", + "streammcomuunity.ru", + "streammcornmunnity.com", + "streamncommnunity.com", + "streamnconmumity.com", + "streamnconmunity.com", + "streamnconmunity.ru", + "streampoered.com", + "streampowered.store", + "streampowereed.com", + "streancommumity.ru.com", + "streancommuniity.ru.com", + "streancommuniliy.ru.com", + "streancommuniliy.ru", + "streancommunitiy.co", + "streancommunitiy.net.ru", + "streancommunitiy.ru", + "streancommunity.ru.com", + "streancommunuty.ru", + "streancomunnitiy.com", + "streancomunnuty.com", + "streancoommunity.com", + "streancoommunity.xyz", + "streanncomminity.ru", + "streanncommunity.space", + "streanncomnnunuty.com", + "streanncomunity.ru", + "strearmcommunity.ru", + "strearmcomunity.ru", + "strearncomuniity.ru.com", + "streawcommunity.xyz", + "streeamcommunuti.ru", + "streemcommunhity.org.ru", + "streemcommunitiy.ru.com", + "strempowered.com", + "streomcommunuty.com", + "strieamcommunniity.com", + "striieamcomnmunniitty.ru", + "stteamcommiunity.com", + "stteamcommunitty.com", + "stteamcommunity.net", + "sttemcomnmuty.ru.com", + "stuamcommnuity.com", + "stuamcommunity.com", + "stuemconmunity.com", + "sturemconmunity.com", + "stwsmarket.ru", + "styamcommunity.com", + "styeampowerd.com", + "styeampowered.com", + "stzeamcomnumiti.ru", + "sueamcommunity.com", + "sueamconmunity.com", + "sufficienttime.rocks", + "summer-rust.xyz", + "sunnygamble.com", + "superbalancednow.com", + "superdealgadgets.com", + "support.verifiedbadgehelp-form.ml", + "supremeskins.cf", + "surveysandpromoonline.com", + "swapskins.ga", + "swapskins.live", + "swapslot.tk", + "sweet-fortune.ru", + "ta-sty.info", + "taceitt.com", + "tacelt.com", + "tacticalusa.com", + "takeit100.xyz", + "takeit101.xyz", + "takeit102.xyz", + "takeit103.xyz", + "takeit104.xyz", + "takeit105.xyz", + "takeit106.xyz", + "takeit107.xyz", + "takeit108.xyz", + "takeit109.xyz", + "takeit110.xyz", + "takeit111.xyz", + "takeit112.xyz", + "takeit113.xyz", + "takeit114.xyz", + "takeit115.xyz", + "takeit116.xyz", + "takeit117.xyz", + "takeit118.xyz", + "takeit119.xyz", + "takeit120.xyz", + "takeit121.xyz", + "takeit122.xyz", + "takeit123.xyz", + "takeit124.xyz", + "takeit125.xyz", + "takeit126.xyz", + "takeit127.xyz", + "takeit128.xyz", + "takeit129.xyz", + "takeit130.xyz", + "takeit131.xyz", + "takeit132.xyz", + "takeit133.xyz", + "takeit134.xyz", + "takeit135.xyz", + "takeit136.xyz", + "takeit137.xyz", + "takeit138.xyz", + "takeit139.xyz", + "takeit140.xyz", + "takeit141.xyz", + "takeit142.xyz", + "takeit143.xyz", + "takeit144.xyz", + "takeit145.xyz", + "takeit146.xyz", + "takeit147.xyz", + "takeit148.xyz", + "takeit149.xyz", + "takeit150.xyz", + "takeit151.xyz", + "takeit152.xyz", + "takeit153.xyz", + "takeit154.xyz", + "takeit155.xyz", + "takeit156.xyz", + "takeit157.xyz", + "takeit158.xyz", + "takeit159.xyz", + "takeit160.xyz", + "takeit161.xyz", + "takeit162.xyz", + "takeit163.xyz", + "takeit164.xyz", + "takeit165.xyz", + "takeit166.xyz", + "takeit167.xyz", + "takeit168.xyz", + "takeit169.xyz", + "takeit170.xyz", + "takeit171.xyz", + "takeit172.xyz", + "takeit173.xyz", + "takeit174.xyz", + "takeit175.xyz", + "takeit176.xyz", + "takeit177.xyz", + "takeit178.xyz", + "takeit179.xyz", + "takeit20.xyz", + "takeit21.xyz", + "takeit22.xyz", + "takeit23.xyz", + "takeit24.xyz", + "takeit25.xyz", + "takeit26.xyz", + "takeit260.xyz", + "takeit261.xyz", + "takeit262.xyz", + "takeit263.xyz", + "takeit264.xyz", + "takeit265.xyz", + "takeit266.xyz", + "takeit267.xyz", + "takeit268.xyz", + "takeit269.xyz", + "takeit27.xyz", + "takeit270.xyz", + "takeit271.xyz", + "takeit272.xyz", + "takeit273.xyz", + "takeit274.xyz", + "takeit275.xyz", + "takeit276.xyz", + "takeit277.xyz", + "takeit278.xyz", + "takeit279.xyz", + "takeit28.xyz", + "takeit280.xyz", + "takeit281.xyz", + "takeit282.xyz", + "takeit283.xyz", + "takeit284.xyz", + "takeit285.xyz", + "takeit286.xyz", + "takeit287.xyz", + "takeit288.xyz", + "takeit289.xyz", + "takeit29.xyz", + "takeit290.xyz", + "takeit291.xyz", + "takeit292.xyz", + "takeit293.xyz", + "takeit294.xyz", + "takeit295.xyz", + "takeit296.xyz", + "takeit297.xyz", + "takeit298.xyz", + "takeit299.xyz", + "takeit30.xyz", + "takeit300.xyz", + "takeit301.xyz", + "takeit302.xyz", + "takeit303.xyz", + "takeit304.xyz", + "takeit305.xyz", + "takeit306.xyz", + "takeit307.xyz", + "takeit308.xyz", + "takeit309.xyz", + "takeit31.xyz", + "takeit310.xyz", + "takeit311.xyz", + "takeit312.xyz", + "takeit313.xyz", + "takeit314.xyz", + "takeit315.xyz", + "takeit316.xyz", + "takeit317.xyz", + "takeit318.xyz", + "takeit319.xyz", + "takeit32.xyz", + "takeit321.xyz", + "takeit322.xyz", + "takeit323.xyz", + "takeit324.xyz", + "takeit325.xyz", + "takeit326.xyz", + "takeit327.xyz", + "takeit328.xyz", + "takeit329.xyz", + "takeit33.xyz", + "takeit330.xyz", + "takeit331.xyz", + "takeit332.xyz", + "takeit333.xyz", + "takeit334.xyz", + "takeit335.xyz", + "takeit336.xyz", + "takeit337.xyz", + "takeit338.xyz", + "takeit339.xyz", + "takeit34.xyz", + "takeit340.xyz", + "takeit341.xyz", + "takeit342.xyz", + "takeit343.xyz", + "takeit344.xyz", + "takeit345.xyz", + "takeit346.xyz", + "takeit347.xyz", + "takeit348.xyz", + "takeit349.xyz", + "takeit35.xyz", + "takeit350.xyz", + "takeit351.xyz", + "takeit352.xyz", + "takeit353.xyz", + "takeit354.xyz", + "takeit355.xyz", + "takeit356.xyz", + "takeit357.xyz", + "takeit358.xyz", + "takeit359.xyz", + "takeit36.xyz", + "takeit360.xyz", + "takeit361.xyz", + "takeit362.xyz", + "takeit363.xyz", + "takeit364.xyz", + "takeit365.xyz", + "takeit366.xyz", + "takeit367.xyz", + "takeit368.xyz", + "takeit369.xyz", + "takeit37.xyz", + "takeit370.xyz", + "takeit371.xyz", + "takeit372.xyz", + "takeit373.xyz", + "takeit374.xyz", + "takeit375.xyz", + "takeit376.xyz", + "takeit377.xyz", + "takeit378.xyz", + "takeit379.xyz", + "takeit38.xyz", + "takeit380.xyz", + "takeit381.xyz", + "takeit382.xyz", + "takeit383.xyz", + "takeit384.xyz", + "takeit385.xyz", + "takeit386.xyz", + "takeit388.xyz", + "takeit389.xyz", + "takeit39.xyz", + "takeit390.xyz", + "takeit391.xyz", + "takeit392.xyz", + "takeit393.xyz", + "takeit394.xyz", + "takeit395.xyz", + "takeit396.xyz", + "takeit397.xyz", + "takeit398.xyz", + "takeit399.xyz", + "takeit40.xyz", + "takeit400.xyz", + "takeit401.xyz", + "takeit402.xyz", + "takeit403.xyz", + "takeit404.xyz", + "takeit405.xyz", + "takeit406.xyz", + "takeit407.xyz", + "takeit408.xyz", + "takeit409.xyz", + "takeit41.xyz", + "takeit410.xyz", + "takeit411.xyz", + "takeit412.xyz", + "takeit413.xyz", + "takeit414.xyz", + "takeit415.xyz", + "takeit416.xyz", + "takeit417.xyz", + "takeit418.xyz", + "takeit419.xyz", + "takeit42.xyz", + "takeit420.xyz", + "takeit422.xyz", + "takeit423.xyz", + "takeit424.xyz", + "takeit425.xyz", + "takeit426.xyz", + "takeit427.xyz", + "takeit428.xyz", + "takeit429.xyz", + "takeit43.xyz", + "takeit430.xyz", + "takeit431.xyz", + "takeit432.xyz", + "takeit433.xyz", + "takeit434.xyz", + "takeit435.xyz", + "takeit436.xyz", + "takeit437.xyz", + "takeit438.xyz", + "takeit439.xyz", + "takeit44.xyz", + "takeit440.xyz", + "takeit441.xyz", + "takeit442.xyz", + "takeit443.xyz", + "takeit444.xyz", + "takeit445.xyz", + "takeit446.xyz", + "takeit447.xyz", + "takeit448.xyz", + "takeit449.xyz", + "takeit45.xyz", + "takeit450.xyz", + "takeit451.xyz", + "takeit452.xyz", + "takeit453.xyz", + "takeit454.xyz", + "takeit455.xyz", + "takeit456.xyz", + "takeit457.xyz", + "takeit458.xyz", + "takeit459.xyz", + "takeit46.xyz", + "takeit460.xyz", + "takeit461.xyz", + "takeit462.xyz", + "takeit463.xyz", + "takeit464.xyz", + "takeit465.xyz", + "takeit466.xyz", + "takeit467.xyz", + "takeit468.xyz", + "takeit469.xyz", + "takeit47.xyz", + "takeit470.xyz", + "takeit471.xyz", + "takeit472.xyz", + "takeit473.xyz", + "takeit474.xyz", + "takeit475.xyz", + "takeit476.xyz", + "takeit477.xyz", + "takeit478.xyz", + "takeit479.xyz", + "takeit48.xyz", + "takeit480.xyz", + "takeit481.xyz", + "takeit482.xyz", + "takeit483.xyz", + "takeit484.xyz", + "takeit485.xyz", + "takeit486.xyz", + "takeit487.xyz", + "takeit488.xyz", + "takeit489.xyz", + "takeit49.xyz", + "takeit490.xyz", + "takeit491.xyz", + "takeit492.xyz", + "takeit493.xyz", + "takeit494.xyz", + "takeit495.xyz", + "takeit496.xyz", + "takeit497.xyz", + "takeit498.xyz", + "takeit499.xyz", + "takeit50.xyz", + "takeit500.xyz", + "takeit501.xyz", + "takeit502.xyz", + "takeit503.xyz", + "takeit504.xyz", + "takeit505.xyz", + "takeit506.xyz", + "takeit507.xyz", + "takeit508.xyz", + "takeit509.xyz", + "takeit51.xyz", + "takeit510.xyz", + "takeit511.xyz", + "takeit512.xyz", + "takeit513.xyz", + "takeit514.xyz", + "takeit515.xyz", + "takeit516.xyz", + "takeit517.xyz", + "takeit518.xyz", + "takeit519.xyz", + "takeit520.xyz", + "takeit521.xyz", + "takeit522.xyz", + "takeit523.xyz", + "takeit524.xyz", + "takeit525.xyz", + "takeit526.xyz", + "takeit527.xyz", + "takeit528.xyz", + "takeit529.xyz", + "takeit53.xyz", + "takeit530.xyz", + "takeit531.xyz", + "takeit533.xyz", + "takeit534.xyz", + "takeit535.xyz", + "takeit536.xyz", + "takeit537.xyz", + "takeit538.xyz", + "takeit539.xyz", + "takeit54.xyz", + "takeit540.xyz", + "takeit541.xyz", + "takeit542.xyz", + "takeit543.xyz", + "takeit544.xyz", + "takeit545.xyz", + "takeit546.xyz", + "takeit547.xyz", + "takeit548.xyz", + "takeit549.xyz", + "takeit55.xyz", + "takeit550.xyz", + "takeit551.xyz", + "takeit552.xyz", + "takeit553.xyz", + "takeit554.xyz", + "takeit555.xyz", + "takeit556.xyz", + "takeit557.xyz", + "takeit558.xyz", + "takeit559.xyz", + "takeit56.xyz", + "takeit560.xyz", + "takeit561.xyz", + "takeit562.xyz", + "takeit563.xyz", + "takeit564.xyz", + "takeit565.xyz", + "takeit566.xyz", + "takeit567.xyz", + "takeit568.xyz", + "takeit569.xyz", + "takeit57.xyz", + "takeit570.xyz", + "takeit571.xyz", + "takeit572.xyz", + "takeit573.xyz", + "takeit574.xyz", + "takeit575.xyz", + "takeit576.xyz", + "takeit577.xyz", + "takeit578.xyz", + "takeit579.xyz", + "takeit58.xyz", + "takeit580.xyz", + "takeit581.xyz", + "takeit582.xyz", + "takeit583.xyz", + "takeit584.xyz", + "takeit586.xyz", + "takeit587.xyz", + "takeit588.xyz", + "takeit589.xyz", + "takeit59.xyz", + "takeit590.xyz", + "takeit591.xyz", + "takeit592.xyz", + "takeit594.xyz", + "takeit596.xyz", + "takeit597.xyz", + "takeit598.xyz", + "takeit599.xyz", + "takeit60.xyz", + "takeit601.xyz", + "takeit602.xyz", + "takeit603.xyz", + "takeit604.xyz", + "takeit605.xyz", + "takeit606.xyz", + "takeit607.xyz", + "takeit608.xyz", + "takeit61.xyz", + "takeit610.xyz", + "takeit611.xyz", + "takeit612.xyz", + "takeit613.xyz", + "takeit614.xyz", + "takeit615.xyz", + "takeit616.xyz", + "takeit617.xyz", + "takeit618.xyz", + "takeit619.xyz", + "takeit62.xyz", + "takeit620.xyz", + "takeit621.xyz", + "takeit622.xyz", + "takeit623.xyz", + "takeit624.xyz", + "takeit625.xyz", + "takeit626.xyz", + "takeit627.xyz", + "takeit628.xyz", + "takeit629.xyz", + "takeit63.xyz", + "takeit630.xyz", + "takeit631.xyz", + "takeit632.xyz", + "takeit633.xyz", + "takeit634.xyz", + "takeit635.xyz", + "takeit636.xyz", + "takeit637.xyz", + "takeit638.xyz", + "takeit639.xyz", + "takeit64.xyz", + "takeit640.xyz", + "takeit641.xyz", + "takeit642.xyz", + "takeit643.xyz", + "takeit644.xyz", + "takeit645.xyz", + "takeit646.xyz", + "takeit647.xyz", + "takeit648.xyz", + "takeit649.xyz", + "takeit650.xyz", + "takeit651.xyz", + "takeit652.xyz", + "takeit653.xyz", + "takeit654.xyz", + "takeit655.xyz", + "takeit656.xyz", + "takeit657.xyz", + "takeit658.xyz", + "takeit659.xyz", + "takeit66.xyz", + "takeit660.xyz", + "takeit661.xyz", + "takeit662.xyz", + "takeit67.xyz", + "takeit68.xyz", + "takeit69.xyz", + "takeit70.xyz", + "takeit71.xyz", + "takeit72.xyz", + "takeit73.xyz", + "takeit74.xyz", + "takeit75.xyz", + "takeit76.xyz", + "takeit77.xyz", + "takeit78.xyz", + "takeit79.xyz", + "takeit80.xyz", + "takeit81.xyz", + "takeit82.xyz", + "takeit83.xyz", + "takeit84.xyz", + "takeit85.xyz", + "takeit86.xyz", + "takeit87.xyz", + "takeit88.xyz", + "takeit89.xyz", + "takeit90.xyz", + "takeit91.xyz", + "takeit92.xyz", + "takeit93.xyz", + "takeit94.xyz", + "takeit95.xyz", + "takeit96.xyz", + "takeit97.xyz", + "takeit98.xyz", + "takeit99.xyz", + "tasty-drop.pp.ua", + "tasty-skill.net.ru", + "tastygo.ru.com", + "tastyskill.net.ru", + "taty-dropp.info", + "team-dream.xyz", + "team.the-shrubbery.co.uk", + "teamastrallis.org.ru", + "teamfnat.net.ru", + "teamfnattic.org.ru", + "teamgog.pp.ua", + "terrifvvev.com", + "test-domuin2.com", + "test-domuin3.ru", + "test-domuin4.ru", + "test-domuin5.ru", + "testbot2021.ru", + "testy-drop.pp.ua", + "tf2market.store", + "thediscordapp.com", + "themekaversed.org", + "themekaverses.org", + "think-when.xyz", + "thor-case.net.ru", + "threemeterssky.ru", + "tigers.pp.ua", + "tik-team-topp.org.ru", + "tiktok.verifiedbadgehelp-form.ml", + "tiktokmagic.ru", + "tiktoksupport.ru.com", + "tini.best", + "tipteamgg.xyz", + "toolprotimenow.com", + "toom-skins.xyz", + "toornirs.pp.ua", + "top-team.org.ru", + "topcase.monster", + "topconsumerproductsonline.com", + "topeasyllucky.pp.ua", + "topgadgetneckmassager.com", + "toprobux.site", + "topstteeamleto2021.net.ru", + "topsweeps.com", + "topvincere.net.ru", + "topvincere.org.ru", + "topvincere.pp.ru", + "topw-gamez.xyz", + "topz-games.xyz", + "tourggesports.ru", + "tournament.ru.com", + "tournamentcs.live", + "tournamentcsgo.ga", + "tournamentcsgo.gq", + "tournaments.ru.com", + "tournamentsplay.site", + "tournamentt.com", + "tournrecruit.xyz", + "trabeoffer.ru", + "trabeoffers.xyz", + "trade-csmoney.ru", + "trade-dexter.xyz", + "trade-leagues.com", + "trade-link-offer.ru", + "trade-linkk.ru", + "trade-offers.link", + "trade-offersz.pp.ua", + "trade-profile.fun", + "trade.ru.com", + "tradeaffix.pp.ua", + "tradeandyou.ru", + "tradecs.ru.com", + "tradelink.live", + "tradeoff.space", + "tradeoffer-link.ru.com", + "tradeoffer-new.ru", + "tradeoffer.com.ru", + "tradeoffers.net.ru", + "tradeoffers11.xyz", + "traderlink.ru.com", + "traders-offers.com", + "trades-league.com", + "trades-offers.xyz", + "tradesoffers.com", + "treader-offer.com", + "tredecsgo.com", + "treders-offers.com", + "treplov.pp.ua", + "triumph.tk", + "true-money.xyz", + "truepnl-giveaway.info", + "trustpool.xyz", + "tryinfinitikloud.com", + "tryultrassenceskin.com", + "tugceyumakogullari.tk", + "twitch-facepanch.com", + "twitch-nude.com", + "twitch-starter.com", + "twitch.facepunch-llc.com", + "twitch.facepunch-ltd.com", + "twitch.facepunchs.com", + "twitch.facepunchstudio.com", + "twitch.rust-ltd.com", + "tylofpcasy.xyz", + "u924157p.beget.tech", + "ultimateskins.xyz", + "ultracup.fun", + "umosleep.ru", + "universityteam.xyz", + "up-discord.ru", + "up-nitro.com", + "up-you.ru", + "upcs.monster", + "us-appmonie.yousweeps.com", + "uspringcup.com", + "ut.ntwrk.yunihost.ru", + "v-roblox.com", + "vbucksminer.ru", + "verifapp.us", + "verification-discord.com", + "verifications-discord.com", + "verifiedbadgehelp-form.ml", + "verify-discord.com", + "verifyaccount-for-bluetick.com", + "versus-cup.ru", + "versus-play.ru", + "versuscs.ru", + "versuscsgoplay.pp.ua", + "versusplay.ru", + "vippobrit.ru", + "vippobrit1.ru.com", + "visaxsteam.ru", + "vitality-cyber.net", + "vitality-playtime.com", + "vitality-top.ru", + "vitalityboxs.com", + "vitalitycamp.ru", + "vitalityesports.net", + "vitalitygg.ru", + "viwwzagul.xyz", + "viwwzaguls.xyz", + "viwwzagulw.xyz", + "viwwzaguly.xyz", + "vkbonus.club", + "vm1189661.firstbyte.club", + "vpitems.xyz", + "vqojiorq.ru", + "waccupzero.ru.com", + "waccupzerow.monster", + "wallet-steam.ml", + "wanmei-hy.ru", + "wanmeics6.ru", + "wanmeicsgo1.ru", + "wanmeipt.ru", + "wanmeizi.ru", + "waterbets.ru", + "waucupsz.monster", + "wavebtc.com", + "we-player.ru", + "wearewinagain.xyz", + "webr-roblox.com", + "weplay.ru.com", + "were-want.ru.com", + "wheel-run.ru", + "white-guns.xyz", + "white-list.live", + "whitelampa.xyz", + "widesdays.com", + "win-lems.org.ru", + "win-skin.top", + "win-skin.xyz", + "win-trader.org.ru", + "winknifespin.xyz", + "winner-roll.ru", + "winrbx1s1.pw", + "wins-navi.com", + "winskin-simple.xyz", + "winskins.top", + "wintheskin.xyz", + "withereum.com", + "word-the.xyz", + "wowfnatic.ru", + "wtf-magic.ru", + "wtf-magic.top", + "wtf-magicru.top", + "wtf-win.net.ru", + "ww1.dicsordapp.com", + "ww1.discordapp.org", + "ww11.steamcommunity.download", + "ww16.discordcanary.com", + "ww8.steamcommmunity.ru.com", + "wwdiscord.com", + "www-steamcommunlty.com", + "www2.c2bit.online", + "wwwlog-in.xyz", + "wyxy.ru", + "x33681t2.beget.tech", + "xdiscord.com", + "xesa-nitro.com", + "xess-nitro.com", + "xfxcheats.online", + "xgamercup.com", + "xn--e1agajgahgxri7a.site", + "xn--steamcommunit-ge3g.com", + "xorialloy.xyz", + "xpro.gift", + "xpro.ws", + "xpromo-discord.com", + "xroll.space", + "xscsgo.com", + "xtradefox.com", + "xtradeskin.com", + "yeppymoll.xyz", + "yolock.site", + "youtubers2021.xyz", + "youtubersrwrds.xyz", + "yummy-nitro.com", + "z93729n9.beget.tech", + "zakat.ntwrk.yunihost.ru", + "zerocup.ru", + "zipsetgo.com", + "zonewarco.org.ru", + "zonewarco.org.ru", + // "steamcommunity.co", +]; diff --git a/lib/badwords.ts b/lib/badwords.ts new file mode 100644 index 0000000..5260264 --- /dev/null +++ b/lib/badwords.ts @@ -0,0 +1,845 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { BadWords, Severity as AutomodSeverity } from "./automod/AutomodShared.js"; + +// duplicated here so that this file can be compiled using the `isolatedModules` option +/** + * @see {@link AutomodSeverity} + */ +const enum Severity { + DELETE, + WARN, + TEMP_MUTE, + PERM_MUTE, +} + +export default { + /* -------------------------------------------------------------------------- */ + /* Slurs */ + /* -------------------------------------------------------------------------- */ + "Slurs": [ + { + match: "faggot", + severity: Severity.TEMP_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "homophobic slur", + regex: false, + userInfo: true, + }, + { + match: "nigga", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "racial slur", + regex: false, + userInfo: true, + }, + { + match: "nigger", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "racial slur", + regex: false, + userInfo: true, + }, + { + match: "nigra", + severity: Severity.PERM_MUTE, + ignoreSpaces: false, + ignoreCapitalization: true, + reason: "racial slur", + regex: false, + userInfo: false, + }, + { + match: "retard", + severity: Severity.TEMP_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "ableist slur", + regex: false, + userInfo: true, + }, + { + match: "retarted", + severity: Severity.TEMP_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "ableist slur", + regex: false, + userInfo: false, + }, + { + match: "slut", + severity: Severity.WARN, + ignoreSpaces: false, + ignoreCapitalization: true, + reason: "derogatory term", + regex: false, + userInfo: false, + }, + { + match: "tar baby", + severity: Severity.TEMP_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "racial slur", + regex: false, + }, + { + match: "whore", + severity: Severity.WARN, + ignoreSpaces: false, + ignoreCapitalization: true, + reason: "derogatory term", + regex: false, + userInfo: false, + }, + { + match: "卍", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "racist symbol", + regex: false, + userInfo: true, + }, + { + //? N word + match: "space movie 1992", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "racial slur", + regex: false, + userInfo: false, + }, + { + //? N word + match: "黑鬼", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "racial slur", + regex: false, + userInfo: true, + }, + ], + + /* -------------------------------------------------------------------------- */ + /* Steam Scams */ + /* -------------------------------------------------------------------------- */ + "Steam Scams": [ + { + //? I'm on tilt, in the cop they gave the status "Unreliable" + match: 'Я в тильте, в кс дали статус "Ненадежный"', + severity: Severity.WARN, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "hello i am leaving cs:go", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "hello! I'm done with csgo", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "hi bro, i'm leaving this fucking game, take my skin", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "hi friend, today i am leaving this fucking game", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "hi guys, i'm leaving this fucking game, take my", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "hi, bro h am leaving cs:go and giving away my skin", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "hi, bro i am leaving cs:go and giving away my skin", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "i confirm all exchanges, there won't be enough", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "i quit csgo", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "the first three who send a trade", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "you can choose any skin for yourself", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Hey, I'm leaving for the army and giving the skins", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "fuck this trash called CS:GO, deleted,", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "please take my skins", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Hi, I stopped playing CS:GO and decided to giveaway my inventory.", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "steam scam phrase", + regex: false, + userInfo: false, + }, + ], + + /* -------------------------------------------------------------------------- */ + /* Nitro Scams */ + /* -------------------------------------------------------------------------- */ + "Nitro Scams": [ + { + match: "and there is discord hallween's giveaway", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "discord nitro for free - steam store", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "free 3 months of discord nitro", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "free discord nitro airdrop", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "get 3 months of discord nitro", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "get discord nitro for free", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "get free discord nitro from steam", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "lol, jahjajha free discord nitro for 3 month!!", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "steam is giving away 3 months of discord nitro for free to all no limited steam users", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + //? Lol, 1 month free discord nitro! + match: "Лол, бесплатный дискорд нитро на 1 месяц!", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Airdrop Discord FREE NITRO from Steam —", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "take nitro faster, it's already running out", + severity: Severity.PERM_MUTE, + ignoreSpaces: false, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "only the first 10 people will have time to take nitro", + severity: Severity.PERM_MUTE, + ignoreSpaces: false, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Discord is giving away nitro!", + severity: Severity.PERM_MUTE, + ignoreSpaces: false, + ignoreCapitalization: false, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Free gift discord nitro for 1 month!", + severity: Severity.PERM_MUTE, + ignoreSpaces: false, + ignoreCapitalization: false, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Hi i claim this nitro for free 3 months lol!", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "bro watch this, working nitro gen", + severity: Severity.PERM_MUTE, + ignoreSpaces: false, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Free distribution of discord nitro for 3 months from steam!", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Get 3 Months of Discord Nitro. Personalize your profile, screen share in HD, upgrade your emojis, and more!", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Steam is giving away free discord nitro, have time to pick up at my link", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Airdrop Discord NITRO with", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Check this lol, there nitro is handed out for free, take it until everything is sorted out", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "A free Discord Nitro | Steam Store Discord Nitro Distribution.", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Xbox gives away discord nitro for free", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "airdrop discord nitro by steam", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + //? 3 months nitro free from steam, take too + match: "3 месяца нитро бесплатно от стима, забирайте тоже", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + // ? includes non-latin characters + match: "Free distributiοn of discοrd nitrο for 3 months from steаm!", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Free discord nitro for 1 month!", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "I got some nitro left over here", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Hey, steam gived nitro", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "nitro giveaway by steam, take it", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "3 months nitro from styme,", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "XBOX and DISCORD are giving away free NITRO FULL for a month.", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Hi,take the Discord Nitro for free", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + //? Discord nitro got free, take it before it's too late + match: "Дискорд нитро получил бесплатно,забирай пока не поздно", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "1 month nitro for free", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Gifts for the new year, nitro for 3 months", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "1 month nitro from steam, take it guys", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Hello, discord and steam are giving away nitro, take it away", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Who is first? :)", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Whо is first? :)", + //? This one uses a different o, prob should make some autodelete if includes link and special char + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Discord Nitro distribution from STEAM", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "3 month nitro for free, take it ", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "3 months nitro from steam, take it guys)", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Gifts from steam nitro, gifts for 3 months", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Free subscription for 3 months DISCORD NITRO", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "who will catch this gift?)", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "take it guys :)", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Discord and Steam are giving away a free 3-month Discord Gift subscription!", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + { + match: "Discord free nitro from steam", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "discord nitro scam phrase", + regex: false, + userInfo: false, + }, + ], + + /* -------------------------------------------------------------------------- */ + /* Misc Scams */ + /* -------------------------------------------------------------------------- */ + "Misc Scams": [ + { + match: "found a cool software that improves the", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "misc. scam phrase", + regex: false, + userInfo: false, + }, + { + match: + "there is a possible chance tomorrow there will be a cyber-attack event where on all social networks including Discord there will be people trying", + severity: Severity.WARN, + ignoreSpaces: false, + ignoreCapitalization: true, + reason: "annoying copy pasta", + regex: false, + userInfo: false, + }, + { + match: "i made a game can you test play ?", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "malware phrase", + regex: false, + userInfo: false, + }, + { + match: "tell me if something is wrong in the game", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "malware phrase", + regex: false, + userInfo: false, + }, + { + match: "Hi, can you check out the game I created today:)", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "malware phrase", + regex: false, + userInfo: false, + }, + { + match: "Just want to get other people's opinions, what to add and what to remove.", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "malware phrase", + regex: false, + userInfo: false, + }, + { + match: "https://discord.gg/KKnGGvEPVM", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "misc. scam phrase", + regex: false, + userInfo: false, + }, + { + match: "https://discord.gg/rykjvpTGrB", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "misc. scam phrase", + regex: false, + userInfo: false, + }, + { + match: "https://discord.gg/XTDQgJ9YMp", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "misc. scam phrase", + regex: false, + userInfo: false, + }, + ], + + /* -------------------------------------------------------------------------- */ + /* Advertising */ + /* -------------------------------------------------------------------------- */ + "Advertising": [ + { + match: "😀 wow only 13+... 😳 are allowed to see my about me 😏", + severity: Severity.PERM_MUTE, + ignoreSpaces: true, + ignoreCapitalization: true, + reason: "advertising", + regex: false, + userInfo: false, + }, + ], +} as BadWords; diff --git a/lib/common/BushCache.ts b/lib/common/BushCache.ts new file mode 100644 index 0000000..22a13ef --- /dev/null +++ b/lib/common/BushCache.ts @@ -0,0 +1,26 @@ +import { BadWords, GlobalModel, SharedModel, type Guild } from '#lib'; +import { Collection, type Snowflake } from 'discord.js'; + +export class BushCache { + public global = new GlobalCache(); + public shared = new SharedCache(); + public guilds = new GuildCache(); +} + +export class GlobalCache implements Omit { + public disabledCommands: string[] = []; + public blacklistedChannels: Snowflake[] = []; + public blacklistedGuilds: Snowflake[] = []; + public blacklistedUsers: Snowflake[] = []; +} + +export class SharedCache implements Omit { + public superUsers: Snowflake[] = []; + public privilegedUsers: Snowflake[] = []; + public badLinksSecret: string[] = []; + public badLinks: string[] = []; + public badWords: BadWords = {}; + public autoBanCode: string | null = null; +} + +export class GuildCache extends Collection {} diff --git a/lib/common/ButtonPaginator.ts b/lib/common/ButtonPaginator.ts new file mode 100644 index 0000000..92f3796 --- /dev/null +++ b/lib/common/ButtonPaginator.ts @@ -0,0 +1,224 @@ +import { DeleteButton, type CommandMessage, type SlashMessage } from '#lib'; +import { CommandUtil } from 'discord-akairo'; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + type APIEmbed, + type Message, + type MessageComponentInteraction +} from 'discord.js'; + +/** + * Sends multiple embeds with controls to switch between them + */ +export class ButtonPaginator { + /** + * The current page of the paginator + */ + protected curPage: number; + + /** + * The paginator message + */ + protected sentMessage: Message | undefined; + + /** + * @param message The message that triggered the command + * @param embeds The embeds to switch between + * @param text The optional text to send with the paginator + * @param {} [deleteOnExit=true] Whether the paginator message gets deleted when the exit button is pressed + * @param startOn The page to start from (**not** the index) + */ + protected constructor( + protected message: CommandMessage | SlashMessage, + protected embeds: EmbedBuilder[] | APIEmbed[], + protected text: string | null, + protected deleteOnExit: boolean, + startOn: number + ) { + this.curPage = startOn - 1; + + // add footers + for (let i = 0; i < embeds.length; i++) { + if (embeds[i] instanceof EmbedBuilder) { + (embeds[i] as EmbedBuilder).setFooter({ text: `Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}` }); + } else { + (embeds[i] as APIEmbed).footer = { + text: `Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}` + }; + } + } + } + + /** + * The number of pages in the paginator + */ + protected get numPages(): number { + return this.embeds.length; + } + + /** + * Sends the paginator message + */ + protected async send() { + this.sentMessage = await this.message.util.reply({ + content: this.text, + embeds: [this.embeds[this.curPage]], + components: [this.getPaginationRow()] + }); + + const collector = this.sentMessage.createMessageComponentCollector({ + filter: (i) => i.customId.startsWith('paginate_'), + time: 300_000 + }); + collector.on('collect', (i) => void this.collect(i)); + collector.on('end', () => void this.end()); + } + + /** + * Handles interactions with the paginator + * @param interaction The interaction received + */ + protected async collect(interaction: MessageComponentInteraction) { + if (interaction.user.id !== this.message.author.id && !this.message.client.config.owners.includes(interaction.user.id)) + return await interaction?.deferUpdate().catch(() => null); + + switch (interaction.customId) { + case 'paginate_beginning': + this.curPage = 0; + await this.edit(interaction); + break; + case 'paginate_back': + this.curPage--; + await this.edit(interaction); + break; + case 'paginate_stop': + if (this.deleteOnExit) { + await interaction.deferUpdate().catch(() => null); + await this.sentMessage!.delete().catch(() => null); + break; + } else { + await interaction + ?.update({ + content: `${ + this.text + ? `${this.text} +` + : '' + }Command closed by user.`, + embeds: [], + components: [] + }) + .catch(() => null); + break; + } + case 'paginate_next': + this.curPage++; + await this.edit(interaction); + break; + case 'paginate_end': + this.curPage = this.embeds.length - 1; + await this.edit(interaction); + break; + } + } + + /** + * Ends the paginator + */ + protected async end() { + if (this.sentMessage && !CommandUtil.deletedMessages.has(this.sentMessage.id)) + await this.sentMessage + .edit({ + content: this.text, + embeds: [this.embeds[this.curPage]], + components: [this.getPaginationRow(true)] + }) + .catch(() => null); + } + + /** + * Edits the paginator message + * @param interaction The interaction received + */ + protected async edit(interaction: MessageComponentInteraction) { + await interaction + ?.update({ + content: this.text, + embeds: [this.embeds[this.curPage]], + components: [this.getPaginationRow()] + }) + .catch(() => null); + } + + /** + * Generates the pagination row based on the class properties + * @param disableAll Whether to disable all buttons + * @returns The generated {@link ActionRow} + */ + protected getPaginationRow(disableAll = false) { + return new ActionRowBuilder().addComponents( + new ButtonBuilder({ + style: ButtonStyle.Primary, + customId: 'paginate_beginning', + emoji: PaginateEmojis.BEGINNING, + disabled: disableAll || this.curPage === 0 + }), + new ButtonBuilder({ + style: ButtonStyle.Primary, + customId: 'paginate_back', + emoji: PaginateEmojis.BACK, + disabled: disableAll || this.curPage === 0 + }), + new ButtonBuilder({ + style: ButtonStyle.Primary, + customId: 'paginate_stop', + emoji: PaginateEmojis.STOP, + disabled: disableAll + }), + new ButtonBuilder({ + style: ButtonStyle.Primary, + customId: 'paginate_next', + emoji: PaginateEmojis.FORWARD, + disabled: disableAll || this.curPage === this.numPages - 1 + }), + new ButtonBuilder({ + style: ButtonStyle.Primary, + customId: 'paginate_end', + emoji: PaginateEmojis.END, + disabled: disableAll || this.curPage === this.numPages - 1 + }) + ); + } + + /** + * Sends multiple embeds with controls to switch between them + * @param message The message to respond to + * @param embeds The embeds to switch between + * @param text The text send with the embeds (optional) + * @param deleteOnExit Whether to delete the message when the exit button is clicked (defaults to true) + * @param startOn The page to start from (**not** the index) + */ + public static async send( + message: CommandMessage | SlashMessage, + embeds: EmbedBuilder[] | APIEmbed[], + text: string | null = null, + deleteOnExit = true, + startOn = 1 + ) { + // no need to paginate if there is only one page + if (embeds.length === 1) return DeleteButton.send(message, { embeds: embeds }); + + return await new ButtonPaginator(message, embeds, text, deleteOnExit, startOn).send(); + } +} + +export const PaginateEmojis = { + BEGINNING: { id: '853667381335162910', name: 'w_paginate_beginning', animated: false } as const, + BACK: { id: '853667410203770881', name: 'w_paginate_back', animated: false } as const, + STOP: { id: '853667471110570034', name: 'w_paginate_stop', animated: false } as const, + FORWARD: { id: '853667492680564747', name: 'w_paginate_next', animated: false } as const, + END: { id: '853667514915225640', name: 'w_paginate_end', animated: false } as const +} as const; diff --git a/lib/common/CanvasProgressBar.ts b/lib/common/CanvasProgressBar.ts new file mode 100644 index 0000000..fb4f778 --- /dev/null +++ b/lib/common/CanvasProgressBar.ts @@ -0,0 +1,83 @@ +import { CanvasRenderingContext2D } from 'canvas'; + +/** + * I just copy pasted this code from stackoverflow don't yell at me if there is issues for it + * @author @TymanWasTaken + */ +export class CanvasProgressBar { + private readonly x: number; + private readonly y: number; + private readonly w: number; + private readonly h: number; + private readonly color: string; + private percentage: number; + private p?: number; + private ctx: CanvasRenderingContext2D; + + public constructor( + ctx: CanvasRenderingContext2D, + dimension: { x: number; y: number; width: number; height: number }, + color: string, + percentage: number + ) { + ({ x: this.x, y: this.y, width: this.w, height: this.h } = dimension); + this.color = color; + this.percentage = percentage; + this.p = undefined; + this.ctx = ctx; + } + + public draw(): void { + // ----------------- + this.p = this.percentage * this.w; + if (this.p <= this.h) { + this.ctx.beginPath(); + this.ctx.arc( + this.h / 2 + this.x, + this.h / 2 + this.y, + this.h / 2, + Math.PI - Math.acos((this.h - this.p) / this.h), + Math.PI + Math.acos((this.h - this.p) / this.h) + ); + this.ctx.save(); + this.ctx.scale(-1, 1); + this.ctx.arc( + this.h / 2 - this.p - this.x, + this.h / 2 + this.y, + this.h / 2, + Math.PI - Math.acos((this.h - this.p) / this.h), + Math.PI + Math.acos((this.h - this.p) / this.h) + ); + this.ctx.restore(); + this.ctx.closePath(); + } else { + this.ctx.beginPath(); + this.ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, (3 / 2) * Math.PI); + this.ctx.lineTo(this.p - this.h + this.x, 0 + this.y); + this.ctx.arc(this.p - this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, (3 / 2) * Math.PI, Math.PI / 2); + this.ctx.lineTo(this.h / 2 + this.x, this.h + this.y); + this.ctx.closePath(); + } + this.ctx.fillStyle = this.color; + this.ctx.fill(); + } + + // public showWholeProgressBar(){ + // this.ctx.beginPath(); + // this.ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, 3 / 2 * Math.PI); + // this.ctx.lineTo(this.w - this.h + this.x, 0 + this.y); + // this.ctx.arc(this.w - this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, 3 / 2 *Math.PI, Math.PI / 2); + // this.ctx.lineTo(this.h / 2 + this.x, this.h + this.y); + // this.ctx.strokeStyle = '#000000'; + // this.ctx.stroke(); + // this.ctx.closePath(); + // } + + public get PPercentage(): number { + return this.percentage * 100; + } + + public set PPercentage(x: number) { + this.percentage = x / 100; + } +} diff --git a/lib/common/ConfirmationPrompt.ts b/lib/common/ConfirmationPrompt.ts new file mode 100644 index 0000000..b87d9ef --- /dev/null +++ b/lib/common/ConfirmationPrompt.ts @@ -0,0 +1,64 @@ +import { type CommandMessage, type SlashMessage } from '#lib'; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, type MessageComponentInteraction, type MessageOptions } from 'discord.js'; + +/** + * Sends a message with buttons for the user to confirm or cancel the action. + */ +export class ConfirmationPrompt { + /** + * @param message The message that triggered the command + * @param messageOptions Options for sending the message + */ + protected constructor(protected message: CommandMessage | SlashMessage, protected messageOptions: MessageOptions) {} + + /** + * Sends a message with buttons for the user to confirm or cancel the action. + */ + protected async send(): Promise { + this.messageOptions.components = [ + new ActionRowBuilder().addComponents( + new ButtonBuilder({ style: ButtonStyle.Success, customId: 'confirmationPrompt_confirm', label: 'Yes' }), + new ButtonBuilder({ style: ButtonStyle.Danger, customId: 'confirmationPrompt_cancel', label: 'No' }) + ) + ]; + + const msg = await this.message.channel!.send(this.messageOptions); + + return await new Promise((resolve) => { + let responded = false; + const collector = msg.createMessageComponentCollector({ + filter: (interaction) => interaction.message?.id == msg.id, + time: 300_000 + }); + + collector.on('collect', async (interaction: MessageComponentInteraction) => { + await interaction.deferUpdate().catch(() => undefined); + if (interaction.user.id == this.message.author.id || this.message.client.config.owners.includes(interaction.user.id)) { + if (interaction.customId === 'confirmationPrompt_confirm') { + responded = true; + collector.stop(); + resolve(true); + } else if (interaction.customId === 'confirmationPrompt_cancel') { + responded = true; + collector.stop(); + resolve(false); + } + } + }); + + collector.on('end', async () => { + await msg.delete().catch(() => undefined); + if (!responded) resolve(false); + }); + }); + } + + /** + * Sends a message with buttons for the user to confirm or cancel the action. + * @param message The message that triggered the command + * @param sendOptions Options for sending the message + */ + public static async send(message: CommandMessage | SlashMessage, sendOptions: MessageOptions): Promise { + return new ConfirmationPrompt(message, sendOptions).send(); + } +} diff --git a/lib/common/DeleteButton.ts b/lib/common/DeleteButton.ts new file mode 100644 index 0000000..340d07f --- /dev/null +++ b/lib/common/DeleteButton.ts @@ -0,0 +1,78 @@ +import { PaginateEmojis, type CommandMessage, type SlashMessage } from '#lib'; +import { CommandUtil } from 'discord-akairo'; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + MessageComponentInteraction, + MessageEditOptions, + MessagePayload, + type MessageOptions +} from 'discord.js'; + +/** + * Sends a message with a button for the user to delete it. + */ +export class DeleteButton { + /** + * @param message The message to respond to + * @param messageOptions The send message options + */ + protected constructor(protected message: CommandMessage | SlashMessage, protected messageOptions: MessageOptions) {} + + /** + * Sends a message with a button for the user to delete it. + */ + protected async send() { + this.updateComponents(); + + const msg = await this.message.util.reply(this.messageOptions); + + const collector = msg.createMessageComponentCollector({ + filter: (interaction) => interaction.customId == 'paginate__stop' && interaction.message?.id == msg.id, + time: 300000 + }); + + collector.on('collect', async (interaction: MessageComponentInteraction) => { + await interaction.deferUpdate().catch(() => undefined); + if (interaction.user.id == this.message.author.id || this.message.client.config.owners.includes(interaction.user.id)) { + if (msg.deletable && !CommandUtil.deletedMessages.has(msg.id)) await msg.delete(); + } + }); + + collector.on('end', async () => { + this.updateComponents(true, true); + await msg.edit(this.messageOptions).catch(() => undefined); + }); + } + + /** + * Generates the components for the message + * @param edit Whether or not the message is being edited + * @param disable Whether or not to disable the buttons + */ + protected updateComponents(edit = false, disable = false): void { + this.messageOptions.components = [ + new ActionRowBuilder().addComponents( + new ButtonBuilder({ + style: ButtonStyle.Primary, + customId: 'paginate__stop', + emoji: PaginateEmojis.STOP, + disabled: disable + }) + ) + ]; + if (edit) { + this.messageOptions.reply = undefined; + } + } + + /** + * Sends a message with a button for the user to delete it. + * @param message The message to respond to + * @param options The send message options + */ + public static async send(message: CommandMessage | SlashMessage, options: Omit) { + return new DeleteButton(message, options).send(); + } +} diff --git a/lib/common/HighlightManager.ts b/lib/common/HighlightManager.ts new file mode 100644 index 0000000..cc31413 --- /dev/null +++ b/lib/common/HighlightManager.ts @@ -0,0 +1,488 @@ +import { addToArray, format, Highlight, removeFromArray, timestamp, type HighlightWord } from '#lib'; +import assert from 'assert/strict'; +import { + ChannelType, + Collection, + GuildMember, + type Channel, + type Client, + type Message, + type Snowflake, + type TextBasedChannel +} from 'discord.js'; +import { colors, Time } from '../utils/BushConstants.js'; +import { sanitizeInputForDiscord } from '../utils/Format.js'; + +const NOTIFY_COOLDOWN = 5 * Time.Minute; +const OWNER_NOTIFY_COOLDOWN = 5 * Time.Minute; +const LAST_MESSAGE_COOLDOWN = 5 * Time.Minute; + +type users = Set; +type channels = Set; +type word = HighlightWord; +type guild = Snowflake; +type user = Snowflake; +type lastMessage = Date; +type lastDM = Message; + +type lastDmInfo = [lastDM: lastDM, guild: guild, channel: Snowflake, highlights: HighlightWord[]]; + +export class HighlightManager { + public static keep = new Set(); + + /** + * Cached guild highlights. + */ + public readonly guildHighlights = new Collection>(); + + //~ /** + //~ * Cached global highlights. + //~ */ + //~ public readonly globalHighlights = new Collection(); + + /** + * A collection of cooldowns of when a user last sent a message in a particular guild. + */ + public readonly userLastTalkedCooldown = new Collection>(); + + /** + * Users that users have blocked + */ + public readonly userBlocks = new Collection>(); + + /** + * Channels that users have blocked + */ + public readonly channelBlocks = new Collection>(); + + /** + * A collection of cooldowns of when the bot last sent each user a highlight message. + */ + public readonly lastedDMedUserCooldown = new Collection(); + + /** + * @param client The client to use. + */ + public constructor(public readonly client: Client) {} + + /** + * Sync the cache with the database. + */ + public async syncCache(): Promise { + const highlights = await Highlight.findAll(); + + this.guildHighlights.clear(); + + for (const highlight of highlights) { + highlight.words.forEach((word) => { + if (!this.guildHighlights.has(highlight.guild)) this.guildHighlights.set(highlight.guild, new Collection()); + const guildCache = this.guildHighlights.get(highlight.guild)!; + if (!guildCache.get(word)) guildCache.set(word, new Set()); + guildCache.get(word)!.add(highlight.user); + }); + + if (!this.userBlocks.has(highlight.guild)) this.userBlocks.set(highlight.guild, new Collection()); + this.userBlocks.get(highlight.guild)!.set(highlight.user, new Set(highlight.blacklistedUsers)); + + if (!this.channelBlocks.has(highlight.guild)) this.channelBlocks.set(highlight.guild, new Collection()); + this.channelBlocks.get(highlight.guild)!.set(highlight.user, new Set(highlight.blacklistedChannels)); + } + } + + /** + * Checks a message for highlights. + * @param message The message to check. + * @returns A collection users mapped to the highlight matched + */ + public checkMessage(message: Message): Collection { + // even if there are multiple matches, only the first one is returned + const ret = new Collection(); + if (!message.content || !message.inGuild()) return ret; + if (!this.guildHighlights.has(message.guildId)) return ret; + + const guildCache = this.guildHighlights.get(message.guildId)!; + + for (const [word, users] of guildCache.entries()) { + if (!this.isMatch(message.content, word)) continue; + + for (const user of users) { + if (ret.has(user)) continue; + + if (!message.channel.permissionsFor(user)?.has('ViewChannel')) continue; + + const blockedUsers = this.userBlocks.get(message.guildId)?.get(user) ?? new Set(); + if (blockedUsers.has(message.author.id)) { + void this.client.console.verbose( + 'Highlight', + `Highlight ignored because <<${this.client.users.cache.get(user)?.tag ?? user}>> blocked the user <<${ + message.author.tag + }>>` + ); + continue; + } + const blockedChannels = this.channelBlocks.get(message.guildId)?.get(user) ?? new Set(); + if (blockedChannels.has(message.channel.id)) { + void this.client.console.verbose( + 'Highlight', + `Highlight ignored because <<${this.client.users.cache.get(user)?.tag ?? user}>> blocked the channel <<${ + message.channel.name + }>>` + ); + continue; + } + if (message.mentions.has(user)) { + void this.client.console.verbose( + 'Highlight', + `Highlight ignored because <<${this.client.users.cache.get(user)?.tag ?? user}>> is already mentioned in the message.` + ); + continue; + } + ret.set(user, word); + } + } + + return ret; + } + + /** + * Checks a user provided phrase for their highlights. + * @param guild The guild to check in. + * @param user The user to get the highlights for. + * @param phrase The phrase for highlights in. + * @returns A collection of the user's highlights mapped to weather or not it was matched. + */ + public async checkPhrase(guild: Snowflake, user: Snowflake, phrase: string): Promise> { + const highlights = await Highlight.findAll({ where: { guild, user } }); + + const results = new Collection(); + + for (const highlight of highlights) { + for (const word of highlight.words) { + results.set(word, this.isMatch(phrase, word)); + } + } + + return results; + } + + /** + * Checks a particular highlight for a match within a phrase. + * @param phrase The phrase to check for the word in. + * @param hl The highlight to check for. + * @returns Whether or not the highlight was matched. + */ + private isMatch(phrase: string, hl: HighlightWord): boolean { + if (hl.regex) { + return new RegExp(hl.word, 'gi').test(phrase); + } else { + if (hl.word.includes(' ')) { + return phrase.toLocaleLowerCase().includes(hl.word.toLocaleLowerCase()); + } else { + const words = phrase.split(/\s*\b\s/); + return words.some((w) => w.toLocaleLowerCase() === hl.word.toLocaleLowerCase()); + } + } + } + + /** + * Adds a new highlight to a user in a particular guild. + * @param guild The guild to add the highlight to. + * @param user The user to add the highlight to. + * @param hl The highlight to add. + * @returns A string representing a user error or a boolean indicating the database success. + */ + public async addHighlight(guild: Snowflake, user: Snowflake, hl: HighlightWord): Promise { + if (!this.guildHighlights.has(guild)) this.guildHighlights.set(guild, new Collection()); + const guildCache = this.guildHighlights.get(guild)!; + + if (!guildCache.has(hl)) guildCache.set(hl, new Set()); + guildCache.get(hl)!.add(user); + + const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); + + if (highlight.words.some((w) => w.word === hl.word)) return `You have already highlighted "${hl.word}".`; + + highlight.words = addToArray(highlight.words, hl); + + return Boolean(await highlight.save().catch(() => false)); + } + + /** + * Removes a highlighted word for a user in a particular guild. + * @param guild The guild to remove the highlight from. + * @param user The user to remove the highlight from. + * @param hl The word to remove. + * @returns A string representing a user error or a boolean indicating the database success. + */ + public async removeHighlight(guild: Snowflake, user: Snowflake, hl: string): Promise { + if (!this.guildHighlights.has(guild)) this.guildHighlights.set(guild, new Collection()); + const guildCache = this.guildHighlights.get(guild)!; + + const wordCache = guildCache.find((_, key) => key.word === hl); + + if (!wordCache?.has(user)) return `You have not highlighted "${hl}".`; + + wordCache!.delete(user); + + const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); + + const toRemove = highlight.words.find((w) => w.word === hl); + if (!toRemove) return `Uhhhhh... This shouldn't happen.`; + + highlight.words = removeFromArray(highlight.words, toRemove); + + return Boolean(await highlight.save().catch(() => false)); + } + + /** + * Remove all highlight words for a user in a particular guild. + * @param guild The guild to remove the highlights from. + * @param user The user to remove the highlights from. + * @returns A boolean indicating the database success. + */ + public async removeAllHighlights(guild: Snowflake, user: Snowflake): Promise { + if (!this.guildHighlights.has(guild)) this.guildHighlights.set(guild, new Collection()); + const guildCache = this.guildHighlights.get(guild)!; + + for (const [word, users] of guildCache.entries()) { + if (users.has(user)) users.delete(user); + if (users.size === 0) guildCache.delete(word); + } + + const highlight = await Highlight.findOne({ where: { guild, user } }); + + if (!highlight) return false; + + highlight.words = []; + + return Boolean(await highlight.save().catch(() => false)); + } + + /** + * Adds a new user or channel block to a user in a particular guild. + * @param guild The guild to add the block to. + * @param user The user that is blocking the target. + * @param target The target that is being blocked. + * @returns The result of the operation. + */ + public async addBlock( + guild: Snowflake, + user: Snowflake, + target: GuildMember | TextBasedChannel + ): Promise { + const cacheKey = `${target instanceof GuildMember ? 'user' : 'channel'}Blocks` as const; + const databaseKey = `blacklisted${target instanceof GuildMember ? 'Users' : 'Channels'}` as const; + + const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); + + if (highlight[databaseKey].includes(target.id)) return HighlightBlockResult.ALREADY_BLOCKED; + + const newBlocks = addToArray(highlight[databaseKey], target.id); + + highlight[databaseKey] = newBlocks; + const res = await highlight.save().catch(() => false); + if (!res) return HighlightBlockResult.ERROR; + + if (!this[cacheKey].has(guild)) this[cacheKey].set(guild, new Collection()); + const guildBlocks = this[cacheKey].get(guild)!; + guildBlocks.set(user, new Set(newBlocks)); + + return HighlightBlockResult.SUCCESS; + } + + /** + * Removes a user or channel block from a user in a particular guild. + * @param guild The guild to remove the block from. + * @param user The user that is unblocking the target. + * @param target The target that is being unblocked. + * @returns The result of the operation. + */ + public async removeBlock(guild: Snowflake, user: Snowflake, target: GuildMember | Channel): Promise { + const cacheKey = `${target instanceof GuildMember ? 'user' : 'channel'}Blocks` as const; + const databaseKey = `blacklisted${target instanceof GuildMember ? 'Users' : 'Channels'}` as const; + + const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); + + if (!highlight[databaseKey].includes(target.id)) return HighlightUnblockResult.NOT_BLOCKED; + + const newBlocks = removeFromArray(highlight[databaseKey], target.id); + + highlight[databaseKey] = newBlocks; + const res = await highlight.save().catch(() => false); + if (!res) return HighlightUnblockResult.ERROR; + + if (!this[cacheKey].has(guild)) this[cacheKey].set(guild, new Collection()); + const guildBlocks = this[cacheKey].get(guild)!; + guildBlocks.set(user, new Set(newBlocks)); + + return HighlightUnblockResult.SUCCESS; + } + + /** + * Sends a user a direct message to alert them of their highlight being triggered. + * @param message The message that triggered the highlight. + * @param user The user who's highlights was triggered. + * @param hl The highlight that was matched. + * @returns Whether or a dm was sent. + */ + public async notify(message: Message, user: Snowflake, hl: HighlightWord): Promise { + assert(message.inGuild()); + + this.client.console.debug(`Notifying ${user} of highlight ${hl.word} in ${message.guild.name}`); + + dmCooldown: { + const lastDM = this.lastedDMedUserCooldown.get(user); + if (!lastDM?.[0]) break dmCooldown; + + const cooldown = this.client.config.owners.includes(user) ? OWNER_NOTIFY_COOLDOWN : NOTIFY_COOLDOWN; + + if (new Date().getTime() - lastDM[0].createdAt.getTime() < cooldown) { + void this.client.console.verbose('Highlight', `User <<${user}>> has been DMed recently.`); + + if (lastDM[0].embeds.length < 10) { + this.client.console.debug(`Trying to add to notification queue for ${user}`); + return this.addToNotification(lastDM, message, hl); + } + + this.client.console.debug(`User has too many embeds (${lastDM[0].embeds.length}).`); + return false; + } + } + + talkCooldown: { + const lastTalked = this.userLastTalkedCooldown.get(message.guildId)?.get(user); + if (!lastTalked) break talkCooldown; + + presence: { + // incase the bot left the guild + if (message.guild) { + const member = message.guild.members.cache.get(user); + if (!member) { + this.client.console.debug(`No member found for ${user} in ${message.guild.name}`); + break presence; + } + + const presence = member.presence ?? (await member.fetch()).presence; + if (!presence) { + this.client.console.debug(`No presence found for ${user} in ${message.guild.name}`); + break presence; + } + + if (presence.status === 'offline') { + void this.client.console.verbose('Highlight', `User <<${user}>> is offline.`); + break talkCooldown; + } + } + } + + const now = new Date().getTime(); + const talked = lastTalked.getTime(); + + if (now - talked < LAST_MESSAGE_COOLDOWN) { + void this.client.console.verbose('Highlight', `User <<${user}>> has talked too recently.`); + + setTimeout(() => { + const newTalked = this.userLastTalkedCooldown.get(message.guildId)?.get(user)?.getTime(); + if (talked !== newTalked) return; + + void this.notify(message, user, hl); + }, LAST_MESSAGE_COOLDOWN).unref(); + + return false; + } + } + + return this.client.users + .send(user, { + // eslint-disable-next-line @typescript-eslint/no-base-to-string + content: `In ${format.input(message.guild.name)} ${message.channel}, your highlight "${hl.word}" was matched:`, + embeds: [this.generateDmEmbed(message, hl)] + }) + .then((dm) => { + this.lastedDMedUserCooldown.set(user, [dm, message.guildId!, message.channelId, [hl]]); + return true; + }) + .catch(() => false); + } + + private async addToNotification( + [originalDm, guild, channel, originalHl]: lastDmInfo, + message: Message, + hl: HighlightWord + ): Promise { + assert(originalDm.embeds.length < 10); + assert(originalDm.embeds.length > 0); + assert(originalDm.channel.type === ChannelType.DM); + this.client.console.debug( + `Adding to notification queue for ${originalDm.channel.recipient?.tag ?? originalDm.channel.recipientId}` + ); + + const sameGuild = guild === message.guildId; + const sameChannel = channel === message.channel.id; + const sameWord = originalHl.every((w) => w.word === hl.word); + + /* eslint-disable @typescript-eslint/no-base-to-string */ + return originalDm + .edit({ + content: `In ${sameGuild ? format.input(message.guild?.name ?? '[Unknown]') : 'multiple servers'} ${ + sameChannel ? message.channel ?? '[Unknown]' : 'multiple channels' + }, ${sameWord ? `your highlight "${hl.word}" was matched:` : 'multiple highlights were matched:'}`, + embeds: [...originalDm.embeds.map((e) => e.toJSON()), this.generateDmEmbed(message, hl)] + }) + .then(() => true) + .catch(() => false); + /* eslint-enable @typescript-eslint/no-base-to-string */ + } + + private generateDmEmbed(message: Message, hl: HighlightWord) { + const recentMessages = message.channel.messages.cache + .filter((m) => m.createdTimestamp <= message.createdTimestamp && m.id !== message.id) + .filter((m) => m.cleanContent?.trim().length > 0) + .sort((a, b) => b.createdTimestamp - a.createdTimestamp) + .first(4) + .reverse(); + + return { + description: [ + // eslint-disable-next-line @typescript-eslint/no-base-to-string + message.channel!.toString(), + ...[...recentMessages, message].map( + (m) => `${timestamp(m.createdAt, 't')} ${format.input(`${m.author.tag}:`)} ${m.cleanContent.trim().substring(0, 512)}` + ) + ].join('\n'), + author: { name: hl.regex ? `/${hl.word}/gi` : hl.word }, + fields: [{ name: 'Source message', value: `[Jump to message](${message.url})` }], + color: colors.default, + footer: { text: `Triggered in ${sanitizeInputForDiscord(`${message.guild}`)}` }, + timestamp: message.createdAt.toISOString() + }; + } + + /** + * Updates the time that a user last talked in a particular guild. + * @param message The message the user sent. + */ + public updateLastTalked(message: Message): void { + if (!message.inGuild()) return; + const lastTalked = ( + this.userLastTalkedCooldown.has(message.guildId) + ? this.userLastTalkedCooldown + : this.userLastTalkedCooldown.set(message.guildId, new Collection()) + ).get(message.guildId)!; + + lastTalked.set(message.author.id, new Date()); + if (!HighlightManager.keep.has(message.author.id)) HighlightManager.keep.add(message.author.id); + } +} + +export enum HighlightBlockResult { + ALREADY_BLOCKED, + ERROR, + SUCCESS +} + +export enum HighlightUnblockResult { + NOT_BLOCKED, + ERROR, + SUCCESS +} diff --git a/lib/common/Moderation.ts b/lib/common/Moderation.ts new file mode 100644 index 0000000..60e32c0 --- /dev/null +++ b/lib/common/Moderation.ts @@ -0,0 +1,556 @@ +import { + ActivePunishment, + ActivePunishmentType, + baseMuteResponse, + colors, + emojis, + format, + Guild as GuildDB, + humanizeDuration, + ModLog, + permissionsResponse, + type ModLogType, + type ValueOf +} from '#lib'; +import assert from 'assert/strict'; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + Client, + EmbedBuilder, + PermissionFlagsBits, + type Guild, + type GuildMember, + type GuildMemberResolvable, + type GuildResolvable, + type Snowflake, + type UserResolvable +} from 'discord.js'; + +enum punishMap { + 'warned' = 'warn', + 'muted' = 'mute', + 'unmuted' = 'unmute', + 'kicked' = 'kick', + 'banned' = 'ban', + 'unbanned' = 'unban', + 'timedout' = 'timeout', + 'untimedout' = 'untimeout', + 'blocked' = 'block', + 'unblocked' = 'unblock' +} +enum reversedPunishMap { + 'warn' = 'warned', + 'mute' = 'muted', + 'unmute' = 'unmuted', + 'kick' = 'kicked', + 'ban' = 'banned', + 'unban' = 'unbanned', + 'timeout' = 'timedout', + 'untimeout' = 'untimedout', + 'block' = 'blocked', + 'unblock' = 'unblocked' +} + +/** + * Checks if a moderator can perform a moderation action on another user. + * @param moderator The person trying to perform the action. + * @param victim The person getting punished. + * @param type The type of punishment - used to format the response. + * @param checkModerator Whether or not to check if the victim is a moderator. + * @param force Override permissions checks. + * @returns `true` if the moderator can perform the action otherwise a reason why they can't. + */ +export async function permissionCheck( + moderator: GuildMember, + victim: GuildMember, + type: + | 'mute' + | 'unmute' + | 'warn' + | 'kick' + | 'ban' + | 'unban' + | 'add a punishment role to' + | 'remove a punishment role from' + | 'block' + | 'unblock' + | 'timeout' + | 'untimeout', + checkModerator = true, + force = false +): Promise { + if (force) return true; + + // If the victim is not in the guild anymore it will be undefined + if ((!victim || !victim.guild) && !['ban', 'unban'].includes(type)) return true; + + if (moderator.guild.id !== victim.guild.id) { + throw new Error('moderator and victim not in same guild'); + } + + const isOwner = moderator.guild.ownerId === moderator.id; + if (moderator.id === victim.id && !type.startsWith('un')) { + return `${emojis.error} You cannot ${type} yourself.`; + } + if ( + moderator.roles.highest.position <= victim.roles.highest.position && + !isOwner && + !(type.startsWith('un') && moderator.id === victim.id) + ) { + return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`; + } + if ( + victim.roles.highest.position >= victim.guild.members.me!.roles.highest.position && + !(type.startsWith('un') && moderator.id === victim.id) + ) { + return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`; + } + if ( + checkModerator && + victim.permissions.has(PermissionFlagsBits.ManageMessages) && + !(type.startsWith('un') && moderator.id === victim.id) + ) { + if (await moderator.guild.hasFeature('modsCanPunishMods')) { + return true; + } else { + return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`; + } + } + return true; +} + +/** + * Performs permission checks that are required in order to (un)mute a member. + * @param guild The guild to check the mute permissions in. + * @returns A {@link MuteResponse} or true if nothing failed. + */ +export async function checkMutePermissions( + guild: Guild +): Promise | ValueOf | true> { + if (!guild.members.me!.permissions.has('ManageRoles')) return permissionsResponse.MISSING_PERMISSIONS; + const muteRoleID = await guild.getSetting('muteRole'); + if (!muteRoleID) return baseMuteResponse.NO_MUTE_ROLE; + const muteRole = guild.roles.cache.get(muteRoleID); + if (!muteRole) return baseMuteResponse.MUTE_ROLE_INVALID; + if (muteRole.position >= guild.members.me!.roles.highest.position || muteRole.managed) + return baseMuteResponse.MUTE_ROLE_NOT_MANAGEABLE; + + return true; +} + +/** + * Creates a modlog entry for a punishment. + * @param options Options for creating a modlog entry. + * @param getCaseNumber Whether or not to get the case number of the entry. + * @returns An object with the modlog and the case number. + */ +export async function createModLogEntry( + options: CreateModLogEntryOptions, + getCaseNumber = false +): Promise<{ log: ModLog | null; caseNum: number | null }> { + const user = (await options.client.utils.resolveNonCachedUser(options.user))!.id; + const moderator = (await options.client.utils.resolveNonCachedUser(options.moderator))!.id; + const guild = options.client.guilds.resolveId(options.guild)!; + + return createModLogEntrySimple( + { + ...options, + user: user, + moderator: moderator, + guild: guild + }, + getCaseNumber + ); +} + +/** + * Creates a modlog entry with already resolved ids. + * @param options Options for creating a modlog entry. + * @param getCaseNumber Whether or not to get the case number of the entry. + * @returns An object with the modlog and the case number. + */ +export async function createModLogEntrySimple( + options: SimpleCreateModLogEntryOptions, + getCaseNumber = false +): Promise<{ log: ModLog | null; caseNum: number | null }> { + // If guild does not exist create it so the modlog can reference a guild. + await GuildDB.findOrCreate({ + where: { id: options.guild }, + defaults: { id: options.guild } + }); + + const modLogEntry = ModLog.build({ + type: options.type, + user: options.user, + moderator: options.moderator, + reason: options.reason, + duration: options.duration ? options.duration : undefined, + guild: options.guild, + pseudo: options.pseudo ?? false, + evidence: options.evidence, + hidden: options.hidden ?? false + }); + const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => { + await options.client.utils.handleError('createModLogEntry', e); + return null; + }); + + if (!getCaseNumber) return { log: saveResult, caseNum: null }; + + const caseNum = ( + await ModLog.findAll({ where: { type: options.type, user: options.user, guild: options.guild, hidden: false } }) + )?.length; + return { log: saveResult, caseNum }; +} + +/** + * Creates a punishment entry. + * @param options Options for creating the punishment entry. + * @returns The database entry, or null if no entry is created. + */ +export async function createPunishmentEntry(options: CreatePunishmentEntryOptions): Promise { + const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined; + const user = (await options.client.utils.resolveNonCachedUser(options.user))!.id; + const guild = options.client.guilds.resolveId(options.guild)!; + const type = findTypeEnum(options.type)!; + + const entry = ActivePunishment.build( + options.extraInfo + ? { user, type, guild, expires, modlog: options.modlog, extraInfo: options.extraInfo } + : { user, type, guild, expires, modlog: options.modlog } + ); + return await entry.save().catch(async (e) => { + await options.client.utils.handleError('createPunishmentEntry', e); + return null; + }); +} + +/** + * Destroys a punishment entry. + * @param options Options for destroying the punishment entry. + * @returns Whether or not the entry was destroyed. + */ +export async function removePunishmentEntry(options: RemovePunishmentEntryOptions): Promise { + const user = await options.client.utils.resolveNonCachedUser(options.user); + const guild = options.client.guilds.resolveId(options.guild); + const type = findTypeEnum(options.type); + + if (!user || !guild) return false; + + let success = true; + + const entries = await ActivePunishment.findAll({ + // finding all cases of a certain type incase there were duplicates or something + where: options.extraInfo + ? { user: user.id, guild: guild, type, extraInfo: options.extraInfo } + : { user: user.id, guild: guild, type } + }).catch(async (e) => { + await options.client.utils.handleError('removePunishmentEntry', e); + success = false; + }); + if (entries) { + const promises = entries.map(async (entry) => + entry.destroy().catch(async (e) => { + await options.client.utils.handleError('removePunishmentEntry', e); + success = false; + }) + ); + + await Promise.all(promises); + } + return success; +} + +/** + * Returns the punishment type enum for the given type. + * @param type The type of the punishment. + * @returns The punishment type enum. + */ +function findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') { + const typeMap = { + ['mute']: ActivePunishmentType.MUTE, + ['ban']: ActivePunishmentType.BAN, + ['role']: ActivePunishmentType.ROLE, + ['block']: ActivePunishmentType.BLOCK + }; + return typeMap[type]; +} + +export function punishmentToPresentTense(punishment: PunishmentTypeDM): PunishmentTypePresent { + return punishMap[punishment]; +} + +export function punishmentToPastTense(punishment: PunishmentTypePresent): PunishmentTypeDM { + return reversedPunishMap[punishment]; +} + +/** + * Notifies the specified user of their punishment. + * @param options Options for notifying the user. + * @returns Whether or not the dm was successfully sent. + */ +export async function punishDM(options: PunishDMOptions): Promise { + const ending = await options.guild.getSetting('punishmentEnding'); + const dmEmbed = + ending && ending.length && options.sendFooter + ? new EmbedBuilder().setDescription(ending).setColor(colors.newBlurple) + : undefined; + + const appealsEnabled = !!( + (await options.guild.hasFeature('punishmentAppeals')) && (await options.guild.getLogChannel('appeals')) + ); + + let content = `You have been ${options.punishment} `; + if (options.punishment.includes('blocked')) { + assert(options.channel); + content += `from <#${options.channel}> `; + } + content += `in ${format.input(options.guild.name)} `; + if (options.duration !== null && options.duration !== undefined) + content += options.duration ? `for ${humanizeDuration(options.duration)} ` : 'permanently '; + const reason = options.reason?.trim() ? options.reason?.trim() : 'No reason provided'; + content += `for ${format.input(reason)}.`; + + let components; + if (appealsEnabled && options.modlog) + components = [ + new ActionRowBuilder({ + components: [ + new ButtonBuilder({ + customId: `appeal;${punishmentToPresentTense(options.punishment)};${ + options.guild.id + };${options.client.users.resolveId(options.user)};${options.modlog}`, + style: ButtonStyle.Primary, + label: 'Appeal' + }).toJSON() + ] + }) + ]; + + const dmSuccess = await options.client.users + .send(options.user, { + content, + embeds: dmEmbed ? [dmEmbed] : undefined, + components + }) + .catch(() => false); + return !!dmSuccess; +} + +interface BaseCreateModLogEntryOptions extends BaseOptions { + /** + * The type of modlog entry. + */ + type: ModLogType; + + /** + * The reason for the punishment. + */ + reason: string | undefined | null; + + /** + * The duration of the punishment. + */ + duration?: number; + + /** + * Whether the punishment is a pseudo punishment. + */ + pseudo?: boolean; + + /** + * The evidence for the punishment. + */ + evidence?: string; + + /** + * Makes the modlog entry hidden. + */ + hidden?: boolean; +} + +/** + * Options for creating a modlog entry. + */ +export interface CreateModLogEntryOptions extends BaseCreateModLogEntryOptions { + /** + * The client. + */ + client: Client; + + /** + * The user that a modlog entry is created for. + */ + user: GuildMemberResolvable; + + /** + * The moderator that created the modlog entry. + */ + moderator: GuildMemberResolvable; + + /** + * The guild that the punishment is created for. + */ + guild: GuildResolvable; +} + +/** + * Simple options for creating a modlog entry. + */ +export interface SimpleCreateModLogEntryOptions extends BaseCreateModLogEntryOptions { + /** + * The user that a modlog entry is created for. + */ + user: Snowflake; + + /** + * The moderator that created the modlog entry. + */ + moderator: Snowflake; + + /** + * The guild that the punishment is created for. + */ + guild: Snowflake; +} + +/** + * Options for creating a punishment entry. + */ +export interface CreatePunishmentEntryOptions extends BaseOptions { + /** + * The type of punishment. + */ + type: 'mute' | 'ban' | 'role' | 'block'; + + /** + * The user that the punishment is created for. + */ + user: GuildMemberResolvable; + + /** + * The length of time the punishment lasts for. + */ + duration: number | undefined; + + /** + * The guild that the punishment is created for. + */ + guild: GuildResolvable; + + /** + * The id of the modlog that is linked to the punishment entry. + */ + modlog: string; + + /** + * Extra information for the punishment. The role for role punishments and the channel for blocks. + */ + extraInfo?: Snowflake; +} + +/** + * Options for removing a punishment entry. + */ +export interface RemovePunishmentEntryOptions extends BaseOptions { + /** + * The type of punishment. + */ + type: 'mute' | 'ban' | 'role' | 'block'; + + /** + * The user that the punishment is destroyed for. + */ + user: GuildMemberResolvable; + + /** + * The guild that the punishment was in. + */ + guild: GuildResolvable; + + /** + * Extra information for the punishment. The role for role punishments and the channel for blocks. + */ + extraInfo?: Snowflake; +} + +/** + * Options for sending a user a punishment dm. + */ +export interface PunishDMOptions extends BaseOptions { + /** + * The modlog case id so the user can make an appeal. + */ + modlog?: string; + + /** + * The guild that the punishment is taking place in. + */ + guild: Guild; + + /** + * The user that is being punished. + */ + user: UserResolvable; + + /** + * The punishment that the user has received. + */ + punishment: PunishmentTypeDM; + + /** + * The reason the user's punishment. + */ + reason?: string; + + /** + * The duration of the punishment. + */ + duration?: number; + + /** + * Whether or not to send the guild's punishment footer with the dm. + * @default true + */ + sendFooter: boolean; + + /** + * The channel that the user was (un)blocked from. + */ + channel?: Snowflake; +} + +interface BaseOptions { + /** + * The client. + */ + client: Client; +} + +export type PunishmentTypeDM = + | 'warned' + | 'muted' + | 'unmuted' + | 'kicked' + | 'banned' + | 'unbanned' + | 'timedout' + | 'untimedout' + | 'blocked' + | 'unblocked'; + +export type PunishmentTypePresent = + | 'warn' + | 'mute' + | 'unmute' + | 'kick' + | 'ban' + | 'unban' + | 'timeout' + | 'untimeout' + | 'block' + | 'unblock'; + +export type AppealButtonId = `appeal;${PunishmentTypePresent};${Snowflake};${Snowflake};${string}`; diff --git a/lib/common/Sentry.ts b/lib/common/Sentry.ts new file mode 100644 index 0000000..446ec27 --- /dev/null +++ b/lib/common/Sentry.ts @@ -0,0 +1,24 @@ +import { RewriteFrames } from '@sentry/integrations'; +import * as SentryNode from '@sentry/node'; +import { Integrations } from '@sentry/node'; +import type { Config } from '../../config/Config.js'; + +export class Sentry { + public constructor(rootdir: string, config: Config) { + if (config.credentials.sentryDsn === null) throw TypeError('sentryDsn cannot be null'); + + SentryNode.init({ + dsn: config.credentials.sentryDsn, + environment: config.environment, + tracesSampleRate: 1.0, + integrations: [ + new RewriteFrames({ + root: rootdir + }), + new Integrations.OnUnhandledRejection({ + mode: 'none' + }) + ] + }); + } +} diff --git a/lib/common/tags.ts b/lib/common/tags.ts new file mode 100644 index 0000000..098cf29 --- /dev/null +++ b/lib/common/tags.ts @@ -0,0 +1,34 @@ +/* these functions are adapted from the common-tags npm package which is licensed under the MIT license */ +/* the js docs are adapted from the @types/common-tags npm package which is licensed under the MIT license */ + +/** + * Strips the **initial** indentation from the beginning of each line in a multiline string. + */ +export function stripIndent(strings: TemplateStringsArray, ...expressions: any[]) { + const str = format(strings, ...expressions); + // remove the shortest leading indentation from each line + const match = str.match(/^[^\S\n]*(?=\S)/gm); + const indent = match && Math.min(...match.map((el) => el.length)); + if (indent) { + const regexp = new RegExp(`^.{${indent}}`, 'gm'); + return str.replace(regexp, ''); + } + return str; +} + +/** + * Strips **all** of the indentation from the beginning of each line in a multiline string. + */ +export function stripIndents(strings: TemplateStringsArray, ...expressions: any[]) { + const str = format(strings, ...expressions); + // remove all indentation from each line + return str.replace(/^[^\S\n]+/gm, ''); +} + +function format(strings: TemplateStringsArray, ...expressions: any[]) { + const str = strings + .reduce((result, string, index) => ''.concat(result, expressions[index - 1], string)) + .replace(/[^\S\n]+$/gm, '') + .replace(/^\n/, ''); + return str; +} diff --git a/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts b/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts new file mode 100644 index 0000000..def7ad6 --- /dev/null +++ b/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts @@ -0,0 +1,3 @@ +import { type CommandMessage } from '#lib'; + +export type BushArgumentTypeCaster = (message: CommandMessage, phrase: string) => R; diff --git a/lib/extensions/discord-akairo/BushClient.ts b/lib/extensions/discord-akairo/BushClient.ts new file mode 100644 index 0000000..1a6bb8c --- /dev/null +++ b/lib/extensions/discord-akairo/BushClient.ts @@ -0,0 +1,600 @@ +import { + abbreviatedNumber, + contentWithDuration, + discordEmoji, + duration, + durationSeconds, + globalUser, + messageLink, + permission, + roleWithDuration, + snowflake +} from '#args'; +import { BushClientEvents, emojis, formatError, inspect } from '#lib'; +import { patch, type PatchedElements } from '@notenoughupdates/events-intercept'; +import * as Sentry from '@sentry/node'; +import { + AkairoClient, + ArgumentTypeCaster, + ContextMenuCommandHandler, + version as akairoVersion, + type ArgumentPromptData, + type OtherwiseContentSupplier +} from 'discord-akairo'; +import { + ActivityType, + GatewayIntentBits, + MessagePayload, + Options, + Partials, + Structures, + version as discordJsVersion, + type Awaitable, + type If, + type InteractionReplyOptions, + type Message, + type MessageEditOptions, + type MessageOptions, + type ReplyMessageOptions, + type Snowflake, + type UserResolvable, + type WebhookEditMessageOptions +} from 'discord.js'; +import type EventEmitter from 'events'; +import { google } from 'googleapis'; +import path from 'path'; +import readline from 'readline'; +import type { Options as SequelizeOptions, Sequelize as SequelizeType } from 'sequelize'; +import { fileURLToPath } from 'url'; +import type { Config } from '../../../config/Config.js'; +import UpdateCacheTask from '../../../src/tasks/cache/updateCache.js'; +import UpdateStatsTask from '../../../src/tasks/feature/updateStats.js'; +import { tinyColor } from '../../arguments/tinyColor.js'; +import { BushCache } from '../../common/BushCache.js'; +import { HighlightManager } from '../../common/HighlightManager.js'; +import { ActivePunishment } from '../../models/instance/ActivePunishment.js'; +import { Guild as GuildDB } from '../../models/instance/Guild.js'; +import { Highlight } from '../../models/instance/Highlight.js'; +import { Level } from '../../models/instance/Level.js'; +import { ModLog } from '../../models/instance/ModLog.js'; +import { Reminder } from '../../models/instance/Reminder.js'; +import { StickyRole } from '../../models/instance/StickyRole.js'; +import { Global } from '../../models/shared/Global.js'; +import { GuildCount } from '../../models/shared/GuildCount.js'; +import { MemberCount } from '../../models/shared/MemberCount.js'; +import { Shared } from '../../models/shared/Shared.js'; +import { Stat } from '../../models/shared/Stat.js'; +import { AllowedMentions } from '../../utils/AllowedMentions.js'; +import { BushClientUtils } from '../../utils/BushClientUtils.js'; +import { BushLogger } from '../../utils/BushLogger.js'; +import { ExtendedGuild } from '../discord.js/ExtendedGuild.js'; +import { ExtendedGuildMember } from '../discord.js/ExtendedGuildMember.js'; +import { ExtendedMessage } from '../discord.js/ExtendedMessage.js'; +import { ExtendedUser } from '../discord.js/ExtendedUser.js'; +import { BushCommandHandler } from './BushCommandHandler.js'; +import { BushInhibitorHandler } from './BushInhibitorHandler.js'; +import { BushListenerHandler } from './BushListenerHandler.js'; +import { BushTaskHandler } from './BushTaskHandler.js'; +const { Sequelize } = (await import('sequelize')).default; + +declare module 'discord.js' { + export interface Client extends EventEmitter { + /** The ID of the owner(s). */ + ownerID: Snowflake | Snowflake[]; + /** The ID of the superUser(s). */ + superUserID: Snowflake | Snowflake[]; + /** Whether or not the client is ready. */ + customReady: boolean; + /** The configuration for the client. */ + readonly config: Config; + /** Stats for the client. */ + readonly stats: BushStats; + /** The handler for the bot's listeners. */ + readonly listenerHandler: BushListenerHandler; + /** The handler for the bot's command inhibitors. */ + readonly inhibitorHandler: BushInhibitorHandler; + /** The handler for the bot's commands. */ + readonly commandHandler: BushCommandHandler; + /** The handler for the bot's tasks. */ + readonly taskHandler: BushTaskHandler; + /** The handler for the bot's context menu commands. */ + readonly contextMenuCommandHandler: ContextMenuCommandHandler; + /** The database connection for this instance of the bot (production, beta, or development). */ + readonly instanceDB: SequelizeType; + /** The database connection that is shared between all instances of the bot. */ + readonly sharedDB: SequelizeType; + /** A custom logging system for the bot. */ + readonly logger: BushLogger; + /** Cached global and guild database data. */ + readonly cache: BushCache; + /** Sentry error reporting for the bot. */ + readonly sentry: typeof Sentry; + /** Manages most aspects of the highlight command */ + readonly highlightManager: HighlightManager; + /** The perspective api */ + perspective: any; + /** Client utilities. */ + readonly utils: BushClientUtils; + /** A custom logging system for the bot. */ + get console(): BushLogger; + on(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; + once(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; + emit(event: K, ...args: BushClientEvents[K]): boolean; + off(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; + removeAllListeners(event?: K): this; + /** + * Checks if a user is the owner of this bot. + * @param user - User to check. + */ + isOwner(user: UserResolvable): boolean; + /** + * Checks if a user is a super user of this bot. + * @param user - User to check. + */ + isSuperUser(user: UserResolvable): boolean; + } +} + +export type ReplyMessageType = string | MessagePayload | ReplyMessageOptions; +export type EditMessageType = string | MessageEditOptions | MessagePayload; +export type SlashSendMessageType = string | MessagePayload | InteractionReplyOptions; +export type SlashEditMessageType = string | MessagePayload | WebhookEditMessageOptions; +export type SendMessageType = string | MessagePayload | MessageOptions; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false +}); + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +/** + * The main hub for interacting with the Discord API. + */ +export class BushClient extends AkairoClient { + public declare ownerID: Snowflake[]; + public declare superUserID: Snowflake[]; + + /** + * Whether or not the client is ready. + */ + public override customReady = false; + + /** + * Stats for the client. + */ + public override readonly stats: BushStats = { cpu: undefined, commandsUsed: 0n, slashCommandsUsed: 0n }; + + /** + * The handler for the bot's listeners. + */ + public override readonly listenerHandler: BushListenerHandler; + + /** + * The handler for the bot's command inhibitors. + */ + public override readonly inhibitorHandler: BushInhibitorHandler; + + /** + * The handler for the bot's commands. + */ + public override readonly commandHandler: BushCommandHandler; + + /** + * The handler for the bot's tasks. + */ + public override readonly taskHandler: BushTaskHandler; + + /** + * The handler for the bot's context menu commands. + */ + public override readonly contextMenuCommandHandler: ContextMenuCommandHandler; + + /** + * The database connection for this instance of the bot (production, beta, or development). + */ + public override readonly instanceDB: SequelizeType; + + /** + * The database connection that is shared between all instances of the bot. + */ + public override readonly sharedDB: SequelizeType; + + /** + * A custom logging system for the bot. + */ + public override readonly logger: BushLogger = new BushLogger(this); + + /** + * Cached global and guild database data. + */ + public override readonly cache = new BushCache(); + + /** + * Sentry error reporting for the bot. + */ + public override readonly sentry!: typeof Sentry; + + /** + * Manages most aspects of the highlight command + */ + public override readonly highlightManager: HighlightManager = new HighlightManager(this); + + /** + * The perspective api + */ + public override perspective: any; + + /** + * Client utilities. + */ + public override readonly utils: BushClientUtils = new BushClientUtils(this); + + /** + * @param config The configuration for the client. + */ + public constructor( + /** + * The configuration for the client. + */ + public override readonly config: Config + ) { + super({ + ownerID: config.owners, + intents: Object.keys(GatewayIntentBits) + .map((i) => (typeof i === 'string' ? GatewayIntentBits[i as keyof typeof GatewayIntentBits] : i)) + .reduce((acc, p) => acc | p, 0), + partials: Object.keys(Partials).map((p) => Partials[p as keyof typeof Partials]), + presence: { + activities: [{ name: 'Beep Boop', type: ActivityType.Watching }], + status: 'online' + }, + allowedMentions: AllowedMentions.none(), // no mentions by default + makeCache: Options.cacheWithLimits({ + PresenceManager: { + maxSize: 0, + keepOverLimit: (_, key) => { + if (config.owners.includes(key)) return true; + + return HighlightManager.keep.has(key); + } + } + }), + failIfNotExists: false, + rest: { api: 'https://canary.discord.com/api' } + }); + patch(this); + + this.token = config.token as If; + + /* =-=-= handlers =-=-= */ + this.listenerHandler = new BushListenerHandler(this, { + directory: path.join(__dirname, '..', '..', '..', 'src', 'listeners'), + extensions: ['.js'], + automateCategories: true + }); + this.inhibitorHandler = new BushInhibitorHandler(this, { + directory: path.join(__dirname, '..', '..', '..', 'src', 'inhibitors'), + extensions: ['.js'], + automateCategories: true + }); + this.taskHandler = new BushTaskHandler(this, { + directory: path.join(__dirname, '..', '..', '..', 'src', 'tasks'), + extensions: ['.js'], + automateCategories: true + }); + + const modify = async ( + message: Message, + text: string | MessagePayload | MessageOptions | OtherwiseContentSupplier, + data: ArgumentPromptData, + replaceError: boolean + ) => { + const ending = '\n\n Type **cancel** to cancel the command'; + const options = typeof text === 'function' ? await text(message, data) : text; + const search = '{error}', + replace = emojis.error; + + if (typeof options === 'string') return (replaceError ? options.replace(search, replace) : options) + ending; + + if (options instanceof MessagePayload) { + if (options.options.content) { + if (replaceError) options.options.content = options.options.content.replace(search, replace); + options.options.content += ending; + } + } else if (options.content) { + if (replaceError) options.content = options.content.replace(search, replace); + options.content += ending; + } + return options; + }; + + this.commandHandler = new BushCommandHandler(this, { + directory: path.join(__dirname, '..', '..', '..', 'src', 'commands'), + extensions: ['.js'], + prefix: async ({ guild }: Message) => { + if (this.config.isDevelopment) return 'dev '; + if (!guild) return this.config.prefix; + const prefix = await guild.getSetting('prefix'); + return (prefix ?? this.config.prefix) as string; + }, + allowMention: true, + handleEdits: true, + commandUtil: true, + commandUtilLifetime: 300_000, // 5 minutes + argumentDefaults: { + prompt: { + start: 'Placeholder argument prompt. **If you see this please tell my developers**.', + retry: 'Placeholder failed argument prompt. **If you see this please tell my developers**.', + modifyStart: (message, text, data) => modify(message, text, data, false), + modifyRetry: (message, text, data) => modify(message, text, data, true), + timeout: ':hourglass: You took too long the command has been cancelled.', + ended: 'You exceeded the maximum amount of tries the command has been cancelled', + cancel: 'The command has been cancelled', + retries: 3, + time: 3e4 + }, + otherwise: '' + }, + automateCategories: false, + autoRegisterSlashCommands: true, + skipBuiltInPostInhibitors: true, + aliasReplacement: /-/g + }); + this.contextMenuCommandHandler = new ContextMenuCommandHandler(this, { + directory: path.join(__dirname, '..', '..', '..', 'src', 'context-menu-commands'), + extensions: ['.js'], + automateCategories: true + }); + + /* =-=-= databases =-=-= */ + const sharedDBOptions: SequelizeOptions = { + username: this.config.db.username, + password: this.config.db.password, + dialect: 'postgres', + host: this.config.db.host, + port: this.config.db.port, + logging: this.config.logging.db ? (sql) => this.logger.debug(sql) : false, + timezone: 'America/New_York' + }; + this.instanceDB = new Sequelize({ + ...sharedDBOptions, + database: this.config.isDevelopment ? 'bushbot-dev' : this.config.isBeta ? 'bushbot-beta' : 'bushbot' + }); + this.sharedDB = new Sequelize({ + ...sharedDBOptions, + database: 'bushbot-shared' + }); + + this.sentry = Sentry; + } + + /** + * A custom logging system for the bot. + */ + public override get console(): BushLogger { + return this.logger; + } + + /** + * Extends discord.js structures before the client is instantiated. + */ + public static extendStructures(): void { + Structures.extend('GuildMember', () => ExtendedGuildMember); + Structures.extend('Guild', () => ExtendedGuild); + Structures.extend('Message', () => ExtendedMessage); + Structures.extend('User', () => ExtendedUser); + } + + /** + * Initializes the bot. + */ + public async init() { + if (parseInt(process.versions.node.split('.')[0]) < 17) { + void (await this.console.error('version', `Please use node <>, not <<${process.version}>>.`, false)); + process.exit(2); + } + + this.setMaxListeners(20); + + this.perspective = await google.discoverAPI('https://commentanalyzer.googleapis.com/$discovery/rest?version=v1alpha1'); + + this.commandHandler.useInhibitorHandler(this.inhibitorHandler); + this.commandHandler.useListenerHandler(this.listenerHandler); + this.commandHandler.useTaskHandler(this.taskHandler); + this.commandHandler.useContextMenuCommandHandler(this.contextMenuCommandHandler); + this.commandHandler.ignorePermissions = this.config.owners; + this.commandHandler.ignoreCooldown = [...new Set([...this.config.owners, ...this.cache.shared.superUsers])]; + const emitters: Emitters = { + client: this, + commandHandler: this.commandHandler, + inhibitorHandler: this.inhibitorHandler, + listenerHandler: this.listenerHandler, + taskHandler: this.taskHandler, + contextMenuCommandHandler: this.contextMenuCommandHandler, + process, + stdin: rl, + gateway: this.ws, + rest: this.rest, + ws: this.ws + }; + this.listenerHandler.setEmitters(emitters); + this.commandHandler.resolver.addTypes({ + duration: duration, + contentWithDuration: contentWithDuration, + permission: permission, + snowflake: snowflake, + discordEmoji: discordEmoji, + roleWithDuration: roleWithDuration, + abbreviatedNumber: abbreviatedNumber, + durationSeconds: durationSeconds, + globalUser: globalUser, + messageLink: messageLink, + tinyColor: tinyColor + }); + + this.sentry.setTag('process', process.pid.toString()); + this.sentry.setTag('discord.js', discordJsVersion); + this.sentry.setTag('discord-akairo', akairoVersion); + void this.logger.success('startup', `Successfully connected to <>.`, false); + + // loads all the handlers + const handlers = { + commands: this.commandHandler, + contextMenuCommands: this.contextMenuCommandHandler, + listeners: this.listenerHandler, + inhibitors: this.inhibitorHandler, + tasks: this.taskHandler + }; + const handlerPromises = Object.entries(handlers).map(([handlerName, handler]) => + handler + .loadAll() + .then(() => { + void this.logger.success('startup', `Successfully loaded <<${handlerName}>>.`, false); + }) + .catch((e) => { + void this.logger.error('startup', `Unable to load loader <<${handlerName}>> with error:\n${formatError(e)}`, false); + if (process.argv.includes('dry')) process.exit(1); + }) + ); + await Promise.allSettled(handlerPromises); + } + + /** + * Connects to the database, initializes models, and creates tables if they do not exist. + */ + public async dbPreInit() { + try { + await this.instanceDB.authenticate(); + GuildDB.initModel(this.instanceDB, this); + ModLog.initModel(this.instanceDB); + ActivePunishment.initModel(this.instanceDB); + Level.initModel(this.instanceDB); + StickyRole.initModel(this.instanceDB); + Reminder.initModel(this.instanceDB); + Highlight.initModel(this.instanceDB); + await this.instanceDB.sync({ alter: true }); // Sync all tables to fix everything if updated + await this.console.success('startup', `Successfully connected to <>.`, false); + } catch (e) { + await this.console.error( + 'startup', + `Failed to connect to <> with error:\n${inspect(e, { colors: true, depth: 1 })}`, + false + ); + process.exit(2); + } + try { + await this.sharedDB.authenticate(); + Stat.initModel(this.sharedDB); + Global.initModel(this.sharedDB); + Shared.initModel(this.sharedDB); + MemberCount.initModel(this.sharedDB); + GuildCount.initModel(this.sharedDB); + await this.sharedDB.sync({ + // Sync all tables to fix everything if updated + // if another instance restarts we don't want to overwrite new changes made in development + alter: this.config.isDevelopment + }); + await this.console.success('startup', `Successfully connected to <>.`, false); + } catch (e) { + await this.console.error( + 'startup', + `Failed to connect to <> with error:\n${inspect(e, { colors: true, depth: 1 })}`, + false + ); + process.exit(2); + } + } + + /** + * Starts the bot + */ + public async start() { + this.intercept('ready', async (arg, done) => { + const promises = this.guilds.cache + .filter((g) => g.large) + .map((guild) => { + return guild.members.fetch(); + }); + await Promise.all(promises); + this.customReady = true; + this.taskHandler.startAll(); + return done(null, `intercepted ${arg}`); + }); + + try { + await this.highlightManager.syncCache(); + await UpdateCacheTask.init(this); + void this.console.success('startup', `Successfully created <>.`, false); + const stats = await UpdateStatsTask.init(this); + this.stats.commandsUsed = stats.commandsUsed; + this.stats.slashCommandsUsed = stats.slashCommandsUsed; + await this.login(this.token!); + } catch (e) { + await this.console.error('start', inspect(e, { colors: true, depth: 1 }), false); + process.exit(1); + } + } + + /** + * Logs out, terminates the connection to Discord, and destroys the client. + */ + public override destroy(relogin = false): void | Promise { + super.destroy(); + if (relogin) { + return this.login(this.token!); + } + } + + public override isOwner(user: UserResolvable): boolean { + return this.config.owners.includes(this.users.resolveId(user!)!); + } + + public override isSuperUser(user: UserResolvable): boolean { + const userID = this.users.resolveId(user)!; + return this.cache.shared.superUsers.includes(userID) || this.config.owners.includes(userID); + } +} + +export interface BushClient extends EventEmitter, PatchedElements, AkairoClient { + on(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; + once(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; + emit(event: K, ...args: BushClientEvents[K]): boolean; + off(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; + removeAllListeners(event?: K): this; +} + +/** + * Various statistics + */ +export interface BushStats { + /** + * The average cpu usage of the bot from the past 60 seconds. + */ + cpu: number | undefined; + + /** + * The total number of times any command has been used. + */ + commandsUsed: bigint; + + /** + * The total number of times any slash command has been used. + */ + slashCommandsUsed: bigint; +} + +export interface Emitters { + client: BushClient; + commandHandler: BushClient['commandHandler']; + inhibitorHandler: BushClient['inhibitorHandler']; + listenerHandler: BushClient['listenerHandler']; + taskHandler: BushClient['taskHandler']; + contextMenuCommandHandler: BushClient['contextMenuCommandHandler']; + process: NodeJS.Process; + stdin: readline.Interface; + gateway: BushClient['ws']; + rest: BushClient['rest']; + ws: BushClient['ws']; +} diff --git a/lib/extensions/discord-akairo/BushCommand.ts b/lib/extensions/discord-akairo/BushCommand.ts new file mode 100644 index 0000000..dc2295f --- /dev/null +++ b/lib/extensions/discord-akairo/BushCommand.ts @@ -0,0 +1,586 @@ +import { type DiscordEmojiInfo, type RoleWithDuration } from '#args'; +import { + type BushArgumentTypeCaster, + type BushClient, + type BushCommandHandler, + type BushInhibitor, + type BushListener, + type BushTask, + type ParsedDuration +} from '#lib'; +import { + ArgumentMatch, + Command, + CommandUtil, + type AkairoApplicationCommandAutocompleteOption, + type AkairoApplicationCommandChannelOptionData, + type AkairoApplicationCommandChoicesData, + type AkairoApplicationCommandNonOptionsData, + type AkairoApplicationCommandNumericOptionData, + type AkairoApplicationCommandOptionData, + type AkairoApplicationCommandSubCommandData, + type AkairoApplicationCommandSubGroupData, + type ArgumentOptions, + type ArgumentType, + type ArgumentTypeCaster, + type BaseArgumentType, + type CommandOptions, + type ContextMenuCommand, + type MissingPermissionSupplier, + type SlashOption, + type SlashResolveType +} from 'discord-akairo'; +import { + Message, + User, + type ApplicationCommandOptionChoiceData, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type ApplicationCommandOptionType, + type PermissionResolvable, + type PermissionsString, + type Snowflake +} from 'discord.js'; +import _ from 'lodash'; +import { SlashMessage } from './SlashMessage.js'; + +export interface OverriddenBaseArgumentType extends BaseArgumentType { + commandAlias: BushCommand | null; + command: BushCommand | null; + inhibitor: BushInhibitor | null; + listener: BushListener | null; + task: BushTask | null; + contextMenuCommand: ContextMenuCommand | null; +} + +export interface BaseBushArgumentType extends OverriddenBaseArgumentType { + duration: number | null; + contentWithDuration: ParsedDuration; + permission: PermissionsString | null; + snowflake: Snowflake | null; + discordEmoji: DiscordEmojiInfo | null; + roleWithDuration: RoleWithDuration | null; + abbreviatedNumber: number | null; + globalUser: User | null; + messageLink: Message | null; + durationSeconds: number | null; + tinyColor: string | null; +} + +export type BushArgumentType = keyof BaseBushArgumentType | RegExp; + +interface BaseBushArgumentOptions extends Omit, ExtraArgumentOptions { + id: string; + description: string; + + /** + * The message sent for the prompt and the slash command description. + */ + prompt?: string; + + /** + * The message set for the retry prompt. + */ + retry?: string; + + /** + * Whether or not the argument is optional. + */ + optional?: boolean; + + /** + * The type used for slash commands. Set to false to disable this argument for slash commands. + */ + slashType: AkairoApplicationCommandOptionData['type'] | false; + + /** + * Allows you to get a discord resolved object + * + * ex. get the resolved member object when the type is {@link ApplicationCommandOptionType.User User} + */ + slashResolve?: SlashResolveType; + + /** + * The choices of the option for the user to pick from + */ + choices?: ApplicationCommandOptionChoiceData[]; + + /** + * Whether the option is an autocomplete option + */ + autocomplete?: boolean; + + /** + * When the option type is channel, the allowed types of channels that can be selected + */ + channelTypes?: AkairoApplicationCommandChannelOptionData['channelTypes']; + + /** + * The minimum value for an {@link ApplicationCommandOptionType.Integer Integer} or {@link ApplicationCommandOptionType.Number Number} option + */ + minValue?: number; + + /** + * The maximum value for an {@link ApplicationCommandOptionType.Integer Integer} or {@link ApplicationCommandOptionType.Number Number} option + */ + maxValue?: number; +} + +interface ExtraArgumentOptions { + /** + * Restrict this argument to only slash or only text commands. + */ + only?: 'slash' | 'text'; + + /** + * Readable type for the help command. + */ + readableType?: string; + + /** + * Whether the argument is only accessible to the owners. + * @default false + */ + ownerOnly?: boolean; + + /** + * Whether the argument is only accessible to the super users. + * @default false + */ + superUserOnly?: boolean; +} + +export interface BushArgumentOptions extends BaseBushArgumentOptions { + /** + * The type that the argument should be cast to. + * - `string` does not cast to any type. + * - `lowercase` makes the input lowercase. + * - `uppercase` makes the input uppercase. + * - `charCodes` transforms the input to an array of char codes. + * - `number` casts to a number. + * - `integer` casts to an integer. + * - `bigint` casts to a big integer. + * - `url` casts to an `URL` object. + * - `date` casts to a `Date` object. + * - `color` casts a hex code to an integer. + * - `commandAlias` tries to resolve to a command from an alias. + * - `command` matches the ID of a command. + * - `inhibitor` matches the ID of an inhibitor. + * - `listener` matches the ID of a listener. + * + * Possible Discord-related types. + * These types can be plural (add an 's' to the end) and a collection of matching objects will be used. + * - `user` tries to resolve to a user. + * - `member` tries to resolve to a member. + * - `relevant` tries to resolve to a relevant user, works in both guilds and DMs. + * - `channel` tries to resolve to a channel. + * - `textChannel` tries to resolve to a text channel. + * - `voiceChannel` tries to resolve to a voice channel. + * - `stageChannel` tries to resolve to a stage channel. + * - `threadChannel` tries to resolve a thread channel. + * - `role` tries to resolve to a role. + * - `emoji` tries to resolve to a custom emoji. + * - `guild` tries to resolve to a guild. + * - `permission` tries to resolve to a permissions. + * + * Other Discord-related types: + * - `message` tries to fetch a message from an ID within the channel. + * - `guildMessage` tries to fetch a message from an ID within the guild. + * - `relevantMessage` is a combination of the above, works in both guilds and DMs. + * - `invite` tries to fetch an invite object from a link. + * - `userMention` matches a mention of a user. + * - `memberMention` matches a mention of a guild member. + * - `channelMention` matches a mention of a channel. + * - `roleMention` matches a mention of a role. + * - `emojiMention` matches a mention of an emoji. + * + * Misc: + * - `duration` tries to parse duration in milliseconds + * - `contentWithDuration` tries to parse duration in milliseconds and returns the remaining content with the duration + * removed + */ + type?: BushArgumentType | (keyof BaseBushArgumentType)[] | BushArgumentTypeCaster; +} + +export interface CustomBushArgumentOptions extends BaseBushArgumentOptions { + /** + * An array of strings can be used to restrict input to only those strings, case insensitive. + * The array can also contain an inner array of strings, for aliases. + * If so, the first entry of the array will be used as the final argument. + * + * A regular expression can also be used. + * The evaluated argument will be an object containing the `match` and `matches` if global. + */ + customType?: (string | string[])[] | RegExp | string | null; +} + +export type BushMissingPermissionSupplier = (message: CommandMessage | SlashMessage) => Promise | any; + +interface ExtendedCommandOptions { + /** + * Whether the command is hidden from the help command. + */ + hidden?: boolean; + + /** + * The channels the command is limited to run in. + */ + restrictedChannels?: Snowflake[]; + + /** + * The guilds the command is limited to run in. + */ + restrictedGuilds?: Snowflake[]; + + /** + * Show how to use the command. + */ + usage: string[]; + + /** + * Examples for how to use the command. + */ + examples: string[]; + + /** + * A fake command, completely hidden from the help command. + */ + pseudo?: boolean; + + /** + * Allow this command to be run in channels that are blacklisted. + */ + bypassChannelBlacklist?: boolean; + + /** + * Use instead of {@link BaseBushCommandOptions.args} when using argument generators or custom slashOptions + */ + helpArgs?: ArgsInfo[]; + + /** + * Extra information about the command, displayed in the help command. + */ + note?: string; +} + +export interface BaseBushCommandOptions + extends Omit, + ExtendedCommandOptions { + /** + * The description of the command. + */ + description: string; + + /** + * The arguments for the command. + */ + args?: BushArgumentOptions[] & CustomBushArgumentOptions[]; + + category: string; + + /** + * Permissions required by the client to run this command. + */ + clientPermissions: bigint | bigint[] | BushMissingPermissionSupplier; + + /** + * Permissions required by the user to run this command. + */ + userPermissions: bigint | bigint[] | BushMissingPermissionSupplier; + + /** + * Whether the argument is only accessible to the owners. + */ + ownerOnly?: boolean; + + /** + * Whether the argument is only accessible to the super users. + */ + superUserOnly?: boolean; +} + +export type BushCommandOptions = Omit | Omit; + +export interface ArgsInfo { + /** + * The name of the argument. + */ + name: string; + + /** + * The description of the argument. + */ + description: string; + + /** + * Whether the argument is optional. + * @default false + */ + optional?: boolean; + + /** + * Whether or not the argument has autocomplete enabled. + * @default false + */ + autocomplete?: boolean; + + /** + * Whether the argument is restricted a certain command. + * @default 'slash & text' + */ + only?: 'slash & text' | 'slash' | 'text'; + + /** + * The method that arguments are matched for text commands. + * @default 'phrase' + */ + match?: ArgumentMatch; + + /** + * The readable type of the argument. + */ + type: string; + + /** + * If {@link match} is 'flag' or 'option', these are the flags that are matched + * @default [] + */ + flag?: string[]; + + /** + * Whether the argument is only accessible to the owners. + * @default false + */ + ownerOnly?: boolean; + + /** + * Whether the argument is only accessible to the super users. + * @default false + */ + superUserOnly?: boolean; +} + +export abstract class BushCommand extends Command { + public declare client: BushClient; + public declare handler: BushCommandHandler; + public declare description: string; + + /** + * Show how to use the command. + */ + public usage: string[]; + + /** + * Examples for how to use the command. + */ + public examples: string[]; + + /** + * The options sent to the constructor + */ + public options: BushCommandOptions; + + /** + * The options sent to the super call + */ + public parsedOptions: CommandOptions; + + /** + * The channels the command is limited to run in. + */ + public restrictedChannels: Snowflake[] | undefined; + + /** + * The guilds the command is limited to run in. + */ + public restrictedGuilds: Snowflake[] | undefined; + + /** + * Whether the command is hidden from the help command. + */ + public hidden: boolean; + + /** + * A fake command, completely hidden from the help command. + */ + public pseudo: boolean; + + /** + * Allow this command to be run in channels that are blacklisted. + */ + public bypassChannelBlacklist: boolean; + + /** + * Info about the arguments for the help command. + */ + public argsInfo?: ArgsInfo[]; + + /** + * Extra information about the command, displayed in the help command. + */ + public note?: string; + + public constructor(id: string, options: BushCommandOptions) { + const options_ = options as BaseBushCommandOptions; + + if (options_.args && typeof options_.args !== 'function') { + options_.args.forEach((_, index: number) => { + if ('customType' in (options_.args?.[index] ?? {})) { + if (!options_.args![index]['type']) options_.args![index]['type'] = options_.args![index]['customType']! as any; + delete options_.args![index]['customType']; + } + }); + } + + const newOptions: Partial = {}; + for (const _key in options_) { + const key = _key as keyof typeof options_; // you got to love typescript + if (key === 'args' && 'args' in options_ && typeof options_.args === 'object') { + const newTextArgs: (ArgumentOptions & ExtraArgumentOptions)[] = []; + const newSlashArgs: SlashOption[] = []; + for (const arg of options_.args) { + if (arg.only !== 'slash' && !options_.slashOnly) { + const newArg: ArgumentOptions & ExtraArgumentOptions = {}; + if ('default' in arg) newArg.default = arg.default; + if ('description' in arg) newArg.description = arg.description; + if ('flag' in arg) newArg.flag = arg.flag; + if ('id' in arg) newArg.id = arg.id; + if ('index' in arg) newArg.index = arg.index; + if ('limit' in arg) newArg.limit = arg.limit; + if ('match' in arg) newArg.match = arg.match; + if ('modifyOtherwise' in arg) newArg.modifyOtherwise = arg.modifyOtherwise; + if ('multipleFlags' in arg) newArg.multipleFlags = arg.multipleFlags; + if ('otherwise' in arg) newArg.otherwise = arg.otherwise; + if ('prompt' in arg || 'retry' in arg || 'optional' in arg) { + newArg.prompt = {}; + if ('prompt' in arg) newArg.prompt.start = arg.prompt; + if ('retry' in arg) newArg.prompt.retry = arg.retry; + if ('optional' in arg) newArg.prompt.optional = arg.optional; + } + if ('type' in arg) newArg.type = arg.type as ArgumentType | ArgumentTypeCaster; + if ('unordered' in arg) newArg.unordered = arg.unordered; + if ('ownerOnly' in arg) newArg.ownerOnly = arg.ownerOnly; + if ('superUserOnly' in arg) newArg.superUserOnly = arg.superUserOnly; + newTextArgs.push(newArg); + } + if ( + arg.only !== 'text' && + !('slashOptions' in options_) && + (options_.slash || options_.slashOnly) && + arg.slashType !== false + ) { + const newArg: { + [key in SlashOptionKeys]?: any; + } = { + name: arg.id, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + description: arg.prompt || arg.description || 'No description provided.', + type: arg.slashType + }; + if ('slashResolve' in arg) newArg.resolve = arg.slashResolve; + if ('autocomplete' in arg) newArg.autocomplete = arg.autocomplete; + if ('channelTypes' in arg) newArg.channelTypes = arg.channelTypes; + if ('choices' in arg) newArg.choices = arg.choices; + if ('minValue' in arg) newArg.minValue = arg.minValue; + if ('maxValue' in arg) newArg.maxValue = arg.maxValue; + newArg.required = 'optional' in arg ? !arg.optional : true; + newSlashArgs.push(newArg as SlashOption); + } + } + if (newTextArgs.length > 0) newOptions.args = newTextArgs; + if (newSlashArgs.length > 0) newOptions.slashOptions = options_.slashOptions ?? newSlashArgs; + } else if (key === 'clientPermissions' || key === 'userPermissions') { + newOptions[key] = options_[key] as PermissionResolvable | PermissionResolvable[] | MissingPermissionSupplier; + } else { + newOptions[key] = options_[key]; + } + } + + super(id, newOptions); + + if (options_.args ?? options_.helpArgs) { + const argsInfo: ArgsInfo[] = []; + const combined = (options_.args ?? options_.helpArgs)!.map((arg) => { + const norm = options_.args + ? options_.args.find((_arg) => _arg.id === ('id' in arg ? arg.id : arg.name)) ?? ({} as BushArgumentOptions) + : ({} as BushArgumentOptions); + const help = options_.helpArgs + ? options_.helpArgs.find((_arg) => _arg.name === ('id' in arg ? arg.id : arg.name)) ?? ({} as ArgsInfo) + : ({} as ArgsInfo); + return { ...norm, ...help }; + }); + + for (const arg of combined) { + const name = _.camelCase('id' in arg ? arg.id : arg.name), + description = arg.description || '*No description provided.*', + optional = arg.optional ?? false, + autocomplete = arg.autocomplete ?? false, + only = arg.only ?? 'slash & text', + match = arg.match ?? 'phrase', + type = match === 'flag' ? 'flag' : arg.readableType ?? arg.type ?? 'string', + flag = arg.flag ? (Array.isArray(arg.flag) ? arg.flag : [arg.flag]) : [], + ownerOnly = arg.ownerOnly ?? false, + superUserOnly = arg.superUserOnly ?? false; + + argsInfo.push({ name, description, optional, autocomplete, only, match, type, flag, ownerOnly, superUserOnly }); + } + + this.argsInfo = argsInfo; + } + + this.description = options_.description; + this.usage = options_.usage; + this.examples = options_.examples; + this.options = options_; + this.parsedOptions = newOptions; + this.hidden = !!options_.hidden; + this.restrictedChannels = options_.restrictedChannels; + this.restrictedGuilds = options_.restrictedGuilds; + this.pseudo = !!options_.pseudo; + this.bypassChannelBlacklist = !!options_.bypassChannelBlacklist; + this.note = options_.note; + } + + /** + * Executes the command. + * @param message - Message that triggered the command. + * @param args - Evaluated arguments. + */ + public abstract override exec(message: CommandMessage, args: any): any; + /** + * Executes the command. + * @param message - Message that triggered the command. + * @param args - Evaluated arguments. + */ + public abstract override exec(message: CommandMessage | SlashMessage, args: any): any; +} + +type SlashOptionKeys = + | keyof AkairoApplicationCommandSubGroupData + | keyof AkairoApplicationCommandNonOptionsData + | keyof AkairoApplicationCommandChannelOptionData + | keyof AkairoApplicationCommandChoicesData + | keyof AkairoApplicationCommandAutocompleteOption + | keyof AkairoApplicationCommandNumericOptionData + | keyof AkairoApplicationCommandSubCommandData; + +interface PseudoArguments extends BaseBushArgumentType { + boolean: boolean; + flag: boolean; + regex: { match: RegExpMatchArray; matches: RegExpExecArray[] }; +} + +export type ArgType = NonNullable; +export type OptArgType = PseudoArguments[T]; + +/** + * `util` is always defined for messages after `'all'` inhibitors + */ +export type CommandMessage = Message & { + /** + * Extra properties applied to the Discord.js message object. + * Utilities for command responding. + * Available on all messages after 'all' inhibitors and built-in inhibitors (bot, client). + * Not all properties of the util are available, depending on the input. + * */ + util: CommandUtil; +}; diff --git a/lib/extensions/discord-akairo/BushCommandHandler.ts b/lib/extensions/discord-akairo/BushCommandHandler.ts new file mode 100644 index 0000000..da49af9 --- /dev/null +++ b/lib/extensions/discord-akairo/BushCommandHandler.ts @@ -0,0 +1,37 @@ +import { type BushCommand, type CommandMessage, type SlashMessage } from '#lib'; +import { CommandHandler, type Category, type CommandHandlerEvents, type CommandHandlerOptions } from 'discord-akairo'; +import { type Collection, type Message, type PermissionsString } from 'discord.js'; + +export type BushCommandHandlerOptions = CommandHandlerOptions; + +export interface BushCommandHandlerEvents extends CommandHandlerEvents { + commandBlocked: [message: CommandMessage, command: BushCommand, reason: string]; + commandBreakout: [message: CommandMessage, command: BushCommand, /* no util */ breakMessage: Message]; + commandCancelled: [message: CommandMessage, command: BushCommand, /* no util */ retryMessage?: Message]; + commandFinished: [message: CommandMessage, command: BushCommand, args: any, returnValue: any]; + commandInvalid: [message: CommandMessage, command: BushCommand]; + commandLocked: [message: CommandMessage, command: BushCommand]; + commandStarted: [message: CommandMessage, command: BushCommand, args: any]; + cooldown: [message: CommandMessage | SlashMessage, command: BushCommand, remaining: number]; + error: [error: Error, message: /* no util */ Message, command?: BushCommand]; + inPrompt: [message: /* no util */ Message]; + load: [command: BushCommand, isReload: boolean]; + messageBlocked: [message: /* no util */ Message | CommandMessage | SlashMessage, reason: string]; + messageInvalid: [message: CommandMessage]; + missingPermissions: [message: CommandMessage, command: BushCommand, type: 'client' | 'user', missing: PermissionsString[]]; + remove: [command: BushCommand]; + slashBlocked: [message: SlashMessage, command: BushCommand, reason: string]; + slashError: [error: Error, message: SlashMessage, command: BushCommand]; + slashFinished: [message: SlashMessage, command: BushCommand, args: any, returnValue: any]; + slashMissingPermissions: [message: SlashMessage, command: BushCommand, type: 'client' | 'user', missing: PermissionsString[]]; + slashStarted: [message: SlashMessage, command: BushCommand, args: any]; +} + +export class BushCommandHandler extends CommandHandler { + public declare modules: Collection; + public declare categories: Collection>; +} + +export interface BushCommandHandler extends CommandHandler { + findCommand(name: string): BushCommand; +} diff --git a/lib/extensions/discord-akairo/BushInhibitor.ts b/lib/extensions/discord-akairo/BushInhibitor.ts new file mode 100644 index 0000000..be396cf --- /dev/null +++ b/lib/extensions/discord-akairo/BushInhibitor.ts @@ -0,0 +1,19 @@ +import { type BushCommand, type CommandMessage, type SlashMessage } from '#lib'; +import { Inhibitor } from 'discord-akairo'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { Message } from 'discord.js'; + +export abstract class BushInhibitor extends Inhibitor { + /** + * Checks if message should be blocked. + * A return value of true will block the message. + * If returning a Promise, a resolved value of true will block the message. + * + * **Note:** `'all'` type inhibitors do not have {@link Message.util} defined. + * + * @param message - Message being handled. + * @param command - Command to check. + */ + public abstract override exec(message: CommandMessage, command: BushCommand): any; + public abstract override exec(message: CommandMessage | SlashMessage, command: BushCommand): any; +} diff --git a/lib/extensions/discord-akairo/BushInhibitorHandler.ts b/lib/extensions/discord-akairo/BushInhibitorHandler.ts new file mode 100644 index 0000000..5e4fb6c --- /dev/null +++ b/lib/extensions/discord-akairo/BushInhibitorHandler.ts @@ -0,0 +1,3 @@ +import { InhibitorHandler } from 'discord-akairo'; + +export class BushInhibitorHandler extends InhibitorHandler {} diff --git a/lib/extensions/discord-akairo/BushListener.ts b/lib/extensions/discord-akairo/BushListener.ts new file mode 100644 index 0000000..6917641 --- /dev/null +++ b/lib/extensions/discord-akairo/BushListener.ts @@ -0,0 +1,3 @@ +import { Listener } from 'discord-akairo'; + +export abstract class BushListener extends Listener {} diff --git a/lib/extensions/discord-akairo/BushListenerHandler.ts b/lib/extensions/discord-akairo/BushListenerHandler.ts new file mode 100644 index 0000000..9c3e4af --- /dev/null +++ b/lib/extensions/discord-akairo/BushListenerHandler.ts @@ -0,0 +1,3 @@ +import { ListenerHandler } from 'discord-akairo'; + +export class BushListenerHandler extends ListenerHandler {} diff --git a/lib/extensions/discord-akairo/BushTask.ts b/lib/extensions/discord-akairo/BushTask.ts new file mode 100644 index 0000000..1b70c88 --- /dev/null +++ b/lib/extensions/discord-akairo/BushTask.ts @@ -0,0 +1,3 @@ +import { Task } from 'discord-akairo'; + +export abstract class BushTask extends Task {} diff --git a/lib/extensions/discord-akairo/BushTaskHandler.ts b/lib/extensions/discord-akairo/BushTaskHandler.ts new file mode 100644 index 0000000..6535abb --- /dev/null +++ b/lib/extensions/discord-akairo/BushTaskHandler.ts @@ -0,0 +1,3 @@ +import { TaskHandler } from 'discord-akairo'; + +export class BushTaskHandler extends TaskHandler {} diff --git a/lib/extensions/discord-akairo/SlashMessage.ts b/lib/extensions/discord-akairo/SlashMessage.ts new file mode 100644 index 0000000..0a6669b --- /dev/null +++ b/lib/extensions/discord-akairo/SlashMessage.ts @@ -0,0 +1,3 @@ +import { AkairoMessage } from 'discord-akairo'; + +export class SlashMessage extends AkairoMessage {} diff --git a/lib/extensions/discord.js/BushClientEvents.ts b/lib/extensions/discord.js/BushClientEvents.ts new file mode 100644 index 0000000..22bae65 --- /dev/null +++ b/lib/extensions/discord.js/BushClientEvents.ts @@ -0,0 +1,200 @@ +import type { + BanResponse, + CommandMessage, + Guild as GuildDB, + GuildSettings +} from '#lib'; +import type { AkairoClientEvents } from 'discord-akairo'; +import type { + ButtonInteraction, + Collection, + Guild, + GuildMember, + GuildTextBasedChannel, + Message, + ModalSubmitInteraction, + Role, + SelectMenuInteraction, + Snowflake, + User +} from 'discord.js'; + +export interface BushClientEvents extends AkairoClientEvents { + bushBan: [ + victim: GuildMember | User, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + duration: number, + dmSuccess?: boolean, + evidence?: string + ]; + bushBlock: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + duration: number, + dmSuccess: boolean, + channel: GuildTextBasedChannel, + evidence?: string + ]; + bushKick: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean, + evidence?: string + ]; + bushMute: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + duration: number, + dmSuccess: boolean, + evidence?: string + ]; + bushPunishRole: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + duration: number, + role: Role, + evidence?: string + ]; + bushPunishRoleRemove: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + role: Role, + evidence?: string + ]; + bushPurge: [ + moderator: User, + guild: Guild, + channel: GuildTextBasedChannel, + messages: Collection + ]; + bushRemoveTimeout: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean, + evidence?: string + ]; + bushTimeout: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + duration: number, + dmSuccess: boolean, + evidence?: string + ]; + bushUnban: [ + victim: User, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean, + evidence?: string + ]; + bushUnblock: [ + victim: GuildMember | User, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean, + channel: GuildTextBasedChannel, + evidence?: string + ]; + bushUnmute: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean, + evidence?: string + ]; + bushUpdateModlog: [ + moderator: GuildMember, + modlogID: string, + key: 'evidence' | 'hidden', + oldModlog: string | boolean, + newModlog: string | boolean + ]; + bushUpdateSettings: [ + setting: Setting, + guild: Guild, + oldValue: GuildDB[Setting], + newValue: GuildDB[Setting], + moderator?: GuildMember + ]; + bushWarn: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean, + evidence?: string + ]; + bushLevelUpdate: [ + member: GuildMember, + oldLevel: number, + newLevel: number, + currentXp: number, + message: CommandMessage + ]; + bushLockdown: [ + moderator: GuildMember, + reason: string | undefined, + channelsSuccessMap: Collection, + all?: boolean + ]; + bushUnlockdown: [ + moderator: GuildMember, + reason: string | undefined, + channelsSuccessMap: Collection, + all?: boolean + ]; + massBan: [ + moderator: GuildMember, + guild: Guild, + reason: string | undefined, + results: Collection + ]; + massEvidence: [ + moderator: GuildMember, + guild: Guild, + evidence: string, + lines: string[] + ]; + /* components */ + button: [button: ButtonInteraction]; + selectMenu: [selectMenu: SelectMenuInteraction]; + modal: [modal: ModalSubmitInteraction]; +} + +type Setting = + | GuildSettings + | 'enabledFeatures' + | 'blacklistedChannels' + | 'blacklistedUsers' + | 'disabledCommands'; diff --git a/lib/extensions/discord.js/ExtendedGuild.ts b/lib/extensions/discord.js/ExtendedGuild.ts new file mode 100644 index 0000000..63ee2fd --- /dev/null +++ b/lib/extensions/discord.js/ExtendedGuild.ts @@ -0,0 +1,919 @@ +import { + AllowedMentions, + banResponse, + colors, + dmResponse, + emojis, + permissionsResponse, + punishmentEntryRemove, + type BanResponse, + type GuildFeatures, + type GuildLogType, + type GuildModel +} from '#lib'; +import assert from 'assert/strict'; +import { + AttachmentBuilder, + AttachmentPayload, + Collection, + Guild, + JSONEncodable, + Message, + MessageType, + PermissionFlagsBits, + SnowflakeUtil, + ThreadChannel, + type APIMessage, + type GuildMember, + type GuildMemberResolvable, + type GuildTextBasedChannel, + type MessageOptions, + type MessagePayload, + type NewsChannel, + type Snowflake, + type TextChannel, + type User, + type UserResolvable, + type VoiceChannel, + type Webhook, + type WebhookMessageOptions +} from 'discord.js'; +import _ from 'lodash'; +import * as Moderation from '../../common/Moderation.js'; +import { Guild as GuildDB } from '../../models/instance/Guild.js'; +import { ModLogType } from '../../models/instance/ModLog.js'; +import { addOrRemoveFromArray } from '../../utils/BushUtils.js'; + +declare module 'discord.js' { + export interface Guild { + /** + * Checks if the guild has a certain custom feature. + * @param feature The feature to check for + */ + hasFeature(feature: GuildFeatures): Promise; + /** + * Adds a custom feature to the guild. + * @param feature The feature to add + * @param moderator The moderator responsible for adding a feature + */ + addFeature(feature: GuildFeatures, moderator?: GuildMember): Promise; + /** + * Removes a custom feature from the guild. + * @param feature The feature to remove + * @param moderator The moderator responsible for removing a feature + */ + removeFeature(feature: GuildFeatures, moderator?: GuildMember): Promise; + /** + * Makes a custom feature the opposite of what it was before + * @param feature The feature to toggle + * @param moderator The moderator responsible for toggling a feature + */ + toggleFeature(feature: GuildFeatures, moderator?: GuildMember): Promise; + /** + * Fetches a custom setting for the guild + * @param setting The setting to get + */ + getSetting(setting: K): Promise; + /** + * Sets a custom setting for the guild + * @param setting The setting to change + * @param value The value to change the setting to + * @param moderator The moderator to responsible for changing the setting + */ + setSetting>( + setting: K, + value: GuildModel[K], + moderator?: GuildMember + ): Promise; + /** + * Get a the log channel configured for a certain log type. + * @param logType The type of log channel to get. + * @returns Either the log channel or undefined if not configured. + */ + getLogChannel(logType: GuildLogType): Promise; + /** + * Sends a message to the guild's specified logging channel + * @param logType The corresponding channel that the message will be sent to + * @param message The parameters for {@link BushTextChannel.send} + */ + sendLogChannel(logType: GuildLogType, message: string | MessagePayload | MessageOptions): Promise; + /** + * Sends a formatted error message in a guild's error log channel + * @param title The title of the error embed + * @param message The description of the error embed + */ + error(title: string, message: string): Promise; + /** + * Bans a user, dms them, creates a mod log entry, and creates a punishment entry. + * @param options Options for banning the user. + * @returns A string status message of the ban. + */ + bushBan(options: GuildBushBanOptions): Promise; + /** + * {@link bushBan} with less resolving and checks + * @param options Options for banning the user. + * @returns A string status message of the ban. + * **Preconditions:** + * - {@link me} has the `BanMembers` permission + * **Warning:** + * - Doesn't emit bushBan Event + */ + massBanOne(options: GuildMassBanOneOptions): Promise; + /** + * Unbans a user, dms them, creates a mod log entry, and destroys the punishment entry. + * @param options Options for unbanning the user. + * @returns A status message of the unban. + */ + bushUnban(options: GuildBushUnbanOptions): Promise; + /** + * Denies send permissions in specified channels + * @param options The options for locking down the guild + */ + lockdown(options: LockdownOptions): Promise; + quote(rawQuote: APIMessage, channel: GuildTextBasedChannel): Promise; + } +} + +/** + * Represents a guild (or a server) on Discord. + * It's recommended to see if a guild is available before performing operations or reading data from it. You can + * check this with {@link ExtendedGuild.available}. + */ +export class ExtendedGuild extends Guild { + /** + * Checks if the guild has a certain custom feature. + * @param feature The feature to check for + */ + public override async hasFeature(feature: GuildFeatures): Promise { + const features = await this.getSetting('enabledFeatures'); + return features.includes(feature); + } + + /** + * Adds a custom feature to the guild. + * @param feature The feature to add + * @param moderator The moderator responsible for adding a feature + */ + public override async addFeature(feature: GuildFeatures, moderator?: GuildMember): Promise { + const features = await this.getSetting('enabledFeatures'); + const newFeatures = addOrRemoveFromArray('add', features, feature); + return (await this.setSetting('enabledFeatures', newFeatures, moderator)).enabledFeatures; + } + + /** + * Removes a custom feature from the guild. + * @param feature The feature to remove + * @param moderator The moderator responsible for removing a feature + */ + public override async removeFeature(feature: GuildFeatures, moderator?: GuildMember): Promise { + const features = await this.getSetting('enabledFeatures'); + const newFeatures = addOrRemoveFromArray('remove', features, feature); + return (await this.setSetting('enabledFeatures', newFeatures, moderator)).enabledFeatures; + } + + /** + * Makes a custom feature the opposite of what it was before + * @param feature The feature to toggle + * @param moderator The moderator responsible for toggling a feature + */ + public override async toggleFeature(feature: GuildFeatures, moderator?: GuildMember): Promise { + return (await this.hasFeature(feature)) + ? await this.removeFeature(feature, moderator) + : await this.addFeature(feature, moderator); + } + + /** + * Fetches a custom setting for the guild + * @param setting The setting to get + */ + public override async getSetting(setting: K): Promise { + return ( + this.client.cache.guilds.get(this.id)?.[setting] ?? + ((await GuildDB.findByPk(this.id)) ?? GuildDB.build({ id: this.id }))[setting] + ); + } + + /** + * Sets a custom setting for the guild + * @param setting The setting to change + * @param value The value to change the setting to + * @param moderator The moderator to responsible for changing the setting + */ + public override async setSetting>( + setting: K, + value: GuildDB[K], + moderator?: GuildMember + ): Promise { + const row = (await GuildDB.findByPk(this.id)) ?? GuildDB.build({ id: this.id }); + const oldValue = row[setting] as GuildDB[K]; + row[setting] = value; + this.client.cache.guilds.set(this.id, row.toJSON() as GuildDB); + this.client.emit('bushUpdateSettings', setting, this, oldValue, row[setting], moderator); + return await row.save(); + } + + /** + * Get a the log channel configured for a certain log type. + * @param logType The type of log channel to get. + * @returns Either the log channel or undefined if not configured. + */ + public override async getLogChannel(logType: GuildLogType): Promise { + const channelId = (await this.getSetting('logChannels'))[logType]; + if (!channelId) return undefined; + return ( + (this.channels.cache.get(channelId) as TextChannel | undefined) ?? + ((await this.channels.fetch(channelId)) as TextChannel | null) ?? + undefined + ); + } + + /** + * Sends a message to the guild's specified logging channel + * @param logType The corresponding channel that the message will be sent to + * @param message The parameters for {@link BushTextChannel.send} + */ + public override async sendLogChannel( + logType: GuildLogType, + message: string | MessagePayload | MessageOptions + ): Promise { + const logChannel = await this.getLogChannel(logType); + if (!logChannel || !logChannel.isTextBased()) { + void this.client.console.warn('sendLogChannel', `No log channel found for <<${logType}<< in <<${this.name}>>.`); + return; + } + if ( + !logChannel + .permissionsFor(this.members.me!.id) + ?.has([PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.EmbedLinks]) + ) + return; + + return await logChannel.send(message).catch(() => null); + } + + /** + * Sends a formatted error message in a guild's error log channel + * @param title The title of the error embed + * @param message The description of the error embed + */ + public override async error(title: string, message: string): Promise { + void this.client.console.info(_.camelCase(title), message.replace(/\*\*(.*?)\*\*/g, '<<$1>>')); + void this.sendLogChannel('error', { embeds: [{ title: title, description: message, color: colors.error }] }); + } + + /** + * Bans a user, dms them, creates a mod log entry, and creates a punishment entry. + * @param options Options for banning the user. + * @returns A string status message of the ban. + */ + public override async bushBan(options: GuildBushBanOptions): Promise { + // checks + if (!this.members.me!.permissions.has(PermissionFlagsBits.BanMembers)) return banResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const user = await this.client.utils.resolveNonCachedUser(options.user); + const moderator = this.client.users.resolve(options.moderator ?? this.client.user!); + if (!user || !moderator) return banResponse.CANNOT_RESOLVE_USER; + + if ((await this.bans.fetch()).has(user.id)) return banResponse.ALREADY_BANNED; + + const ret = await (async () => { + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, + user: user, + moderator: moderator.id, + reason: options.reason, + duration: options.duration, + guild: this, + evidence: options.evidence + }); + if (!modlog) return banResponse.MODLOG_ERROR; + caseID = modlog.id; + + // dm user + dmSuccessEvent = await Moderation.punishDM({ + client: this.client, + modlog: modlog.id, + guild: this, + user: user, + punishment: 'banned', + duration: options.duration ?? 0, + reason: options.reason ?? undefined, + sendFooter: true + }); + + // ban + const banSuccess = await this.bans + .create(user?.id ?? options.user, { + reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, + deleteMessageDays: options.deleteDays + }) + .catch(() => false); + if (!banSuccess) return banResponse.ACTION_ERROR; + + // add punishment entry so they can be unbanned later + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, + type: 'ban', + user: user, + guild: this, + duration: options.duration, + modlog: modlog.id + }); + if (!punishmentEntrySuccess) return banResponse.PUNISHMENT_ENTRY_ADD_ERROR; + + if (!dmSuccessEvent) return banResponse.DM_ERROR; + return banResponse.SUCCESS; + })(); + + if (!([banResponse.ACTION_ERROR, banResponse.MODLOG_ERROR, banResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes(ret)) + this.client.emit( + 'bushBan', + user, + moderator, + this, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + dmSuccessEvent, + options.evidence + ); + return ret; + } + + /** + * {@link bushBan} with less resolving and checks + * @param options Options for banning the user. + * @returns A string status message of the ban. + * **Preconditions:** + * - {@link me} has the `BanMembers` permission + * **Warning:** + * - Doesn't emit bushBan Event + */ + public override async massBanOne(options: GuildMassBanOneOptions): Promise { + if (this.bans.cache.has(options.user)) return banResponse.ALREADY_BANNED; + + const ret = await (async () => { + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntrySimple({ + client: this.client, + type: ModLogType.PERM_BAN, + user: options.user, + moderator: options.moderator, + reason: options.reason, + duration: 0, + guild: this.id + }); + if (!modlog) return banResponse.MODLOG_ERROR; + + let dmSuccessEvent: boolean | undefined = undefined; + // dm user + if (this.members.cache.has(options.user)) { + dmSuccessEvent = await Moderation.punishDM({ + client: this.client, + modlog: modlog.id, + guild: this, + user: options.user, + punishment: 'banned', + duration: 0, + reason: options.reason ?? undefined, + sendFooter: true + }); + } + + // ban + const banSuccess = await this.bans + .create(options.user, { + reason: `${options.moderator} | ${options.reason}`, + deleteMessageDays: options.deleteDays + }) + .catch(() => false); + if (!banSuccess) return banResponse.ACTION_ERROR; + + // add punishment entry so they can be unbanned later + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, + type: 'ban', + user: options.user, + guild: this, + duration: 0, + modlog: modlog.id + }); + if (!punishmentEntrySuccess) return banResponse.PUNISHMENT_ENTRY_ADD_ERROR; + + if (!dmSuccessEvent) return banResponse.DM_ERROR; + return banResponse.SUCCESS; + })(); + return ret; + } + + /** + * Unbans a user, dms them, creates a mod log entry, and destroys the punishment entry. + * @param options Options for unbanning the user. + * @returns A status message of the unban. + */ + public override async bushUnban(options: GuildBushUnbanOptions): Promise { + // checks + if (!this.members.me!.permissions.has(PermissionFlagsBits.BanMembers)) return unbanResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const user = await this.client.utils.resolveNonCachedUser(options.user); + const moderator = this.client.users.resolve(options.moderator ?? this.client.user!); + if (!user || !moderator) return unbanResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + const bans = await this.bans.fetch(); + + let notBanned = false; + if (!bans.has(user.id)) notBanned = true; + + const unbanSuccess = await this.bans + .remove(user, `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) + .catch((e) => { + if (e?.code === 'UNKNOWN_BAN') { + notBanned = true; + return true; + } else return false; + }); + + if (notBanned) return unbanResponse.NOT_BANNED; + if (!unbanSuccess) return unbanResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.UNBAN, + user: user.id, + moderator: moderator.id, + reason: options.reason, + guild: this, + evidence: options.evidence + }); + if (!modlog) return unbanResponse.MODLOG_ERROR; + caseID = modlog.id; + + // remove punishment entry + const removePunishmentEntrySuccess = await Moderation.removePunishmentEntry({ + client: this.client, + type: 'ban', + user: user.id, + guild: this + }); + if (!removePunishmentEntrySuccess) return unbanResponse.PUNISHMENT_ENTRY_REMOVE_ERROR; + + // dm user + dmSuccessEvent = await Moderation.punishDM({ + client: this.client, + guild: this, + user: user, + punishment: 'unbanned', + reason: options.reason ?? undefined, + sendFooter: false + }); + + if (!dmSuccessEvent) return unbanResponse.DM_ERROR; + return unbanResponse.SUCCESS; + })(); + if ( + !([unbanResponse.ACTION_ERROR, unbanResponse.MODLOG_ERROR, unbanResponse.PUNISHMENT_ENTRY_REMOVE_ERROR] as const).includes( + ret + ) + ) + this.client.emit( + 'bushUnban', + user, + moderator, + this, + options.reason ?? undefined, + caseID!, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Denies send permissions in specified channels + * @param options The options for locking down the guild + */ + public override async lockdown(options: LockdownOptions): Promise { + if (!options.all && !options.channel) return 'all not chosen and no channel specified'; + const channelIds = options.all ? await this.getSetting('lockdownChannels') : [options.channel!.id]; + + if (!channelIds.length) return 'no channels configured'; + const mappedChannels = channelIds.map((id) => this.channels.cache.get(id)); + + const invalidChannels = mappedChannels.filter((c) => c === undefined); + if (invalidChannels.length) return `invalid channel configured: ${invalidChannels.join(', ')}`; + + const moderator = this.members.resolve(options.moderator); + if (!moderator) return 'moderator not found'; + + const errors = new Collection(); + const success = new Collection(); + const ret = await (async (): Promise => { + for (const _channel of mappedChannels) { + const channel = _channel!; + if (!channel.isTextBased()) { + errors.set(channel.id, new Error('wrong channel type')); + success.set(channel.id, false); + continue; + } + if (!channel.permissionsFor(this.members.me!.id)?.has([PermissionFlagsBits.ManageChannels])) { + errors.set(channel.id, new Error('client no permission')); + success.set(channel.id, false); + continue; + } else if (!channel.permissionsFor(moderator)?.has([PermissionFlagsBits.ManageChannels])) { + errors.set(channel.id, new Error('moderator no permission')); + success.set(channel.id, false); + continue; + } + + const reason = `[${options.unlock ? 'Unlockdown' : 'Lockdown'}] ${moderator.user.tag} | ${ + options.reason ?? 'No reason provided' + }`; + + const permissionOverwrites = channel.isThread() ? channel.parent!.permissionOverwrites : channel.permissionOverwrites; + const perms = { + SendMessagesInThreads: options.unlock ? null : false, + SendMessages: options.unlock ? null : false + }; + const permsForMe = { + [channel.isThread() ? 'SendMessagesInThreads' : 'SendMessages']: options.unlock ? null : true + }; // so I can send messages in the channel + + const changePermSuccess = await permissionOverwrites.edit(this.id, perms, { reason }).catch((e) => e); + if (changePermSuccess instanceof Error) { + errors.set(channel.id, changePermSuccess); + success.set(channel.id, false); + } else { + success.set(channel.id, true); + await permissionOverwrites.edit(this.members.me!, permsForMe, { reason }); + await channel.send({ + embeds: [ + { + author: { name: moderator.user.tag, icon_url: moderator.displayAvatarURL() }, + title: `This channel has been ${options.unlock ? 'un' : ''}locked`, + description: options.reason ?? 'No reason provided', + color: options.unlock ? colors.Green : colors.Red, + timestamp: new Date().toISOString() + } + ] + }); + } + } + + if (errors.size) return errors; + else return `success: ${success.filter((c) => c === true).size}`; + })(); + + this.client.emit(options.unlock ? 'bushUnlockdown' : 'bushLockdown', moderator, options.reason, success, options.all); + return ret; + } + + public override async quote(rawQuote: APIMessage, channel: GuildTextBasedChannel): Promise { + if (!channel.isTextBased() || channel.isDMBased() || channel.guildId !== this.id || !this.members.me) return null; + if (!channel.permissionsFor(this.members.me).has('ManageWebhooks')) return null; + + const quote = new Message(this.client, rawQuote); + + const target = channel instanceof ThreadChannel ? channel.parent : channel; + if (!target) return null; + + const webhooks: Collection = await target.fetchWebhooks().catch((e) => e); + if (!(webhooks instanceof Collection)) return null; + + // find a webhook that we can use + let webhook = webhooks.find((w) => !!w.token) ?? null; + if (!webhook) + webhook = await target + .createWebhook({ + name: `${this.client.user!.username} Quotes #${target.name}`, + avatar: this.client.user!.displayAvatarURL({ size: 2048 }), + reason: 'Creating a webhook for quoting' + }) + .catch(() => null); + + if (!webhook) return null; + + const sendOptions: Omit = {}; + + const displayName = quote.member?.displayName ?? quote.author.username; + + switch (quote.type) { + case MessageType.Default: + case MessageType.Reply: + case MessageType.ChatInputCommand: + case MessageType.ContextMenuCommand: + case MessageType.ThreadStarterMessage: + sendOptions.content = quote.content || undefined; + sendOptions.threadId = channel instanceof ThreadChannel ? channel.id : undefined; + sendOptions.embeds = quote.embeds.length ? quote.embeds : undefined; + //@ts-expect-error: jank + sendOptions.attachments = quote.attachments.size + ? [...quote.attachments.values()].map((a) => AttachmentBuilder.from(a as JSONEncodable)) + : undefined; + + if (quote.stickers.size && !(quote.content || quote.embeds.length || quote.attachments.size)) + sendOptions.content = '[[This message has a sticker but not content]]'; + + break; + case MessageType.RecipientAdd: { + const recipient = rawQuote.mentions[0]; + if (!recipient) { + sendOptions.content = `${emojis.error} Cannot resolve recipient.`; + break; + } + + if (quote.channel.isThread()) { + const recipientDisplay = quote.guild?.members.cache.get(recipient.id)?.displayName ?? recipient.username; + sendOptions.content = `${emojis.join} ${displayName} added ${recipientDisplay} to the thread.`; + } else { + // this should never happen + sendOptions.content = `${emojis.join} ${displayName} added ${recipient.username} to the group.`; + } + + break; + } + case MessageType.RecipientRemove: { + const recipient = rawQuote.mentions[0]; + if (!recipient) { + sendOptions.content = `${emojis.error} Cannot resolve recipient.`; + break; + } + + if (quote.channel.isThread()) { + const recipientDisplay = quote.guild?.members.cache.get(recipient.id)?.displayName ?? recipient.username; + sendOptions.content = `${emojis.leave} ${displayName} removed ${recipientDisplay} from the thread.`; + } else { + // this should never happen + sendOptions.content = `${emojis.leave} ${displayName} removed ${recipient.username} from the group.`; + } + + break; + } + + case MessageType.ChannelNameChange: + sendOptions.content = `<:pencil:957988608994861118> ${displayName} changed the channel name: **${quote.content}**`; + + break; + + case MessageType.ChannelPinnedMessage: + throw new Error('Not implemented yet: MessageType.ChannelPinnedMessage case'); + case MessageType.UserJoin: { + const messages = [ + '{username} joined the party.', + '{username} is here.', + 'Welcome, {username}. We hope you brought pizza.', + 'A wild {username} appeared.', + '{username} just landed.', + '{username} just slid into the server.', + '{username} just showed up!', + 'Welcome {username}. Say hi!', + '{username} hopped into the server.', + 'Everyone welcome {username}!', + "Glad you're here, {username}.", + 'Good to see you, {username}.', + 'Yay you made it, {username}!' + ]; + + const timestamp = SnowflakeUtil.timestampFrom(quote.id); + + // this is the same way that the discord client decides what message to use. + const message = messages[timestamp % messages.length].replace(/{username}/g, displayName); + + sendOptions.content = `${emojis.join} ${message}`; + break; + } + case MessageType.GuildBoost: + sendOptions.content = `<:NitroBoost:585558042309820447> ${displayName} just boosted the server${ + quote.content ? ` **${quote.content}** times` : '' + }!`; + + break; + case MessageType.GuildBoostTier1: + case MessageType.GuildBoostTier2: + case MessageType.GuildBoostTier3: + sendOptions.content = `<:NitroBoost:585558042309820447> ${displayName} just boosted the server${ + quote.content ? ` **${quote.content}** times` : '' + }! ${quote.guild?.name} has achieved **Level ${quote.type - 8}!**`; + + break; + case MessageType.ChannelFollowAdd: + sendOptions.content = `${displayName} has added **${quote.content}** to this channel. Its most important updates will show up here.`; + + break; + case MessageType.GuildDiscoveryDisqualified: + sendOptions.content = + '<:SystemMessageCross:842172192418693173> This server has been removed from Server Discovery because it no longer passes all the requirements. Check Server Settings for more details.'; + + break; + case MessageType.GuildDiscoveryRequalified: + sendOptions.content = + '<:SystemMessageCheck:842172191801212949> This server is eligible for Server Discovery again and has been automatically relisted!'; + + break; + case MessageType.GuildDiscoveryGracePeriodInitialWarning: + sendOptions.content = + '<:SystemMessageWarn:842172192401915971> This server has failed Discovery activity requirements for 1 week. If this server fails for 4 weeks in a row, it will be automatically removed from Discovery.'; + + break; + case MessageType.GuildDiscoveryGracePeriodFinalWarning: + sendOptions.content = + '<:SystemMessageWarn:842172192401915971> This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery.'; + + break; + case MessageType.ThreadCreated: { + const threadId = rawQuote.message_reference?.channel_id; + + sendOptions.content = `<:thread:865033845753249813> ${displayName} started a thread: **[${quote.content}](https://discord.com/channels/${quote.guildId}/${threadId} + )**. See all threads.`; + + break; + } + case MessageType.GuildInviteReminder: + sendOptions.content = 'Wondering who to invite? Start by inviting anyone who can help you build the server!'; + + break; + // todo: use enum for this + case 24 as MessageType: { + const embed = quote.embeds[0]; + // eslint-disable-next-line deprecation/deprecation + assert.equal(embed.data.type, 'auto_moderation_message'); + const ruleName = embed.fields!.find((f) => f.name === 'rule_name')!.value; + const channelId = embed.fields!.find((f) => f.name === 'channel_id')!.value; + const keyword = embed.fields!.find((f) => f.name === 'keyword')!.value; + + sendOptions.username = `AutoMod (${quote.member?.displayName ?? quote.author.username})`; + sendOptions.content = `Automod has blocked a message in <#${channelId}>`; + sendOptions.embeds = [ + { + title: quote.member?.displayName ?? quote.author.username, + description: embed.description ?? 'There is no content???', + footer: { + text: `Keyword: ${keyword} • Rule: ${ruleName}` + }, + color: 0x36393f + } + ]; + + break; + } + case MessageType.ChannelIconChange: + case MessageType.Call: + default: + sendOptions.content = `${emojis.error} I cannot quote messages of type **${ + MessageType[quote.type] || quote.type + }** messages, please report this to my developers.`; + + break; + } + + sendOptions.allowedMentions = AllowedMentions.none(); + sendOptions.username ??= quote.member?.displayName ?? quote.author.username; + sendOptions.avatarURL = quote.member?.displayAvatarURL({ size: 2048 }) ?? quote.author.displayAvatarURL({ size: 2048 }); + + return await webhook.send(sendOptions); /* .catch((e: any) => e); */ + } +} + +/** + * Options for unbanning a user + */ +export interface GuildBushUnbanOptions { + /** + * The user to unban + */ + user: UserResolvable | User; + + /** + * The reason for unbanning the user + */ + reason?: string | null; + + /** + * The moderator who unbanned the user + */ + moderator?: UserResolvable; + + /** + * The evidence for the unban + */ + evidence?: string; +} + +export interface GuildMassBanOneOptions { + /** + * The user to ban + */ + user: Snowflake; + + /** + * The reason to ban the user + */ + reason: string; + + /** + * The moderator who banned the user + */ + moderator: Snowflake; + + /** + * The number of days to delete the user's messages for + */ + deleteDays?: number; +} + +/** + * Options for banning a user + */ +export interface GuildBushBanOptions { + /** + * The user to ban + */ + user: UserResolvable; + + /** + * The reason to ban the user + */ + reason?: string | null; + + /** + * The moderator who banned the user + */ + moderator?: UserResolvable; + + /** + * The duration of the ban + */ + duration?: number; + + /** + * The number of days to delete the user's messages for + */ + deleteDays?: number; + + /** + * The evidence for the ban + */ + evidence?: string; +} + +type ValueOf = T[keyof T]; + +export const unbanResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + ...punishmentEntryRemove, + NOT_BANNED: 'user not banned' +} as const); + +/** + * Response returned when unbanning a user + */ +export type UnbanResponse = ValueOf; + +/** + * Options for locking down channel(s) + */ +export interface LockdownOptions { + /** + * The moderator responsible for the lockdown + */ + moderator: GuildMemberResolvable; + + /** + * Whether to lock down all (specified) channels + */ + all: boolean; + + /** + * Reason for the lockdown + */ + reason?: string; + + /** + * A specific channel to lockdown + */ + channel?: ThreadChannel | NewsChannel | TextChannel | VoiceChannel; + + /** + * Whether or not to unlock the channel(s) instead of locking them + */ + unlock?: boolean; +} + +/** + * Response returned when locking down a channel + */ +export type LockdownResponse = + | `success: ${number}` + | 'all not chosen and no channel specified' + | 'no channels configured' + | `invalid channel configured: ${string}` + | 'moderator not found' + | Collection; diff --git a/lib/extensions/discord.js/ExtendedGuildMember.ts b/lib/extensions/discord.js/ExtendedGuildMember.ts new file mode 100644 index 0000000..f8add83 --- /dev/null +++ b/lib/extensions/discord.js/ExtendedGuildMember.ts @@ -0,0 +1,1255 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { formatError, Moderation, ModLogType, Time, type BushClientEvents, type PunishmentTypeDM, type ValueOf } from '#lib'; +import { + ChannelType, + GuildMember, + PermissionFlagsBits, + type GuildChannelResolvable, + type GuildTextBasedChannel, + type Role +} from 'discord.js'; +/* eslint-enable @typescript-eslint/no-unused-vars */ + +declare module 'discord.js' { + export interface GuildMember { + /** + * Send a punishment dm to the user. + * @param punishment The punishment that the user has received. + * @param reason The reason for the user's punishment. + * @param duration The duration of the punishment. + * @param modlog The modlog case id so the user can make an appeal. + * @param sendFooter Whether or not to send the guild's punishment footer with the dm. + * @returns Whether or not the dm was sent successfully. + */ + bushPunishDM( + punishment: PunishmentTypeDM, + reason?: string | null, + duration?: number, + modlog?: string, + sendFooter?: boolean + ): Promise; + /** + * Warn the user, create a modlog entry, and send a dm to the user. + * @param options Options for warning the user. + * @returns An object with the result of the warning, and the case number of the warn. + * @emits {@link BushClientEvents.bushWarn} + */ + bushWarn(options: BushPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number | null }>; + /** + * Add a role to the user, if it is a punishment create a modlog entry, and create a punishment entry if it is temporary or a punishment. + * @param options Options for adding a role to the user. + * @returns A status message for adding the add. + * @emits {@link BushClientEvents.bushPunishRole} + */ + bushAddRole(options: AddRoleOptions): Promise; + /** + * Remove a role from the user, if it is a punishment create a modlog entry, and destroy a punishment entry if it was temporary or a punishment. + * @param options Options for removing a role from the user. + * @returns A status message for removing the role. + * @emits {@link BushClientEvents.bushPunishRoleRemove} + */ + bushRemoveRole(options: RemoveRoleOptions): Promise; + /** + * Mute the user, create a modlog entry, creates a punishment entry, and dms the user. + * @param options Options for muting the user. + * @returns A status message for muting the user. + * @emits {@link BushClientEvents.bushMute} + */ + bushMute(options: BushTimedPunishmentOptions): Promise; + /** + * Unmute the user, create a modlog entry, remove the punishment entry, and dm the user. + * @param options Options for unmuting the user. + * @returns A status message for unmuting the user. + * @emits {@link BushClientEvents.bushUnmute} + */ + bushUnmute(options: BushPunishmentOptions): Promise; + /** + * Kick the user, create a modlog entry, and dm the user. + * @param options Options for kicking the user. + * @returns A status message for kicking the user. + * @emits {@link BushClientEvents.bushKick} + */ + bushKick(options: BushPunishmentOptions): Promise; + /** + * Ban the user, create a modlog entry, create a punishment entry, and dm the user. + * @param options Options for banning the user. + * @returns A status message for banning the user. + * @emits {@link BushClientEvents.bushBan} + */ + bushBan(options: BushBanOptions): Promise>; + /** + * Prevents a user from speaking in a channel. + * @param options Options for blocking the user. + */ + bushBlock(options: BlockOptions): Promise; + /** + * Allows a user to speak in a channel. + * @param options Options for unblocking the user. + */ + bushUnblock(options: UnblockOptions): Promise; + /** + * Mutes a user using discord's timeout feature. + * @param options Options for timing out the user. + */ + bushTimeout(options: BushTimeoutOptions): Promise; + /** + * Removes a timeout from a user. + * @param options Options for removing the timeout. + */ + bushRemoveTimeout(options: BushPunishmentOptions): Promise; + /** + * Whether or not the user is an owner of the bot. + */ + isOwner(): boolean; + /** + * Whether or not the user is a super user of the bot. + */ + isSuperUser(): boolean; + } +} + +/** + * Represents a member of a guild on Discord. + */ +export class ExtendedGuildMember extends GuildMember { + /** + * Send a punishment dm to the user. + * @param punishment The punishment that the user has received. + * @param reason The reason for the user's punishment. + * @param duration The duration of the punishment. + * @param modlog The modlog case id so the user can make an appeal. + * @param sendFooter Whether or not to send the guild's punishment footer with the dm. + * @returns Whether or not the dm was sent successfully. + */ + public override async bushPunishDM( + punishment: PunishmentTypeDM, + reason?: string | null, + duration?: number, + modlog?: string, + sendFooter = true + ): Promise { + return Moderation.punishDM({ + client: this.client, + modlog, + guild: this.guild, + user: this, + punishment, + reason: reason ?? undefined, + duration, + sendFooter + }); + } + + /** + * Warn the user, create a modlog entry, and send a dm to the user. + * @param options Options for warning the user. + * @returns An object with the result of the warning, and the case number of the warn. + * @emits {@link BushClientEvents.bushWarn} + */ + public override async bushWarn(options: BushPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number | null }> { + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return { result: warnResponse.CANNOT_RESOLVE_USER, caseNum: null }; + + const ret = await (async (): Promise<{ result: WarnResponse; caseNum: number | null }> => { + // add modlog entry + const result = await Moderation.createModLogEntry( + { + client: this.client, + type: ModLogType.WARN, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }, + true + ); + caseID = result.log?.id; + if (!result || !result.log) return { result: warnResponse.MODLOG_ERROR, caseNum: null }; + + if (!options.silent) { + // dm user + const dmSuccess = await this.bushPunishDM('warned', options.reason); + dmSuccessEvent = dmSuccess; + if (!dmSuccess) return { result: warnResponse.DM_ERROR, caseNum: result.caseNum }; + } + + return { result: warnResponse.SUCCESS, caseNum: result.caseNum }; + })(); + if (!([warnResponse.MODLOG_ERROR] as const).includes(ret.result) && !options.silent) + this.client.emit('bushWarn', this, moderator, this.guild, options.reason ?? undefined, caseID!, dmSuccessEvent!); + return ret; + } + + /** + * Add a role to the user, if it is a punishment create a modlog entry, and create a punishment entry if it is temporary or a punishment. + * @param options Options for adding a role to the user. + * @returns A status message for adding the add. + * @emits {@link BushClientEvents.bushPunishRole} + */ + public override async bushAddRole(options: AddRoleOptions): Promise { + // checks + if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ManageRoles)) return addRoleResponse.MISSING_PERMISSIONS; + const ifShouldAddRole = this.#checkIfShouldAddRole(options.role, options.moderator); + if (ifShouldAddRole !== true) return ifShouldAddRole; + + let caseID: string | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return addRoleResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + if (options.addToModlog || options.duration) { + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: options.duration ? ModLogType.TEMP_PUNISHMENT_ROLE : ModLogType.PERM_PUNISHMENT_ROLE, + guild: this.guild, + moderator: moderator.id, + user: this, + reason: 'N/A', + pseudo: !options.addToModlog, + evidence: options.evidence, + hidden: options.silent ?? false + }); + + if (!modlog) return addRoleResponse.MODLOG_ERROR; + caseID = modlog.id; + + if (options.addToModlog || options.duration) { + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, + type: 'role', + user: this, + guild: this.guild, + modlog: modlog.id, + duration: options.duration, + extraInfo: options.role.id + }); + if (!punishmentEntrySuccess) return addRoleResponse.PUNISHMENT_ENTRY_ADD_ERROR; + } + } + + const removeRoleSuccess = await this.roles.add(options.role, `${moderator.tag}`); + if (!removeRoleSuccess) return addRoleResponse.ACTION_ERROR; + + return addRoleResponse.SUCCESS; + })(); + if ( + !( + [addRoleResponse.ACTION_ERROR, addRoleResponse.MODLOG_ERROR, addRoleResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const + ).includes(ret) && + options.addToModlog && + !options.silent + ) + this.client.emit( + 'bushPunishRole', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + options.role, + options.evidence + ); + return ret; + } + + /** + * Remove a role from the user, if it is a punishment create a modlog entry, and destroy a punishment entry if it was temporary or a punishment. + * @param options Options for removing a role from the user. + * @returns A status message for removing the role. + * @emits {@link BushClientEvents.bushPunishRoleRemove} + */ + public override async bushRemoveRole(options: RemoveRoleOptions): Promise { + // checks + if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ManageRoles)) return removeRoleResponse.MISSING_PERMISSIONS; + const ifShouldAddRole = this.#checkIfShouldAddRole(options.role, options.moderator); + if (ifShouldAddRole !== true) return ifShouldAddRole; + + let caseID: string | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return removeRoleResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + if (options.addToModlog) { + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.REMOVE_PUNISHMENT_ROLE, + guild: this.guild, + moderator: moderator.id, + user: this, + reason: 'N/A', + evidence: options.evidence, + hidden: options.silent ?? false + }); + + if (!modlog) return removeRoleResponse.MODLOG_ERROR; + caseID = modlog.id; + + const punishmentEntrySuccess = await Moderation.removePunishmentEntry({ + client: this.client, + type: 'role', + user: this, + guild: this.guild, + extraInfo: options.role.id + }); + + if (!punishmentEntrySuccess) return removeRoleResponse.PUNISHMENT_ENTRY_REMOVE_ERROR; + } + + const removeRoleSuccess = await this.roles.remove(options.role, `${moderator.tag}`); + if (!removeRoleSuccess) return removeRoleResponse.ACTION_ERROR; + + return removeRoleResponse.SUCCESS; + })(); + + if ( + !( + [ + removeRoleResponse.ACTION_ERROR, + removeRoleResponse.MODLOG_ERROR, + removeRoleResponse.PUNISHMENT_ENTRY_REMOVE_ERROR + ] as const + ).includes(ret) && + options.addToModlog && + !options.silent + ) + this.client.emit( + 'bushPunishRoleRemove', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.role, + options.evidence + ); + return ret; + } + + /** + * Check whether or not a role should be added/removed from the user based on hierarchy. + * @param role The role to check if can be modified. + * @param moderator The moderator that is trying to add/remove the role. + * @returns `true` if the role should be added/removed or a string for the reason why it shouldn't. + */ + #checkIfShouldAddRole( + role: Role | Role, + moderator?: GuildMember + ): true | 'user hierarchy' | 'role managed' | 'client hierarchy' { + if (moderator && moderator.roles.highest.position <= role.position && this.guild.ownerId !== this.user.id) { + return shouldAddRoleResponse.USER_HIERARCHY; + } else if (role.managed) { + return shouldAddRoleResponse.ROLE_MANAGED; + } else if (this.guild.members.me!.roles.highest.position <= role.position) { + return shouldAddRoleResponse.CLIENT_HIERARCHY; + } + return true; + } + + /** + * Mute the user, create a modlog entry, creates a punishment entry, and dms the user. + * @param options Options for muting the user. + * @returns A status message for muting the user. + * @emits {@link BushClientEvents.bushMute} + */ + public override async bushMute(options: BushTimedPunishmentOptions): Promise { + // checks + const checks = await Moderation.checkMutePermissions(this.guild); + if (checks !== true) return checks; + + const muteRoleID = (await this.guild.getSetting('muteRole'))!; + const muteRole = this.guild.roles.cache.get(muteRoleID)!; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return muteResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + // add role + const muteSuccess = await this.roles + .add(muteRole, `[Mute] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) + .catch(async (e) => { + await this.client.console.warn('muteRoleAddError', e); + this.client.console.debug(e); + return false; + }); + if (!muteSuccess) return muteResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: options.duration ? ModLogType.TEMP_MUTE : ModLogType.PERM_MUTE, + user: this, + moderator: moderator.id, + reason: options.reason, + duration: options.duration, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + + if (!modlog) return muteResponse.MODLOG_ERROR; + caseID = modlog.id; + + // add punishment entry so they can be unmuted later + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, + type: 'mute', + user: this, + guild: this.guild, + duration: options.duration, + modlog: modlog.id + }); + + if (!punishmentEntrySuccess) return muteResponse.PUNISHMENT_ENTRY_ADD_ERROR; + + if (!options.silent) { + // dm user + const dmSuccess = await this.bushPunishDM('muted', options.reason, options.duration ?? 0, modlog.id); + dmSuccessEvent = dmSuccess; + if (!dmSuccess) return muteResponse.DM_ERROR; + } + + return muteResponse.SUCCESS; + })(); + + if ( + !([muteResponse.ACTION_ERROR, muteResponse.MODLOG_ERROR, muteResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes(ret) && + !options.silent + ) + this.client.emit( + 'bushMute', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Unmute the user, create a modlog entry, remove the punishment entry, and dm the user. + * @param options Options for unmuting the user. + * @returns A status message for unmuting the user. + * @emits {@link BushClientEvents.bushUnmute} + */ + public override async bushUnmute(options: BushPunishmentOptions): Promise { + // checks + const checks = await Moderation.checkMutePermissions(this.guild); + if (checks !== true) return checks; + + const muteRoleID = (await this.guild.getSetting('muteRole'))!; + const muteRole = this.guild.roles.cache.get(muteRoleID)!; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return unmuteResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + // remove role + const muteSuccess = await this.roles + .remove(muteRole, `[Unmute] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) + .catch(async (e) => { + await this.client.console.warn('muteRoleAddError', formatError(e, true)); + return false; + }); + if (!muteSuccess) return unmuteResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.UNMUTE, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + + if (!modlog) return unmuteResponse.MODLOG_ERROR; + caseID = modlog.id; + + // remove mute entry + const removePunishmentEntrySuccess = await Moderation.removePunishmentEntry({ + client: this.client, + type: 'mute', + user: this, + guild: this.guild + }); + + if (!removePunishmentEntrySuccess) return unmuteResponse.PUNISHMENT_ENTRY_REMOVE_ERROR; + + if (!options.silent) { + // dm user + const dmSuccess = await this.bushPunishDM('unmuted', options.reason, undefined, '', false); + dmSuccessEvent = dmSuccess; + if (!dmSuccess) return unmuteResponse.DM_ERROR; + } + + return unmuteResponse.SUCCESS; + })(); + + if ( + !( + [unmuteResponse.ACTION_ERROR, unmuteResponse.MODLOG_ERROR, unmuteResponse.PUNISHMENT_ENTRY_REMOVE_ERROR] as const + ).includes(ret) && + !options.silent + ) + this.client.emit( + 'bushUnmute', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Kick the user, create a modlog entry, and dm the user. + * @param options Options for kicking the user. + * @returns A status message for kicking the user. + * @emits {@link BushClientEvents.bushKick} + */ + public override async bushKick(options: BushPunishmentOptions): Promise { + // checks + if (!this.guild.members.me?.permissions.has(PermissionFlagsBits.KickMembers) || !this.kickable) + return kickResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return kickResponse.CANNOT_RESOLVE_USER; + const ret = await (async () => { + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.KICK, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + if (!modlog) return kickResponse.MODLOG_ERROR; + caseID = modlog.id; + + // dm user + const dmSuccess = options.silent ? null : await this.bushPunishDM('kicked', options.reason, undefined, modlog.id); + dmSuccessEvent = dmSuccess ?? undefined; + + // kick + const kickSuccess = await this.kick(`${moderator?.tag} | ${options.reason ?? 'No reason provided.'}`).catch(() => false); + if (!kickSuccess) return kickResponse.ACTION_ERROR; + + if (dmSuccess === false) return kickResponse.DM_ERROR; + return kickResponse.SUCCESS; + })(); + if (!([kickResponse.ACTION_ERROR, kickResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent) + this.client.emit( + 'bushKick', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Ban the user, create a modlog entry, create a punishment entry, and dm the user. + * @param options Options for banning the user. + * @returns A status message for banning the user. + * @emits {@link BushClientEvents.bushBan} + */ + public override async bushBan(options: BushBanOptions): Promise> { + // checks + if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.BanMembers) || !this.bannable) + return banResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return banResponse.CANNOT_RESOLVE_USER; + + // ignore result, they should still be banned even if their mute cannot be removed + await this.bushUnmute({ + reason: 'User is about to be banned, a mute is no longer necessary.', + moderator: this.guild.members.me!, + silent: true + }); + + const ret = await (async () => { + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, + user: this, + moderator: moderator.id, + reason: options.reason, + duration: options.duration, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + if (!modlog) return banResponse.MODLOG_ERROR; + caseID = modlog.id; + + // dm user + const dmSuccess = options.silent + ? null + : await this.bushPunishDM('banned', options.reason, options.duration ?? 0, modlog.id); + dmSuccessEvent = dmSuccess ?? undefined; + + // ban + const banSuccess = await this.ban({ + reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, + deleteMessageDays: options.deleteDays + }).catch(() => false); + if (!banSuccess) return banResponse.ACTION_ERROR; + + // add punishment entry so they can be unbanned later + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, + type: 'ban', + user: this, + guild: this.guild, + duration: options.duration, + modlog: modlog.id + }); + if (!punishmentEntrySuccess) return banResponse.PUNISHMENT_ENTRY_ADD_ERROR; + + if (!dmSuccess) return banResponse.DM_ERROR; + return banResponse.SUCCESS; + })(); + if ( + !([banResponse.ACTION_ERROR, banResponse.MODLOG_ERROR, banResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes(ret) && + !options.silent + ) + this.client.emit( + 'bushBan', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Prevents a user from speaking in a channel. + * @param options Options for blocking the user. + */ + public override async bushBlock(options: BlockOptions): Promise { + const channel = this.guild.channels.resolve(options.channel); + if (!channel || (!channel.isTextBased() && !channel.isThread())) return blockResponse.INVALID_CHANNEL; + + // checks + if (!channel.permissionsFor(this.guild.members.me!)!.has(PermissionFlagsBits.ManageChannels)) + return blockResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return blockResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + // change channel permissions + const channelToUse = channel.isThread() ? channel.parent! : channel; + const perm = channel.isThread() ? { SendMessagesInThreads: false } : { SendMessages: false }; + const blockSuccess = await channelToUse.permissionOverwrites + .edit(this, perm, { reason: `[Block] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}` }) + .catch(() => false); + if (!blockSuccess) return blockResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: options.duration ? ModLogType.TEMP_CHANNEL_BLOCK : ModLogType.PERM_CHANNEL_BLOCK, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + if (!modlog) return blockResponse.MODLOG_ERROR; + caseID = modlog.id; + + // add punishment entry so they can be unblocked later + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, + type: 'block', + user: this, + guild: this.guild, + duration: options.duration, + modlog: modlog.id, + extraInfo: channel.id + }); + if (!punishmentEntrySuccess) return blockResponse.PUNISHMENT_ENTRY_ADD_ERROR; + + // dm user + const dmSuccess = options.silent + ? null + : await Moderation.punishDM({ + client: this.client, + punishment: 'blocked', + reason: options.reason ?? undefined, + duration: options.duration ?? 0, + modlog: modlog.id, + guild: this.guild, + user: this, + sendFooter: true, + channel: channel.id + }); + dmSuccessEvent = !!dmSuccess; + if (!dmSuccess) return blockResponse.DM_ERROR; + + return blockResponse.SUCCESS; + })(); + + if ( + !([blockResponse.ACTION_ERROR, blockResponse.MODLOG_ERROR, blockResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes( + ret + ) && + !options.silent + ) + this.client.emit( + 'bushBlock', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + dmSuccessEvent!, + channel, + options.evidence + ); + return ret; + } + + /** + * Allows a user to speak in a channel. + * @param options Options for unblocking the user. + */ + public override async bushUnblock(options: UnblockOptions): Promise { + const _channel = this.guild.channels.resolve(options.channel); + if (!_channel || (_channel.type !== ChannelType.GuildText && !_channel.isThread())) return unblockResponse.INVALID_CHANNEL; + const channel = _channel as GuildTextBasedChannel; + + // checks + if (!channel.permissionsFor(this.guild.members.me!)!.has(PermissionFlagsBits.ManageChannels)) + return unblockResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return unblockResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + // change channel permissions + const channelToUse = channel.isThread() ? channel.parent! : channel; + const perm = channel.isThread() ? { SendMessagesInThreads: null } : { SendMessages: null }; + const blockSuccess = await channelToUse.permissionOverwrites + .edit(this, perm, { reason: `[Unblock] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}` }) + .catch(() => false); + if (!blockSuccess) return unblockResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.CHANNEL_UNBLOCK, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + if (!modlog) return unblockResponse.MODLOG_ERROR; + caseID = modlog.id; + + // remove punishment entry + const punishmentEntrySuccess = await Moderation.removePunishmentEntry({ + client: this.client, + type: 'block', + user: this, + guild: this.guild, + extraInfo: channel.id + }); + if (!punishmentEntrySuccess) return unblockResponse.ACTION_ERROR; + + // dm user + const dmSuccess = options.silent + ? null + : await Moderation.punishDM({ + client: this.client, + punishment: 'unblocked', + reason: options.reason ?? undefined, + guild: this.guild, + user: this, + sendFooter: false, + channel: channel.id + }); + dmSuccessEvent = !!dmSuccess; + if (!dmSuccess) return blockResponse.DM_ERROR; + + dmSuccessEvent = !!dmSuccess; + if (!dmSuccess) return unblockResponse.DM_ERROR; + + return unblockResponse.SUCCESS; + })(); + + if ( + !([unblockResponse.ACTION_ERROR, unblockResponse.MODLOG_ERROR, unblockResponse.ACTION_ERROR] as const).includes(ret) && + !options.silent + ) + this.client.emit( + 'bushUnblock', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + dmSuccessEvent!, + channel, + options.evidence + ); + return ret; + } + + /** + * Mutes a user using discord's timeout feature. + * @param options Options for timing out the user. + */ + public override async bushTimeout(options: BushTimeoutOptions): Promise { + // checks + if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ModerateMembers)) return timeoutResponse.MISSING_PERMISSIONS; + + const twentyEightDays = Time.Day * 28; + if (options.duration > twentyEightDays) return timeoutResponse.INVALID_DURATION; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return timeoutResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + // timeout + const timeoutSuccess = await this.timeout( + options.duration, + `${moderator.tag} | ${options.reason ?? 'No reason provided.'}` + ).catch(() => false); + if (!timeoutSuccess) return timeoutResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.TIMEOUT, + user: this, + moderator: moderator.id, + reason: options.reason, + duration: options.duration, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + + if (!modlog) return timeoutResponse.MODLOG_ERROR; + caseID = modlog.id; + + if (!options.silent) { + // dm user + const dmSuccess = await this.bushPunishDM('timedout', options.reason, options.duration, modlog.id); + dmSuccessEvent = dmSuccess; + if (!dmSuccess) return timeoutResponse.DM_ERROR; + } + + return timeoutResponse.SUCCESS; + })(); + + if (!([timeoutResponse.ACTION_ERROR, timeoutResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent) + this.client.emit( + 'bushTimeout', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Removes a timeout from a user. + * @param options Options for removing the timeout. + */ + public override async bushRemoveTimeout(options: BushPunishmentOptions): Promise { + // checks + if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ModerateMembers)) + return removeTimeoutResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return removeTimeoutResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + // remove timeout + const timeoutSuccess = await this.timeout(null, `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`).catch( + () => false + ); + if (!timeoutSuccess) return removeTimeoutResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.REMOVE_TIMEOUT, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + + if (!modlog) return removeTimeoutResponse.MODLOG_ERROR; + caseID = modlog.id; + + if (!options.silent) { + // dm user + const dmSuccess = await this.bushPunishDM('untimedout', options.reason, undefined, '', false); + dmSuccessEvent = dmSuccess; + if (!dmSuccess) return removeTimeoutResponse.DM_ERROR; + } + + return removeTimeoutResponse.SUCCESS; + })(); + + if (!([removeTimeoutResponse.ACTION_ERROR, removeTimeoutResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent) + this.client.emit( + 'bushRemoveTimeout', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Whether or not the user is an owner of the bot. + */ + public override isOwner(): boolean { + return this.client.isOwner(this); + } + + /** + * Whether or not the user is a super user of the bot. + */ + public override isSuperUser(): boolean { + return this.client.isSuperUser(this); + } +} + +/** + * Options for punishing a user. + */ +export interface BushPunishmentOptions { + /** + * The reason for the punishment. + */ + reason?: string | null; + + /** + * The moderator who punished the user. + */ + moderator?: GuildMember; + + /** + * Evidence for the punishment. + */ + evidence?: string; + + /** + * Makes the punishment silent by not sending the user a punishment dm and not broadcasting the event to be logged. + */ + silent?: boolean; +} + +/** + * Punishment options for punishments that can be temporary. + */ +export interface BushTimedPunishmentOptions extends BushPunishmentOptions { + /** + * The duration of the punishment. + */ + duration?: number; +} + +/** + * Options for a role add punishment. + */ +export interface AddRoleOptions extends BushTimedPunishmentOptions { + /** + * The role to add to the user. + */ + role: Role; + + /** + * Whether to create a modlog entry for this punishment. + */ + addToModlog: boolean; +} + +/** + * Options for a role remove punishment. + */ +export interface RemoveRoleOptions extends BushTimedPunishmentOptions { + /** + * The role to remove from the user. + */ + role: Role; + + /** + * Whether to create a modlog entry for this punishment. + */ + addToModlog: boolean; +} + +/** + * Options for banning a user. + */ +export interface BushBanOptions extends BushTimedPunishmentOptions { + /** + * The number of days to delete the user's messages for. + */ + deleteDays?: number; +} + +/** + * Options for blocking a user from a channel. + */ +export interface BlockOptions extends BushTimedPunishmentOptions { + /** + * The channel to block the user from. + */ + channel: GuildChannelResolvable; +} + +/** + * Options for unblocking a user from a channel. + */ +export interface UnblockOptions extends BushPunishmentOptions { + /** + * The channel to unblock the user from. + */ + channel: GuildChannelResolvable; +} + +/** + * Punishment options for punishments that can be temporary. + */ +export interface BushTimeoutOptions extends BushPunishmentOptions { + /** + * The duration of the punishment. + */ + duration: number; +} + +export const basePunishmentResponse = Object.freeze({ + SUCCESS: 'success', + MODLOG_ERROR: 'error creating modlog entry', + ACTION_ERROR: 'error performing action', + CANNOT_RESOLVE_USER: 'cannot resolve user' +} as const); + +export const dmResponse = Object.freeze({ + ...basePunishmentResponse, + DM_ERROR: 'failed to dm' +} as const); + +export const permissionsResponse = Object.freeze({ + MISSING_PERMISSIONS: 'missing permissions' +} as const); + +export const punishmentEntryAdd = Object.freeze({ + PUNISHMENT_ENTRY_ADD_ERROR: 'error creating punishment entry' +} as const); + +export const punishmentEntryRemove = Object.freeze({ + PUNISHMENT_ENTRY_REMOVE_ERROR: 'error removing punishment entry' +} as const); + +export const shouldAddRoleResponse = Object.freeze({ + USER_HIERARCHY: 'user hierarchy', + CLIENT_HIERARCHY: 'client hierarchy', + ROLE_MANAGED: 'role managed' +} as const); + +export const baseBlockResponse = Object.freeze({ + INVALID_CHANNEL: 'invalid channel' +} as const); + +export const baseMuteResponse = Object.freeze({ + NO_MUTE_ROLE: 'no mute role', + MUTE_ROLE_INVALID: 'invalid mute role', + MUTE_ROLE_NOT_MANAGEABLE: 'mute role not manageable' +} as const); + +export const warnResponse = Object.freeze({ + ...dmResponse +} as const); + +export const addRoleResponse = Object.freeze({ + ...basePunishmentResponse, + ...permissionsResponse, + ...shouldAddRoleResponse, + ...punishmentEntryAdd +} as const); + +export const removeRoleResponse = Object.freeze({ + ...basePunishmentResponse, + ...permissionsResponse, + ...shouldAddRoleResponse, + ...punishmentEntryRemove +} as const); + +export const muteResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + ...baseMuteResponse, + ...punishmentEntryAdd +} as const); + +export const unmuteResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + ...baseMuteResponse, + ...punishmentEntryRemove +} as const); + +export const kickResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse +} as const); + +export const banResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + ...punishmentEntryAdd, + ALREADY_BANNED: 'already banned' +} as const); + +export const blockResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + ...baseBlockResponse, + ...punishmentEntryAdd +}); + +export const unblockResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + ...baseBlockResponse, + ...punishmentEntryRemove +}); + +export const timeoutResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + INVALID_DURATION: 'duration too long' +} as const); + +export const removeTimeoutResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse +} as const); + +/** + * Response returned when warning a user. + */ +export type WarnResponse = ValueOf; + +/** + * Response returned when adding a role to a user. + */ +export type AddRoleResponse = ValueOf; + +/** + * Response returned when removing a role from a user. + */ +export type RemoveRoleResponse = ValueOf; + +/** + * Response returned when muting a user. + */ +export type MuteResponse = ValueOf; + +/** + * Response returned when unmuting a user. + */ +export type UnmuteResponse = ValueOf; + +/** + * Response returned when kicking a user. + */ +export type KickResponse = ValueOf; + +/** + * Response returned when banning a user. + */ +export type BanResponse = ValueOf; + +/** + * Response returned when blocking a user. + */ +export type BlockResponse = ValueOf; + +/** + * Response returned when unblocking a user. + */ +export type UnblockResponse = ValueOf; + +/** + * Response returned when timing out a user. + */ +export type TimeoutResponse = ValueOf; + +/** + * Response returned when removing a timeout from a user. + */ +export type RemoveTimeoutResponse = ValueOf; + +/** + * @typedef {BushClientEvents} VSCodePleaseDontRemove + */ diff --git a/lib/extensions/discord.js/ExtendedMessage.ts b/lib/extensions/discord.js/ExtendedMessage.ts new file mode 100644 index 0000000..1bb0904 --- /dev/null +++ b/lib/extensions/discord.js/ExtendedMessage.ts @@ -0,0 +1,12 @@ +import { CommandUtil } from 'discord-akairo'; +import { Message, type Client } from 'discord.js'; +import type { RawMessageData } from 'discord.js/typings/rawDataTypes.js'; + +export class ExtendedMessage extends Message { + public declare util: CommandUtil; + + public constructor(client: Client, data: RawMessageData) { + super(client, data); + this.util = new CommandUtil(client.commandHandler, this); + } +} diff --git a/lib/extensions/discord.js/ExtendedUser.ts b/lib/extensions/discord.js/ExtendedUser.ts new file mode 100644 index 0000000..23de523 --- /dev/null +++ b/lib/extensions/discord.js/ExtendedUser.ts @@ -0,0 +1,35 @@ +import { User, type Partialize } from 'discord.js'; + +declare module 'discord.js' { + export interface User { + /** + * Indicates whether the user is an owner of the bot. + */ + isOwner(): boolean; + /** + * Indicates whether the user is a superuser of the bot. + */ + isSuperUser(): boolean; + } +} + +export type PartialBushUser = Partialize; + +/** + * Represents a user on Discord. + */ +export class ExtendedUser extends User { + /** + * Indicates whether the user is an owner of the bot. + */ + public override isOwner(): boolean { + return this.client.isOwner(this); + } + + /** + * Indicates whether the user is a superuser of the bot. + */ + public override isSuperUser(): boolean { + return this.client.isSuperUser(this); + } +} diff --git a/lib/extensions/global.ts b/lib/extensions/global.ts new file mode 100644 index 0000000..a9020d7 --- /dev/null +++ b/lib/extensions/global.ts @@ -0,0 +1,13 @@ +/* eslint-disable no-var */ +declare global { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface ReadonlyArray { + includes}`>( + this: ReadonlyArray, + searchElement: S, + fromIndex?: number + ): searchElement is R & S; + } +} + +export {}; diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..5a8ecde --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,56 @@ +export * from './automod/AutomodShared.js'; +export * from './automod/MemberAutomod.js'; +export * from './automod/MessageAutomod.js'; +export * from './automod/PresenceAutomod.js'; +export * from './common/BushCache.js'; +export * from './common/ButtonPaginator.js'; +export * from './common/CanvasProgressBar.js'; +export * from './common/ConfirmationPrompt.js'; +export * from './common/DeleteButton.js'; +export * as Moderation from './common/Moderation.js'; +export type { + AppealButtonId, + CreateModLogEntryOptions, + CreatePunishmentEntryOptions, + PunishDMOptions, + PunishmentTypeDM, + PunishmentTypePresent, + RemovePunishmentEntryOptions, + SimpleCreateModLogEntryOptions +} from './common/Moderation.js'; +export * from './extensions/discord-akairo/BushArgumentTypeCaster.js'; +export * from './extensions/discord-akairo/BushClient.js'; +export * from './extensions/discord-akairo/BushCommand.js'; +export * from './extensions/discord-akairo/BushCommandHandler.js'; +export * from './extensions/discord-akairo/BushInhibitor.js'; +export * from './extensions/discord-akairo/BushInhibitorHandler.js'; +export * from './extensions/discord-akairo/BushListener.js'; +export * from './extensions/discord-akairo/BushListenerHandler.js'; +export * from './extensions/discord-akairo/BushTask.js'; +export * from './extensions/discord-akairo/BushTaskHandler.js'; +export * from './extensions/discord-akairo/SlashMessage.js'; +export type { BushClientEvents } from './extensions/discord.js/BushClientEvents.js'; +export * from './extensions/discord.js/ExtendedGuild.js'; +export * from './extensions/discord.js/ExtendedGuildMember.js'; +export * from './extensions/discord.js/ExtendedMessage.js'; +export * from './extensions/discord.js/ExtendedUser.js'; +export * from './models/BaseModel.js'; +export * from './models/instance/ActivePunishment.js'; +export * from './models/instance/Guild.js'; +export * from './models/instance/Highlight.js'; +export * from './models/instance/Level.js'; +export * from './models/instance/ModLog.js'; +export * from './models/instance/Reminder.js'; +export * from './models/instance/StickyRole.js'; +export * from './models/shared/Global.js'; +export * from './models/shared/MemberCount.js'; +export * from './models/shared/Shared.js'; +export * from './models/shared/Stat.js'; +export type { BushInspectOptions } from './types/BushInspectOptions.js'; +export type { CodeBlockLang } from './types/CodeBlockLang.js'; +export * from './utils/AllowedMentions.js'; +export * as Arg from './utils/Arg.js'; +export * from './utils/BushConstants.js'; +export * from './utils/BushLogger.js'; +export * from './utils/BushUtils.js'; +export * as Format from './utils/Format.js'; diff --git a/lib/models/BaseModel.ts b/lib/models/BaseModel.ts new file mode 100644 index 0000000..8fba5e5 --- /dev/null +++ b/lib/models/BaseModel.ts @@ -0,0 +1,13 @@ +import { Model } from 'sequelize'; + +export abstract class BaseModel extends Model { + /** + * The date when the row was created. + */ + public declare readonly createdAt: Date; + + /** + * The date when the row was last updated. + */ + public declare readonly updatedAt: Date; +} diff --git a/lib/models/instance/ActivePunishment.ts b/lib/models/instance/ActivePunishment.ts new file mode 100644 index 0000000..38012ca --- /dev/null +++ b/lib/models/instance/ActivePunishment.ts @@ -0,0 +1,94 @@ +import { type Snowflake } from 'discord.js'; +import { nanoid } from 'nanoid'; +import { type Sequelize } from 'sequelize'; +import { BaseModel } from '../BaseModel.js'; +const { DataTypes } = (await import('sequelize')).default; + +export enum ActivePunishmentType { + BAN = 'BAN', + MUTE = 'MUTE', + ROLE = 'ROLE', + BLOCK = 'BLOCK' +} + +export interface ActivePunishmentModel { + id: string; + type: ActivePunishmentType; + user: Snowflake; + guild: Snowflake; + extraInfo: Snowflake; + expires: Date | null; + modlog: string; +} + +export interface ActivePunishmentModelCreationAttributes { + id?: string; + type: ActivePunishmentType; + user: Snowflake; + guild: Snowflake; + extraInfo?: Snowflake; + expires?: Date; + modlog: string; +} + +/** + * Keeps track of active punishments so they can be removed later. + */ +export class ActivePunishment + extends BaseModel + implements ActivePunishmentModel +{ + /** + * The ID of this punishment (no real use just for a primary key) + */ + public declare id: string; + + /** + * The type of punishment. + */ + public declare type: ActivePunishmentType; + + /** + * The user who is punished. + */ + public declare user: Snowflake; + + /** + * The guild they are punished in. + */ + public declare guild: Snowflake; + + /** + * Additional info about the punishment if applicable. The channel id for channel blocks and role for punishment roles. + */ + public declare extraInfo: Snowflake; + + /** + * The date when this punishment expires (optional). + */ + public declare expires: Date | null; + + /** + * The reference to the modlog entry. + */ + public declare modlog: string; + + /** + * Initializes the model. + * @param sequelize The sequelize instance. + */ + public static initModel(sequelize: Sequelize): void { + ActivePunishment.init( + { + id: { type: DataTypes.STRING, primaryKey: true, defaultValue: nanoid }, + type: { type: DataTypes.STRING, allowNull: false }, + user: { type: DataTypes.STRING, allowNull: false }, + guild: { type: DataTypes.STRING, allowNull: false, references: { model: 'Guilds', key: 'id' } }, + extraInfo: { type: DataTypes.STRING, allowNull: true }, + expires: { type: DataTypes.DATE, allowNull: true }, + modlog: { type: DataTypes.STRING, allowNull: true, references: { model: 'ModLogs', key: 'id' } } + }, + { sequelize } + ); + } +} diff --git a/lib/models/instance/Guild.ts b/lib/models/instance/Guild.ts new file mode 100644 index 0000000..f258d48 --- /dev/null +++ b/lib/models/instance/Guild.ts @@ -0,0 +1,431 @@ +import { ChannelType, Constants, type Snowflake } from 'discord.js'; +import { type Sequelize } from 'sequelize'; +import { BadWordDetails } from '../../automod/AutomodShared.js'; +import { type BushClient } from '../../extensions/discord-akairo/BushClient.js'; +import { BaseModel } from '../BaseModel.js'; +const { DataTypes } = (await import('sequelize')).default; + +export interface GuildModel { + id: Snowflake; + prefix: string; + autoPublishChannels: Snowflake[]; + blacklistedChannels: Snowflake[]; + blacklistedUsers: Snowflake[]; + welcomeChannel: Snowflake | null; + muteRole: Snowflake | null; + punishmentEnding: string | null; + disabledCommands: string[]; + lockdownChannels: Snowflake[]; + autoModPhases: BadWordDetails[]; + enabledFeatures: GuildFeatures[]; + joinRoles: Snowflake[]; + logChannels: LogChannelDB; + bypassChannelBlacklist: Snowflake[]; + noXpChannels: Snowflake[]; + levelRoles: { [level: number]: Snowflake }; + levelUpChannel: Snowflake | null; +} + +export interface GuildModelCreationAttributes { + id: Snowflake; + prefix?: string; + autoPublishChannels?: Snowflake[]; + blacklistedChannels?: Snowflake[]; + blacklistedUsers?: Snowflake[]; + welcomeChannel?: Snowflake; + muteRole?: Snowflake; + punishmentEnding?: string; + disabledCommands?: string[]; + lockdownChannels?: Snowflake[]; + autoModPhases?: BadWordDetails[]; + enabledFeatures?: GuildFeatures[]; + joinRoles?: Snowflake[]; + logChannels?: LogChannelDB; + bypassChannelBlacklist?: Snowflake[]; + noXpChannels?: Snowflake[]; + levelRoles?: { [level: number]: Snowflake }; + levelUpChannel?: Snowflake; +} + +/** + * Settings for a guild. + */ +export class Guild extends BaseModel implements GuildModel { + /** + * The ID of the guild + */ + public declare id: Snowflake; + + /** + * The bot's prefix for the guild + */ + public declare prefix: string; + + /** + * Channels that will have their messages automatically published + */ + public declare autoPublishChannels: Snowflake[]; + + /** + * Channels where the bot won't respond in. + */ + public declare blacklistedChannels: Snowflake[]; + + /** + * Users that the bot ignores in this guild + */ + public declare blacklistedUsers: Snowflake[]; + + /** + * The channels where the welcome messages are sent + */ + public declare welcomeChannel: Snowflake | null; + + /** + * The role given out when muting someone + */ + public declare muteRole: Snowflake | null; + + /** + * The message that gets sent after someone gets a punishment dm + */ + public declare punishmentEnding: string | null; + + /** + * Guild specific disabled commands + */ + public declare disabledCommands: string[]; + + /** + * Channels that should get locked down when the lockdown command gets used. + */ + public declare lockdownChannels: Snowflake[]; + + /** + * Custom automod phases + */ + public declare autoModPhases: BadWordDetails[]; + + /** + * The features enabled in a guild + */ + public declare enabledFeatures: GuildFeatures[]; + + /** + * The roles to assign to a user if they are not assigned sticky roles + */ + public declare joinRoles: Snowflake[]; + + /** + * The channels where logging messages will be sent. + */ + public declare logChannels: LogChannelDB; + + /** + * These users will be able to use commands in channels blacklisted + */ + public declare bypassChannelBlacklist: Snowflake[]; + + /** + * Channels where users will not earn xp for leveling. + */ + public declare noXpChannels: Snowflake[]; + + /** + * What roles get given to users when they reach certain levels. + */ + public declare levelRoles: { [level: number]: Snowflake }; + + /** + * The channel to send level up messages in instead of last channel. + */ + public declare levelUpChannel: Snowflake | null; + + /** + * Initializes the model. + * @param sequelize The sequelize instance. + */ + public static initModel(sequelize: Sequelize, client: BushClient): void { + Guild.init( + { + id: { type: DataTypes.STRING, primaryKey: true }, + prefix: { type: DataTypes.TEXT, allowNull: false, defaultValue: client.config.prefix }, + autoPublishChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + blacklistedChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + blacklistedUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + welcomeChannel: { type: DataTypes.STRING, allowNull: true }, + muteRole: { type: DataTypes.STRING, allowNull: true }, + punishmentEnding: { type: DataTypes.TEXT, allowNull: true }, + disabledCommands: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + lockdownChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + autoModPhases: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + enabledFeatures: { + type: DataTypes.JSONB, + allowNull: false, + defaultValue: Object.keys(guildFeaturesObj).filter( + (key) => guildFeaturesObj[key as keyof typeof guildFeaturesObj].default + ) + }, + joinRoles: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + logChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: {} }, + bypassChannelBlacklist: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + noXpChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + levelRoles: { type: DataTypes.JSONB, allowNull: false, defaultValue: {} }, + levelUpChannel: { type: DataTypes.STRING, allowNull: true } + }, + { sequelize } + ); + } +} + +export type BaseGuildSetting = 'channel' | 'role' | 'user'; +export type GuildNoArraySetting = 'string' | 'custom' | BaseGuildSetting; +export type GuildSettingType = GuildNoArraySetting | `${BaseGuildSetting}-array`; + +export interface GuildSetting { + name: string; + description: string; + type: GuildSettingType; + subType: ChannelType[] | undefined; + configurable: boolean; + replaceNullWith: () => string | null; +} +const asGuildSetting = (et: { [K in keyof T]: PartialBy }) => { + for (const key in et) { + et[key].subType ??= undefined; + et[key].configurable ??= true; + et[key].replaceNullWith ??= () => null; + } + return et as { [K in keyof T]: GuildSetting }; +}; + +const { default: config } = await import('../../../config/options.js'); + +export const guildSettingsObj = asGuildSetting({ + prefix: { + name: 'Prefix', + description: 'The phrase required to trigger text commands in this server.', + type: 'string', + replaceNullWith: () => config.prefix + }, + autoPublishChannels: { + name: 'Auto Publish Channels', + description: 'Channels were every message is automatically published.', + type: 'channel-array', + subType: [ChannelType.GuildNews] + }, + welcomeChannel: { + name: 'Welcome Channel', + description: 'The channel where the bot will send join and leave message.', + type: 'channel', + subType: [ + ChannelType.GuildText, + ChannelType.GuildNews, + ChannelType.GuildNewsThread, + ChannelType.GuildPublicThread, + ChannelType.GuildPrivateThread + ] + }, + muteRole: { + name: 'Mute Role', + description: 'The role assigned when muting someone.', + type: 'role' + }, + punishmentEnding: { + name: 'Punishment Ending', + description: 'The message after punishment information to a user in a dm.', + type: 'string' + }, + lockdownChannels: { + name: 'Lockdown Channels', + description: 'Channels that are locked down when a mass lockdown is specified.', + type: 'channel-array', + subType: [ChannelType.GuildText] + }, + joinRoles: { + name: 'Join Roles', + description: 'Roles assigned to users on join who do not have sticky role information.', + type: 'role-array' + }, + bypassChannelBlacklist: { + name: 'Bypass Channel Blacklist', + description: 'These users will be able to use commands in channels blacklisted.', + type: 'user-array' + }, + logChannels: { + name: 'Log Channels', + description: 'The channel were logs are sent.', + type: 'custom', + subType: [ChannelType.GuildText], + configurable: false + }, + autoModPhases: { + name: 'Automod Phases', + description: 'Custom phrases to be detected by automod.', + type: 'custom', + configurable: false + }, + noXpChannels: { + name: 'No Xp Channels', + description: 'Channels where users will not earn xp for leveling.', + type: 'channel-array', + subType: Constants.TextBasedChannelTypes.filter((type) => type !== ChannelType.DM) + }, + levelRoles: { + name: 'Level Roles', + description: 'What roles get given to users when they reach certain levels.', + type: 'custom', + configurable: false + }, + levelUpChannel: { + name: 'Level Up Channel', + description: 'The channel to send level up messages in instead of last channel.', + type: 'channel', + subType: Constants.TextBasedChannelTypes.filter((type) => type !== ChannelType.DM) + } +}); + +export type GuildSettings = keyof typeof guildSettingsObj; +export const settingsArr = Object.keys(guildSettingsObj).filter( + (s) => guildSettingsObj[s as GuildSettings].configurable +) as GuildSettings[]; + +interface GuildFeature { + name: string; + description: string; + default: boolean; + hidden: boolean; +} + +type PartialBy = Omit & Partial>; + +const asGuildFeature = (gf: { [K in keyof T]: PartialBy }): { + [K in keyof T]: GuildFeature; +} => { + for (const key in gf) { + gf[key].hidden ??= false; + gf[key].default ??= false; + } + return gf as { [K in keyof T]: GuildFeature }; +}; + +export const guildFeaturesObj = asGuildFeature({ + automod: { + name: 'Automod', + description: 'Deletes offensive content as well as phishing links.' + }, + excludeDefaultAutomod: { + name: 'Exclude Default Automod', + description: 'Opt out of using the default automod options.' + }, + excludeAutomodScamLinks: { + name: 'Exclude Automod Scam Links', + description: 'Opt out of having automod delete scam links.' + }, + delScamMentions: { + name: 'Delete Scam Mentions', + description: 'Deletes messages with @everyone and @here mentions that have common scam phrases.' + }, + automodPresence: { + name: 'Automod Presence', + description: 'Logs presence changes that trigger automod.', + hidden: true + }, + automodMembers: { + name: 'Automod Members', + description: "Logs members' usernames and nicknames changes if they match automod." + }, + blacklistedFile: { + name: 'Blacklisted File', + description: 'Automatically deletes malicious files.' + }, + autoPublish: { + name: 'Auto Publish', + description: 'Publishes messages in configured announcement channels.' + }, + // todo implement a better auto thread system + autoThread: { + name: 'Auto Thread', + description: 'Creates a new thread for messages in configured channels.', + hidden: true + }, + perspectiveApi: { + name: 'Perspective API', + description: 'Use the Perspective API to detect toxicity.', + hidden: true + }, + boosterMessageReact: { + name: 'Booster Message React', + description: 'Reacts to booster messages with the boost emoji.' + }, + leveling: { + name: 'Leveling', + description: "Tracks users' messages and assigns them xp." + }, + sendLevelUpMessages: { + name: 'Send Level Up Messages', + description: 'Send a message when a user levels up.', + default: true + }, + stickyRoles: { + name: 'Sticky Roles', + description: 'Restores past roles to a user when they rejoin.' + }, + reporting: { + name: 'Reporting', + description: 'Allow users to make reports.' + }, + modsCanPunishMods: { + name: 'Mods Can Punish Mods', + description: 'Allow moderators to punish other moderators.' + }, + logManualPunishments: { + name: 'Log Manual Punishments', + description: "Adds manual punishment to the user's modlogs and the logging channels.", + default: true + }, + punishmentAppeals: { + name: 'Punishment Appeals', + description: 'Allow users to appeal their punishments and send the appeal to the configured channel.', + hidden: true + }, + highlight: { + name: 'Highlight', + description: 'Allows the highlight command to be used.', + default: true + } +}); + +export const guildLogsObj = { + automod: { + description: 'Sends a message in this channel every time automod is activated.', + configurable: true + }, + moderation: { + description: 'Sends a message in this channel every time a moderation action is performed.', + configurable: true + }, + report: { + description: 'Logs user reports.', + configurable: true + }, + error: { + description: 'Logs errors that occur with the bot.', + configurable: true + }, + appeals: { + description: 'Where punishment appeals are sent.', + configurable: false + } +}; + +export type GuildLogType = keyof typeof guildLogsObj; +export const guildLogsArr = Object.keys(guildLogsObj).filter( + (s) => guildLogsObj[s as GuildLogType].configurable +) as GuildLogType[]; +type LogChannelDB = { [x in keyof typeof guildLogsObj]?: Snowflake }; + +export type GuildFeatures = keyof typeof guildFeaturesObj; +export const guildFeaturesArr: GuildFeatures[] = Object.keys(guildFeaturesObj).filter( + (f) => !guildFeaturesObj[f as keyof typeof guildFeaturesObj].hidden +) as GuildFeatures[]; diff --git a/lib/models/instance/Highlight.ts b/lib/models/instance/Highlight.ts new file mode 100644 index 0000000..5889fad --- /dev/null +++ b/lib/models/instance/Highlight.ts @@ -0,0 +1,81 @@ +import { type Snowflake } from 'discord.js'; +import { nanoid } from 'nanoid'; +import { type Sequelize } from 'sequelize'; +import { BaseModel } from '../BaseModel.js'; +const { DataTypes } = (await import('sequelize')).default; + +export interface HighlightModel { + pk: string; + user: Snowflake; + guild: Snowflake; + words: HighlightWord[]; + blacklistedChannels: Snowflake[]; + blacklistedUsers: Snowflake[]; +} + +export interface HighLightCreationAttributes { + pk?: string; + user: Snowflake; + guild: Snowflake; + words?: HighlightWord[]; + blacklistedChannels?: Snowflake[]; + blacklistedUsers?: Snowflake[]; +} + +export interface HighlightWord { + word: string; + regex: boolean; +} + +/** + * List of words that should cause the user to be notified for if found in the specified guild. + */ +export class Highlight extends BaseModel implements HighlightModel { + /** + * The primary key of the highlight. + */ + public declare pk: string; + + /** + * The user that the highlight is for. + */ + public declare user: Snowflake; + + /** + * The guild to look for highlights in. + */ + public declare guild: Snowflake; + + /** + * The words to look for. + */ + public declare words: HighlightWord[]; + + /** + * Channels that the user choose to ignore highlights in. + */ + public declare blacklistedChannels: Snowflake[]; + + /** + * Users that the user choose to ignore highlights from. + */ + public declare blacklistedUsers: Snowflake[]; + + /** + * Initializes the model. + * @param sequelize The sequelize instance. + */ + public static initModel(sequelize: Sequelize): void { + Highlight.init( + { + pk: { type: DataTypes.STRING, primaryKey: true, defaultValue: nanoid }, + user: { type: DataTypes.STRING, allowNull: false }, + guild: { type: DataTypes.STRING, allowNull: false }, + words: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + blacklistedChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + blacklistedUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] } + }, + { sequelize } + ); + } +} diff --git a/lib/models/instance/Level.ts b/lib/models/instance/Level.ts new file mode 100644 index 0000000..d8d16f0 --- /dev/null +++ b/lib/models/instance/Level.ts @@ -0,0 +1,70 @@ +import { type Snowflake } from 'discord.js'; +import { type Sequelize } from 'sequelize'; +import { BaseModel } from '../BaseModel.js'; +const { DataTypes } = (await import('sequelize')).default; + +export interface LevelModel { + user: Snowflake; + guild: Snowflake; + xp: number; +} + +export interface LevelModelCreationAttributes { + user: Snowflake; + guild: Snowflake; + xp?: number; +} + +/** + * Leveling information for a user in a guild. + */ +export class Level extends BaseModel implements LevelModel { + /** + * The user's id. + */ + public declare user: Snowflake; + + /** + * The guild where the user is gaining xp. + */ + public declare guild: Snowflake; + + /** + * The user's xp. + */ + public declare xp: number; + + /** + * The user's level. + */ + public get level(): number { + return Level.convertXpToLevel(this.xp); + } + + /** + * Initializes the model. + * @param sequelize The sequelize instance. + */ + public static initModel(sequelize: Sequelize): void { + Level.init( + { + user: { type: DataTypes.STRING, allowNull: false }, + guild: { type: DataTypes.STRING, allowNull: false }, + xp: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 } + }, + { sequelize } + ); + } + + public static convertXpToLevel(xp: number): number { + return Math.floor((-25 + Math.sqrt(625 + 200 * xp)) / 100); + } + + public static convertLevelToXp(level: number): number { + return 50 * level * level + 25 * level; // 50x² + 25x + } + + public static genRandomizedXp(): number { + return Math.floor(Math.random() * (40 - 15 + 1)) + 15; + } +} diff --git a/lib/models/instance/ModLog.ts b/lib/models/instance/ModLog.ts new file mode 100644 index 0000000..c25f043 --- /dev/null +++ b/lib/models/instance/ModLog.ts @@ -0,0 +1,127 @@ +import { type Snowflake } from 'discord.js'; +import { nanoid } from 'nanoid'; +import { type Sequelize } from 'sequelize'; +import { BaseModel } from '../BaseModel.js'; +const { DataTypes } = (await import('sequelize')).default; + +export enum ModLogType { + PERM_BAN = 'PERM_BAN', + TEMP_BAN = 'TEMP_BAN', + UNBAN = 'UNBAN', + KICK = 'KICK', + PERM_MUTE = 'PERM_MUTE', + TEMP_MUTE = 'TEMP_MUTE', + UNMUTE = 'UNMUTE', + WARN = 'WARN', + PERM_PUNISHMENT_ROLE = 'PERM_PUNISHMENT_ROLE', + TEMP_PUNISHMENT_ROLE = 'TEMP_PUNISHMENT_ROLE', + REMOVE_PUNISHMENT_ROLE = 'REMOVE_PUNISHMENT_ROLE', + PERM_CHANNEL_BLOCK = 'PERM_CHANNEL_BLOCK', + TEMP_CHANNEL_BLOCK = 'TEMP_CHANNEL_BLOCK', + CHANNEL_UNBLOCK = 'CHANNEL_UNBLOCK', + TIMEOUT = 'TIMEOUT', + REMOVE_TIMEOUT = 'REMOVE_TIMEOUT' +} + +export interface ModLogModel { + id: string; + type: ModLogType; + user: Snowflake; + moderator: Snowflake; + reason: string | null; + duration: number | null; + guild: Snowflake; + evidence: string; + pseudo: boolean; + hidden: boolean; +} + +export interface ModLogModelCreationAttributes { + id?: string; + type: ModLogType; + user: Snowflake; + moderator: Snowflake; + reason?: string | null; + duration?: number; + guild: Snowflake; + evidence?: string; + pseudo?: boolean; + hidden?: boolean; +} + +/** + * A mod log case. + */ +export class ModLog extends BaseModel implements ModLogModel { + /** + * The primary key of the modlog entry. + */ + public declare id: string; + + /** + * The type of punishment. + */ + public declare type: ModLogType; + + /** + * The user being punished. + */ + public declare user: Snowflake; + + /** + * The user carrying out the punishment. + */ + public declare moderator: Snowflake; + + /** + * The reason the user is getting punished. + */ + public declare reason: string | null; + + /** + * The amount of time the user is getting punished for. + */ + public declare duration: number | null; + + /** + * The guild the user is getting punished in. + */ + public declare guild: Snowflake; + + /** + * Evidence of what the user is getting punished for. + */ + public declare evidence: string; + + /** + * Not an actual modlog just used so a punishment entry can be made. + */ + public declare pseudo: boolean; + + /** + * Hides from the modlog command unless show hidden is specified. + */ + public declare hidden: boolean; + + /** + * Initializes the model. + * @param sequelize The sequelize instance. + */ + public static initModel(sequelize: Sequelize): void { + ModLog.init( + { + id: { type: DataTypes.STRING, primaryKey: true, allowNull: false, defaultValue: nanoid }, + type: { type: DataTypes.STRING, allowNull: false }, //? This is not an enum because of a sequelize issue: https://github.com/sequelize/sequelize/issues/2554 + user: { type: DataTypes.STRING, allowNull: false }, + moderator: { type: DataTypes.STRING, allowNull: false }, + duration: { type: DataTypes.STRING, allowNull: true }, + reason: { type: DataTypes.TEXT, allowNull: true }, + guild: { type: DataTypes.STRING, references: { model: 'Guilds', key: 'id' } }, + evidence: { type: DataTypes.TEXT, allowNull: true }, + pseudo: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }, + hidden: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false } + }, + { sequelize } + ); + } +} diff --git a/lib/models/instance/Reminder.ts b/lib/models/instance/Reminder.ts new file mode 100644 index 0000000..964ea63 --- /dev/null +++ b/lib/models/instance/Reminder.ts @@ -0,0 +1,84 @@ +import { Snowflake } from 'discord.js'; +import { nanoid } from 'nanoid'; +import { type Sequelize } from 'sequelize'; +import { BaseModel } from '../BaseModel.js'; +const { DataTypes } = (await import('sequelize')).default; + +export interface ReminderModel { + id: string; + user: Snowflake; + messageUrl: string; + content: string; + created: Date; + expires: Date; + notified: boolean; +} + +export interface ReminderModelCreationAttributes { + id?: string; + user: Snowflake; + messageUrl: string; + content: string; + created: Date; + expires: Date; + notified?: boolean; +} + +/** + * Represents a reminder the a user has set. + */ +export class Reminder extends BaseModel implements ReminderModel { + /** + * The id of the reminder. + */ + public declare id: string; + + /** + * The user that the reminder is for. + */ + public declare user: Snowflake; + + /** + * The url of the message where the reminder was created. + */ + public declare messageUrl: string; + + /** + * The content of the reminder. + */ + public declare content: string; + + /** + * The date the reminder was created. + */ + public declare created: Date; + + /** + * The date when the reminder expires. + */ + public declare expires: Date; + + /** + * Whether the user has been notified about the reminder. + */ + public declare notified: boolean; + + /** + * Initializes the model. + * @param sequelize The sequelize instance. + */ + public static initModel(sequelize: Sequelize): void { + Reminder.init( + { + id: { type: DataTypes.STRING, primaryKey: true, defaultValue: nanoid }, + user: { type: DataTypes.STRING, allowNull: false }, + messageUrl: { type: DataTypes.STRING, allowNull: false }, + content: { type: DataTypes.TEXT, allowNull: false }, + created: { type: DataTypes.DATE, allowNull: false }, + expires: { type: DataTypes.DATE, allowNull: false }, + notified: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false } + }, + { sequelize } + ); + } +} diff --git a/lib/models/instance/StickyRole.ts b/lib/models/instance/StickyRole.ts new file mode 100644 index 0000000..00e98ce --- /dev/null +++ b/lib/models/instance/StickyRole.ts @@ -0,0 +1,58 @@ +import { type Snowflake } from 'discord.js'; +import { type Sequelize } from 'sequelize'; +import { BaseModel } from '../BaseModel.js'; +const { DataTypes } = (await import('sequelize')).default; + +export interface StickyRoleModel { + user: Snowflake; + guild: Snowflake; + roles: Snowflake[]; + nickname: string; +} +export interface StickyRoleModelCreationAttributes { + user: Snowflake; + guild: Snowflake; + roles: Snowflake[]; + nickname?: string; +} + +/** + * Information about a user's roles and nickname when they leave a guild. + */ +export class StickyRole extends BaseModel implements StickyRoleModel { + /** + * The id of the user the roles belongs to. + */ + public declare user: Snowflake; + + /** + * The guild where this should happen. + */ + public declare guild: Snowflake; + + /** + * The roles that the user should have returned + */ + public declare roles: Snowflake[]; + + /** + * The user's previous nickname + */ + public declare nickname: string; + + /** + * Initializes the model. + * @param sequelize The sequelize instance. + */ + public static initModel(sequelize: Sequelize): void { + StickyRole.init( + { + user: { type: DataTypes.STRING, allowNull: false }, + guild: { type: DataTypes.STRING, allowNull: false }, + roles: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + nickname: { type: DataTypes.STRING, allowNull: true } + }, + { sequelize } + ); + } +} diff --git a/lib/models/shared/Global.ts b/lib/models/shared/Global.ts new file mode 100644 index 0000000..b1aa0cc --- /dev/null +++ b/lib/models/shared/Global.ts @@ -0,0 +1,67 @@ +import { type Snowflake } from 'discord.js'; +import { type Sequelize } from 'sequelize'; +import { BaseModel } from '../BaseModel.js'; +const { DataTypes } = (await import('sequelize')).default; + +export interface GlobalModel { + environment: 'production' | 'development' | 'beta'; + disabledCommands: string[]; + blacklistedUsers: Snowflake[]; + blacklistedGuilds: Snowflake[]; + blacklistedChannels: Snowflake[]; +} + +export interface GlobalModelCreationAttributes { + environment: 'production' | 'development' | 'beta'; + disabledCommands?: string[]; + blacklistedUsers?: Snowflake[]; + blacklistedGuilds?: Snowflake[]; + blacklistedChannels?: Snowflake[]; +} + +/** + * Data specific to a certain instance of the bot. + */ +export class Global extends BaseModel implements GlobalModel { + /** + * The bot's environment. + */ + public declare environment: 'production' | 'development' | 'beta'; + + /** + * Globally disabled commands. + */ + public declare disabledCommands: string[]; + + /** + * Globally blacklisted users. + */ + public declare blacklistedUsers: Snowflake[]; + + /** + * Guilds blacklisted from using the bot. + */ + public declare blacklistedGuilds: Snowflake[]; + + /** + * Channels where the bot is prevented from running commands in. + */ + public declare blacklistedChannels: Snowflake[]; + + /** + * Initializes the model. + * @param sequelize The sequelize instance. + */ + public static initModel(sequelize: Sequelize): void { + Global.init( + { + environment: { type: DataTypes.STRING, primaryKey: true }, + disabledCommands: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + blacklistedUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + blacklistedGuilds: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + blacklistedChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] } + }, + { sequelize } + ); + } +} diff --git a/lib/models/shared/GuildCount.ts b/lib/models/shared/GuildCount.ts new file mode 100644 index 0000000..7afef56 --- /dev/null +++ b/lib/models/shared/GuildCount.ts @@ -0,0 +1,38 @@ +import { DataTypes, Model, type Sequelize } from 'sequelize'; +import { Environment } from '../../../config/Config.js'; + +export interface GuildCountModel { + timestamp: Date; + environment: Environment; + guildCount: number; +} + +export interface GuildCountCreationAttributes { + timestamp?: Date; + environment: Environment; + guildCount: number; +} + +/** + * The number of guilds that the bot is in for each environment. + */ +export class GuildCount extends Model implements GuildCountModel { + public declare timestamp: Date; + public declare environment: Environment; + public declare guildCount: number; + + /** + * Initializes the model. + * @param sequelize The sequelize instance. + */ + public static initModel(sequelize: Sequelize): void { + GuildCount.init( + { + timestamp: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW }, + environment: { type: DataTypes.STRING, allowNull: false }, + guildCount: { type: DataTypes.BIGINT, allowNull: false } + }, + { sequelize, timestamps: false } + ); + } +} diff --git a/lib/models/shared/MemberCount.ts b/lib/models/shared/MemberCount.ts new file mode 100644 index 0000000..200a58e --- /dev/null +++ b/lib/models/shared/MemberCount.ts @@ -0,0 +1,37 @@ +import { DataTypes, Model, type Sequelize } from 'sequelize'; + +export interface MemberCountModel { + timestamp: Date; + guildId: string; + memberCount: number; +} + +export interface MemberCountCreationAttributes { + timestamp?: Date; + guildId: string; + memberCount: number; +} + +/** + * The member count of each guild that the bot is in that have over 100 members. + */ +export class MemberCount extends Model implements MemberCountModel { + public declare timestamp: Date; + public declare guildId: string; + public declare memberCount: number; + + /** + * Initializes the model. + * @param sequelize The sequelize instance. + */ + public static initModel(sequelize: Sequelize): void { + MemberCount.init( + { + timestamp: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW }, + guildId: { type: DataTypes.STRING, allowNull: false }, + memberCount: { type: DataTypes.BIGINT, allowNull: false } + }, + { sequelize, timestamps: false } + ); + } +} diff --git a/lib/models/shared/Shared.ts b/lib/models/shared/Shared.ts new file mode 100644 index 0000000..dec77d1 --- /dev/null +++ b/lib/models/shared/Shared.ts @@ -0,0 +1,84 @@ +import { Snowflake } from 'discord.js'; +import type { Sequelize } from 'sequelize'; +import { BadWords } from '../../automod/AutomodShared.js'; +import { BaseModel } from '../BaseModel.js'; +const { DataTypes } = (await import('sequelize')).default; + +export interface SharedModel { + primaryKey: 0; + superUsers: Snowflake[]; + privilegedUsers: Snowflake[]; + badLinksSecret: string[]; + badLinks: string[]; + badWords: BadWords; + autoBanCode: string | null; +} + +export interface SharedModelCreationAttributes { + primaryKey?: 0; + superUsers?: Snowflake[]; + privilegedUsers?: Snowflake[]; + badLinksSecret?: string[]; + badLinks?: string[]; + badWords?: BadWords; + autoBanCode?: string; +} + +/** + * Data shared between all bot instances. + */ +export class Shared extends BaseModel implements SharedModel { + /** + * The primary key of the shared model. + */ + public declare primaryKey: 0; + + /** + * Trusted users. + */ + public declare superUsers: Snowflake[]; + + /** + * Users that have all permissions that devs have except eval. + */ + public declare privilegedUsers: Snowflake[]; + + /** + * Non-public bad links. + */ + public declare badLinksSecret: string[]; + + /** + * Public Bad links. + */ + public declare badLinks: string[]; + + /** + * Bad words. + */ + public declare badWords: BadWords; + + /** + * Code that is used to match for auto banning users in moulberry's bush + */ + public declare autoBanCode: string; + + /** + * Initializes the model. + * @param sequelize The sequelize instance. + */ + public static initModel(sequelize: Sequelize): void { + Shared.init( + { + primaryKey: { type: DataTypes.INTEGER, primaryKey: true, validate: { min: 0, max: 0 } }, + superUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + privilegedUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + badLinksSecret: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + badLinks: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, + badWords: { type: DataTypes.JSONB, allowNull: false, defaultValue: {} }, + autoBanCode: { type: DataTypes.TEXT } + }, + { sequelize, freezeTableName: true } + ); + } +} diff --git a/lib/models/shared/Stat.ts b/lib/models/shared/Stat.ts new file mode 100644 index 0000000..8e2e0b3 --- /dev/null +++ b/lib/models/shared/Stat.ts @@ -0,0 +1,72 @@ +import { type Sequelize } from 'sequelize'; +import { BaseModel } from '../BaseModel.js'; +const { DataTypes } = (await import('sequelize')).default; + +type Environment = 'production' | 'development' | 'beta'; + +export interface StatModel { + environment: Environment; + commandsUsed: bigint; + slashCommandsUsed: bigint; +} + +export interface StatModelCreationAttributes { + environment: Environment; + commandsUsed?: bigint; + slashCommandsUsed?: bigint; +} + +/** + * Statistics for each instance of the bot. + */ +export class Stat extends BaseModel implements StatModel { + /** + * The bot's environment. + */ + public declare environment: Environment; + + /** + * The number of commands used + */ + public declare commandsUsed: bigint; + + /** + * The number of slash commands used + */ + public declare slashCommandsUsed: bigint; + + /** + * Initializes the model. + * @param sequelize The sequelize instance. + */ + public static initModel(sequelize: Sequelize): void { + Stat.init( + { + environment: { type: DataTypes.STRING, primaryKey: true }, + commandsUsed: { + type: DataTypes.TEXT, + get: function (): bigint { + return BigInt(this.getDataValue('commandsUsed')); + }, + set: function (val: bigint) { + return this.setDataValue('commandsUsed', `${val}`); + }, + allowNull: false, + defaultValue: `${0n}` + }, + slashCommandsUsed: { + type: DataTypes.TEXT, + get: function (): bigint { + return BigInt(this.getDataValue('slashCommandsUsed')); + }, + set: function (val: bigint) { + return this.setDataValue('slashCommandsUsed', `${val}`); + }, + allowNull: false, + defaultValue: `${0n}` + } + }, + { sequelize } + ); + } +} diff --git a/lib/tsconfig.json b/lib/tsconfig.json new file mode 100644 index 0000000..e6d554e --- /dev/null +++ b/lib/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../dist/lib", + "composite": true + }, + "include": ["lib/**/*.ts"], + "references": [{ "path": "../config" }] +} diff --git a/lib/types/BushInspectOptions.ts b/lib/types/BushInspectOptions.ts new file mode 100644 index 0000000..30ed01a --- /dev/null +++ b/lib/types/BushInspectOptions.ts @@ -0,0 +1,123 @@ +import { type InspectOptions } from 'util'; + +/** + * {@link https://nodejs.org/api/util.html#utilinspectobject-showhidden-depth-colors util.inspect Options Documentation} + */ +export interface BushInspectOptions extends InspectOptions { + /** + * If `true`, object's non-enumerable symbols and properties are included in the + * formatted result. [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) + * and [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) entries + * are also included as well as user defined prototype properties (excluding method properties). + * + * @default false + */ + showHidden?: boolean | undefined; + + /** + * Specifies the number of times to recurse while formatting `object`. This is useful + * for inspecting large objects. To recurse up to the maximum call stack size pass + * `Infinity` or `null`. + * + * @default 2 + */ + depth?: number | null | undefined; + + /** + * If `true`, the output is styled with ANSI color codes. Colors are customizable. See + * [Customizing util.inspect colors](https://nodejs.org/api/util.html#util_customizing_util_inspect_colors). + * + * @default false + */ + colors?: boolean | undefined; + + /** + * If `false`, `[util.inspect.custom](depth, opts)` functions are not invoked. + * + * @default true + */ + customInspect?: boolean | undefined; + + /** + * If `true`, `Proxy` inspection includes the + * [`target` and `handler`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Terminology) + * objects. + * + * @default false + */ + showProxy?: boolean | undefined; + + /** + * Specifies the maximum number of `Array`, [`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray), + * [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) and + * [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) elements to + * include when formatting. Set to `null` or `Infinity` to show all elements. + * Set to `0` or negative to show no elements. + * + * @default 100 + */ + maxArrayLength?: number | null | undefined; + + /** + * Specifies the maximum number of characters to include when formatting. Set to + * `null` or `Infinity` to show all elements. Set to `0` or negative to show no + * characters. + * + * @default 10000 + */ + maxStringLength?: number | null | undefined; + + /** + * The length at which input values are split across multiple lines. Set to + * `Infinity` to format the input as a single line (in combination with compact set + * to `true` or any number >= `1`). + * + * @default 80 + */ + breakLength?: number | undefined; + + /** + * Setting this to `false` causes each object key to be displayed on a new line. It + * will break on new lines in text that is longer than `breakLength`. If set to a + * number, the most `n` inner elements are united on a single line as long as all + * properties fit into `breakLength`. Short array elements are also grouped together. + * + * @default 3 + */ + compact?: boolean | number | undefined; + + /** + * If set to `true` or a function, all properties of an object, and `Set` and `Map` + * entries are sorted in the resulting string. If set to `true` the + * [default sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) is used. + * If set to a function, it is used as a + * [compare function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters). + * + * @default false + */ + sorted?: boolean | ((a: string, b: string) => number) | undefined; + + /** + * If set to `true`, getters are inspected. If set to `'get'`, only getters without a + * corresponding setter are inspected. If set to `'set'`, only getters with a + * corresponding setter are inspected. This might cause side effects depending on + * the getter function. + * + * @default false + */ + getters?: 'get' | 'set' | boolean | undefined; + + /** + * If set to `true`, an underscore is used to separate every three digits in all bigints and numbers. + * + * @default false + */ + numericSeparator?: boolean; + + /** + * Whether or not to inspect strings. + * + * @default false + */ + inspectStrings?: boolean; +} diff --git a/lib/types/CodeBlockLang.ts b/lib/types/CodeBlockLang.ts new file mode 100644 index 0000000..d0eb4f3 --- /dev/null +++ b/lib/types/CodeBlockLang.ts @@ -0,0 +1,311 @@ +export type CodeBlockLang = + | '1c' + | 'abnf' + | 'accesslog' + | 'actionscript' + | 'ada' + | 'arduino' + | 'ino' + | 'armasm' + | 'arm' + | 'avrasm' + | 'actionscript' + | 'as' + | 'angelscript' + | 'asc' + | 'apache' + | 'apacheconf' + | 'applescript' + | 'osascript' + | 'arcade' + | 'asciidoc' + | 'adoc' + | 'aspectj' + | 'autohotkey' + | 'autoit' + | 'awk' + | 'mawk' + | 'nawk' + | 'gawk' + | 'bash' + | 'sh' + | 'zsh' + | 'basic' + | 'bnf' + | 'brainfuck' + | 'bf' + | 'csharp' + | 'cs' + | 'c' + | 'h' + | 'cpp' + | 'hpp' + | 'cc' + | 'hh' + | 'c++' + | 'h++' + | 'cxx' + | 'hxx' + | 'cal' + | 'cos' + | 'cls' + | 'cmake' + | 'cmake.in' + | 'coq' + | 'csp' + | 'css' + | 'capnproto' + | 'capnp' + | 'clojure' + | 'clj' + | 'coffeescript' + | 'coffee' + | 'cson' + | 'iced' + | 'crmsh' + | 'crm' + | 'pcmk' + | 'crystal' + | 'cr' + | 'd' + | 'dns' + | 'zone' + | 'bind' + | 'dos' + | 'bat' + | 'cmd' + | 'dart' + | 'dpr' + | 'dfm' + | 'pas' + | 'pascal' + | 'diff' + | 'patch' + | 'django' + | 'jinja' + | 'dockerfile' + | 'docker' + | 'dsconfig' + | 'dts' + | 'dust' + | 'dst' + | 'ebnf' + | 'elixir' + | 'elm' + | 'erlang' + | 'erl' + | 'excel' + | 'xls' + | 'xlsx' + | 'fsharp' + | 'fs' + | 'fix' + | 'fortran' + | 'f90' + | 'f95' + | 'gcode' + | 'nc' + | 'gams' + | 'gms' + | 'gauss' + | 'gss' + | 'gherkin' + | 'go' + | 'golang' + | 'golo' + | 'gololang' + | 'gradle' + | 'groovy' + | 'xml' + | 'html' + | 'xhtml' + | 'rss' + | 'atom' + | 'xjb' + | 'xsd' + | 'xsl' + | 'plist' + | 'svg' + | 'http' + | 'https' + | 'haml' + | 'handlebars' + | 'hbs' + | 'html.hbs' + | 'html.handlebars' + | 'haskell' + | 'hs' + | 'haxe' + | 'hx' + | 'hlsl' + | 'hy' + | 'hylang' + | 'ini' + | 'toml' + | 'inform7' + | 'i7' + | 'irpf90' + | 'json' + | 'java' + | 'jsp' + | 'javascript' + | 'js' + | 'jsx' + | 'julia' + | 'julia-repl' + | 'kotlin' + | 'kt' + | 'tex' + | 'leaf' + | 'lasso' + | 'ls' + | 'lassoscript' + | 'less' + | 'ldif' + | 'lisp' + | 'livecodeserver' + | 'livescript' + | 'ls' + | 'lua' + | 'makefile' + | 'mk' + | 'mak' + | 'make' + | 'markdown' + | 'md' + | 'mkdown' + | 'mkd' + | 'mathematica' + | 'mma' + | 'wl' + | 'matlab' + | 'maxima' + | 'mel' + | 'mercury' + | 'mizar' + | 'mojolicious' + | 'monkey' + | 'moonscript' + | 'moon' + | 'n1ql' + | 'nsis' + | 'nginx' + | 'nginxconf' + | 'nim' + | 'nimrod' + | 'nix' + | 'ocaml' + | 'ml' + | 'objectivec' + | 'mm' + | 'objc' + | 'obj-c' + | 'obj-c++' + | 'objective-c++' + | 'glsl' + | 'openscad' + | 'scad' + | 'ruleslanguage' + | 'oxygene' + | 'pf' + | 'pf.conf' + | 'php' + | 'parser3' + | 'perl' + | 'pl' + | 'pm' + | 'plaintext' + | 'txt' + | 'text' + | 'pony' + | 'pgsql' + | 'postgres' + | 'postgresql' + | 'powershell' + | 'ps' + | 'ps1' + | 'processing' + | 'prolog' + | 'properties' + | 'protobuf' + | 'puppet' + | 'pp' + | 'python' + | 'py' + | 'gyp' + | 'profile' + | 'python-repl' + | 'pycon' + | 'k' + | 'kdb' + | 'qml' + | 'r' + | 'reasonml' + | 're' + | 'rib' + | 'rsl' + | 'graph' + | 'instances' + | 'ruby' + | 'rb' + | 'gemspec' + | 'podspec' + | 'thor' + | 'irb' + | 'rust' + | 'rs' + | 'sas' + | 'scss' + | 'sql' + | 'p21' + | 'step' + | 'stp' + | 'scala' + | 'scheme' + | 'scilab' + | 'sci' + | 'shell' + | 'console' + | 'smali' + | 'smalltalk' + | 'st' + | 'sml' + | 'ml' + | 'stan' + | 'stanfuncs' + | 'stata' + | 'stylus' + | 'styl' + | 'subunit' + | 'swift' + | 'tcl' + | 'tk' + | 'tap' + | 'thrift' + | 'tp' + | 'twig' + | 'craftcms' + | 'typescript' + | 'ts' + | 'vbnet' + | 'vb' + | 'vbscript' + | 'vbs' + | 'vhdl' + | 'vala' + | 'verilog' + | 'v' + | 'vim' + | 'axapta' + | 'x++' + | 'x86asm' + | 'xl' + | 'tao' + | 'xquery' + | 'xpath' + | 'xq' + | 'yml' + | 'yaml' + | 'zephir' + | 'zep' + | 'ansi'; diff --git a/lib/utils/AllowedMentions.ts b/lib/utils/AllowedMentions.ts new file mode 100644 index 0000000..d2eb030 --- /dev/null +++ b/lib/utils/AllowedMentions.ts @@ -0,0 +1,68 @@ +import { type MessageMentionOptions, type MessageMentionTypes } from 'discord.js'; + +/** + * A utility class for creating allowed mentions. + */ +export class AllowedMentions { + /** + * @param everyone Whether everyone and here should be mentioned. + * @param roles Whether roles should be mentioned. + * @param users Whether users should be mentioned. + * @param repliedUser Whether the author of the Message being replied to should be mentioned. + */ + public constructor(public everyone = false, public roles = false, public users = true, public repliedUser = true) {} + + /** + * Don't mention anyone. + * @param repliedUser Whether the author of the Message being replied to should be mentioned. + */ + public static none(repliedUser = true): MessageMentionOptions { + return { parse: [], repliedUser }; + } + + /** + * Mention @everyone and @here, roles, and users. + * @param repliedUser Whether the author of the Message being replied to should be mentioned. + */ + public static all(repliedUser = true): MessageMentionOptions { + return { parse: ['everyone', 'roles', 'users'], repliedUser }; + } + + /** + * Mention users. + * @param repliedUser Whether the author of the Message being replied to should be mentioned. + */ + public static users(repliedUser = true): MessageMentionOptions { + return { parse: ['users'], repliedUser }; + } + + /** + * Mention everyone and here. + * @param repliedUser Whether the author of the Message being replied to should be mentioned. + */ + public static everyone(repliedUser = true): MessageMentionOptions { + return { parse: ['everyone'], repliedUser }; + } + + /** + * Mention roles. + * @param repliedUser Whether the author of the Message being replied to should be mentioned. + */ + public static roles(repliedUser = true): MessageMentionOptions { + return { parse: ['roles'], repliedUser }; + } + + /** + * Converts this into a MessageMentionOptions object. + */ + public toObject(): MessageMentionOptions { + return { + parse: [ + ...(this.users ? ['users'] : []), + ...(this.roles ? ['roles'] : []), + ...(this.everyone ? ['everyone'] : []) + ] as MessageMentionTypes[], + repliedUser: this.repliedUser + }; + } +} diff --git a/lib/utils/Arg.ts b/lib/utils/Arg.ts new file mode 100644 index 0000000..d362225 --- /dev/null +++ b/lib/utils/Arg.ts @@ -0,0 +1,192 @@ +import { + type BaseBushArgumentType, + type BushArgumentType, + type BushArgumentTypeCaster, + type CommandMessage, + type SlashMessage +} from '#lib'; +import { Argument, type Command, type Flag, type ParsedValuePredicate } from 'discord-akairo'; +import { type Message } from 'discord.js'; + +/** + * Casts a phrase to this argument's type. + * @param type - The type to cast to. + * @param message - Message that called the command. + * @param phrase - Phrase to process. + */ +export async function cast(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise>; +export async function cast(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise; +export async function cast(type: AT | ATC, message: CommandMessage | SlashMessage, phrase: string): Promise; +export async function cast( + this: ThisType, + type: ATC | AT, + message: CommandMessage | SlashMessage, + phrase: string +): Promise { + return Argument.cast.call(this, type as any, message.client.commandHandler.resolver, message as Message, phrase); +} + +/** + * Creates a type that is the left-to-right composition of the given types. + * If any of the types fails, the entire composition fails. + * @param types - Types to use. + */ +export function compose(...types: T[]): ATCATCR; +export function compose(...types: T[]): ATCBAT; +export function compose(...types: (AT | ATC)[]): ATC; +export function compose(...types: (AT | ATC)[]): ATC { + return Argument.compose(...(types as any)); +} + +/** + * Creates a type that is the left-to-right composition of the given types. + * If any of the types fails, the composition still continues with the failure passed on. + * @param types - Types to use. + */ +export function composeWithFailure(...types: T[]): ATCATCR; +export function composeWithFailure(...types: T[]): ATCBAT; +export function composeWithFailure(...types: (AT | ATC)[]): ATC; +export function composeWithFailure(...types: (AT | ATC)[]): ATC { + return Argument.composeWithFailure(...(types as any)); +} + +/** + * Checks if something is null, undefined, or a fail flag. + * @param value - Value to check. + */ +export function isFailure(value: any): value is null | undefined | (Flag & { value: any }) { + return Argument.isFailure(value); +} + +/** + * Creates a type from multiple types (product type). + * Only inputs where each type resolves with a non-void value are valid. + * @param types - Types to use. + */ +export function product(...types: T[]): ATCATCR; +export function product(...types: T[]): ATCBAT; +export function product(...types: (AT | ATC)[]): ATC; +export function product(...types: (AT | ATC)[]): ATC { + return Argument.product(...(types as any)); +} + +/** + * Creates a type where the parsed value must be within a range. + * @param type - The type to use. + * @param min - Minimum value. + * @param max - Maximum value. + * @param inclusive - Whether or not to be inclusive on the upper bound. + */ +export function range(type: T, min: number, max: number, inclusive?: boolean): ATCATCR; +export function range(type: T, min: number, max: number, inclusive?: boolean): ATCBAT; +export function range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC; +export function range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC { + return Argument.range(type as any, min, max, inclusive); +} + +/** + * Creates a type that parses as normal but also tags it with some data. + * Result is in an object `{ tag, value }` and wrapped in `Flag.fail` when failed. + * @param type - The type to use. + * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. + */ +export function tagged(type: T, tag?: any): ATCATCR; +export function tagged(type: T, tag?: any): ATCBAT; +export function tagged(type: AT | ATC, tag?: any): ATC; +export function tagged(type: AT | ATC, tag?: any): ATC { + return Argument.tagged(type as any, tag); +} + +/** + * Creates a type from multiple types (union type). + * The first type that resolves to a non-void value is used. + * Each type will also be tagged using `tagged` with themselves. + * @param types - Types to use. + */ +export function taggedUnion(...types: T[]): ATCATCR; +export function taggedUnion(...types: T[]): ATCBAT; +export function taggedUnion(...types: (AT | ATC)[]): ATC; +export function taggedUnion(...types: (AT | ATC)[]): ATC { + return Argument.taggedUnion(...(types as any)); +} + +/** + * Creates a type that parses as normal but also tags it with some data and carries the original input. + * Result is in an object `{ tag, input, value }` and wrapped in `Flag.fail` when failed. + * @param type - The type to use. + * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. + */ +export function taggedWithInput(type: T, tag?: any): ATCATCR; +export function taggedWithInput(type: T, tag?: any): ATCBAT; +export function taggedWithInput(type: AT | ATC, tag?: any): ATC; +export function taggedWithInput(type: AT | ATC, tag?: any): ATC { + return Argument.taggedWithInput(type as any, tag); +} + +/** + * Creates a type from multiple types (union type). + * The first type that resolves to a non-void value is used. + * @param types - Types to use. + */ +export function union(...types: T[]): ATCATCR; +export function union(...types: T[]): ATCBAT; +export function union(...types: (AT | ATC)[]): ATC; +export function union(...types: (AT | ATC)[]): ATC { + return Argument.union(...(types as any)); +} + +/** + * Creates a type with extra validation. + * If the predicate is not true, the value is considered invalid. + * @param type - The type to use. + * @param predicate - The predicate function. + */ +export function validate(type: T, predicate: ParsedValuePredicate): ATCATCR; +export function validate(type: T, predicate: ParsedValuePredicate): ATCBAT; +export function validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC; +export function validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC { + return Argument.validate(type as any, predicate); +} + +/** + * Creates a type that parses as normal but also carries the original input. + * Result is in an object `{ input, value }` and wrapped in `Flag.fail` when failed. + * @param type - The type to use. + */ +export function withInput(type: T): ATC>; +export function withInput(type: T): ATCBAT; +export function withInput(type: AT | ATC): ATC; +export function withInput(type: AT | ATC): ATC { + return Argument.withInput(type as any); +} + +type BushArgumentTypeCasterReturn = R extends BushArgumentTypeCaster ? S : R; +/** ```ts + * = BushArgumentTypeCaster + * ``` */ +type ATC = BushArgumentTypeCaster; +/** ```ts + * keyof BaseBushArgumentType + * ``` */ +type KBAT = keyof BaseBushArgumentType; +/** ```ts + * = BushArgumentTypeCasterReturn + * ``` */ +type ATCR = BushArgumentTypeCasterReturn; +/** ```ts + * BushArgumentType + * ``` */ +type AT = BushArgumentType; +/** ```ts + * BaseBushArgumentType + * ``` */ +type BAT = BaseBushArgumentType; + +/** ```ts + * = BushArgumentTypeCaster> + * ``` */ +type ATCATCR = BushArgumentTypeCaster>; +/** ```ts + * = BushArgumentTypeCaster + * ``` */ +type ATCBAT = BushArgumentTypeCaster; diff --git a/lib/utils/BushClientUtils.ts b/lib/utils/BushClientUtils.ts new file mode 100644 index 0000000..68a1dc3 --- /dev/null +++ b/lib/utils/BushClientUtils.ts @@ -0,0 +1,499 @@ +import assert from 'assert/strict'; +import { + cleanCodeBlockContent, + DMChannel, + escapeCodeBlock, + GuildMember, + Message, + PartialDMChannel, + Routes, + TextBasedChannel, + ThreadMember, + User, + type APIMessage, + type Client, + type Snowflake, + type UserResolvable +} from 'discord.js'; +import got from 'got'; +import _ from 'lodash'; +import { ConfigChannelKey } from '../../config/Config.js'; +import CommandErrorListener from '../../src/listeners/commands/commandError.js'; +import { GlobalCache, SharedCache } from '../common/BushCache.js'; +import { CommandMessage } from '../extensions/discord-akairo/BushCommand.js'; +import { SlashMessage } from '../extensions/discord-akairo/SlashMessage.js'; +import { Global } from '../models/shared/Global.js'; +import { Shared } from '../models/shared/Shared.js'; +import { BushInspectOptions } from '../types/BushInspectOptions.js'; +import { CodeBlockLang } from '../types/CodeBlockLang.js'; +import { emojis, Pronoun, PronounCode, pronounMapping, regex } from './BushConstants.js'; +import { addOrRemoveFromArray, formatError, inspect } from './BushUtils.js'; + +/** + * Utilities that require access to the client. + */ +export class BushClientUtils { + /** + * The hastebin urls used to post to hastebin, attempts to post in order + */ + #hasteURLs: string[] = [ + 'https://hst.sh', + // 'https://hasteb.in', + 'https://hastebin.com', + 'https://mystb.in', + 'https://haste.clicksminuteper.net', + 'https://paste.pythondiscord.com', + 'https://haste.unbelievaboat.com' + // 'https://haste.tyman.tech' + ]; + + public constructor(private readonly client: Client) {} + + /** + * Maps an array of user ids to user objects. + * @param ids The list of IDs to map + * @returns The list of users mapped + */ + public async mapIDs(ids: Snowflake[]): Promise { + return await Promise.all(ids.map((id) => this.client.users.fetch(id))); + } + + /** + * Posts text to hastebin + * @param content The text to post + * @returns The url of the posted text + */ + public async haste(content: string, substr = false): Promise { + let isSubstr = false; + if (content.length > 400_000 && !substr) { + void this.handleError('haste', new Error(`content over 400,000 characters (${content.length.toLocaleString()})`)); + return { error: 'content too long' }; + } else if (content.length > 400_000) { + content = content.substring(0, 400_000); + isSubstr = true; + } + for (const url of this.#hasteURLs) { + try { + const res: HastebinRes = await got.post(`${url}/documents`, { body: content }).json(); + return { url: `${url}/${res.key}`, error: isSubstr ? 'substr' : undefined }; + } catch { + void this.client.console.error('haste', `Unable to upload haste to ${url}`); + } + } + return { error: 'unable to post' }; + } + + /** + * Resolves a user-provided string into a user object, if possible + * @param text The text to try and resolve + * @returns The user resolved or null + */ + public async resolveUserAsync(text: string): Promise { + const idReg = /\d{17,19}/; + const idMatch = text.match(idReg); + if (idMatch) { + try { + return await this.client.users.fetch(text as Snowflake); + } catch {} + } + const mentionReg = /<@!?(?\d{17,19})>/; + const mentionMatch = text.match(mentionReg); + if (mentionMatch) { + try { + return await this.client.users.fetch(mentionMatch.groups!.id as Snowflake); + } catch {} + } + const user = this.client.users.cache.find((u) => u.username === text); + if (user) return user; + return null; + } + + /** + * Surrounds text in a code block with the specified language and puts it in a hastebin if its too long. + * * Embed Description Limit = 4096 characters + * * Embed Field Limit = 1024 characters + * @param code The content of the code block. + * @param length The maximum length of the code block. + * @param language The language of the code. + * @param substr Whether or not to substring the code if it is too long. + * @returns The generated code block + */ + public async codeblock(code: string, length: number, language: CodeBlockLang | '' = '', substr = false): Promise { + let hasteOut = ''; + code = escapeCodeBlock(code); + const prefix = `\`\`\`${language}\n`; + const suffix = '\n```'; + if (code.length + (prefix + suffix).length >= length) { + const haste_ = await this.haste(code, substr); + hasteOut = `Too large to display. ${ + haste_.url + ? `Hastebin: ${haste_.url}${language ? `.${language}` : ''}${haste_.error ? ` - ${haste_.error}` : ''}` + : `${emojis.error} Hastebin: ${haste_.error}` + }`; + } + + const FormattedHaste = hasteOut.length ? `\n${hasteOut}` : ''; + const shortenedCode = hasteOut ? code.substring(0, length - (prefix + FormattedHaste + suffix).length) : code; + const code3 = code.length ? prefix + shortenedCode + suffix + FormattedHaste : prefix + suffix; + if (code3.length > length) { + void this.client.console.warn(`codeblockError`, `Required Length: ${length}. Actual Length: ${code3.length}`, true); + void this.client.console.warn(`codeblockError`, code3, true); + throw new Error('code too long'); + } + return code3; + } + + /** + * Maps the key of a credential with a readable version when redacting. + * @param key The key of the credential. + * @returns The readable version of the key or the original key if there isn't a mapping. + */ + #mapCredential(key: string): string { + return ( + { + token: 'Main Token', + devToken: 'Dev Token', + betaToken: 'Beta Token', + hypixelApiKey: 'Hypixel Api Key', + wolframAlphaAppId: 'Wolfram|Alpha App ID', + dbPassword: 'Database Password' + }[key] ?? key + ); + } + + /** + * Redacts credentials from a string. + * @param text The text to redact credentials from. + * @returns The redacted text. + */ + public redact(text: string) { + for (const credentialName in { ...this.client.config.credentials, dbPassword: this.client.config.db.password }) { + const credential = { ...this.client.config.credentials, dbPassword: this.client.config.db.password }[ + credentialName as keyof typeof this.client.config.credentials + ]; + if (credential === null || credential === '') continue; + const replacement = this.#mapCredential(credentialName); + const escapeRegex = /[.*+?^${}()|[\]\\]/g; + text = text.replace(new RegExp(credential.toString().replace(escapeRegex, '\\$&'), 'g'), `[${replacement} Omitted]`); + text = text.replace( + new RegExp([...credential.toString()].reverse().join('').replace(escapeRegex, '\\$&'), 'g'), + `[${replacement} Omitted]` + ); + } + return text; + } + + /** + * Takes an any value, inspects it, redacts credentials, and puts it in a codeblock + * (and uploads to hast if the content is too long). + * @param input The object to be inspect, redacted, and put into a codeblock. + * @param language The language to make the codeblock. + * @param inspectOptions The options for {@link BushClientUtil.inspect}. + * @param length The maximum length that the codeblock can be. + * @returns The generated codeblock. + */ + public async inspectCleanRedactCodeblock( + input: any, + language?: CodeBlockLang | '', + inspectOptions?: BushInspectOptions, + length = 1024 + ) { + input = inspect(input, inspectOptions ?? undefined); + if (inspectOptions) inspectOptions.inspectStrings = undefined; + input = cleanCodeBlockContent(input); + input = this.redact(input); + return this.codeblock(input, length, language, true); + } + + /** + * Takes an any value, inspects it, redacts credentials, and uploads it to haste. + * @param input The object to be inspect, redacted, and upload. + * @param inspectOptions The options for {@link BushClientUtil.inspect}. + * @returns The {@link HasteResults}. + */ + public async inspectCleanRedactHaste(input: any, inspectOptions?: BushInspectOptions): Promise { + input = inspect(input, inspectOptions ?? undefined); + input = this.redact(input); + return this.haste(input, true); + } + + /** + * Takes an any value, inspects it and redacts credentials. + * @param input The object to be inspect and redacted. + * @param inspectOptions The options for {@link BushClientUtil.inspect}. + * @returns The redacted and inspected object. + */ + public inspectAndRedact(input: any, inspectOptions?: BushInspectOptions): string { + input = inspect(input, inspectOptions ?? undefined); + return this.redact(input); + } + + /** + * Get the global cache. + */ + public getGlobal(): GlobalCache; + /** + * Get a key from the global cache. + * @param key The key to get in the global cache. + */ + public getGlobal(key: K): GlobalCache[K]; + public getGlobal(key?: keyof GlobalCache) { + return key ? this.client.cache.global[key] : this.client.cache.global; + } + + /** + * Get the shared cache. + */ + public getShared(): SharedCache; + /** + * Get a key from the shared cache. + * @param key The key to get in the shared cache. + */ + public getShared(key: K): SharedCache[K]; + public getShared(key?: keyof SharedCache) { + return key ? this.client.cache.shared[key] : this.client.cache.shared; + } + + /** + * Add or remove an element from an array stored in the Globals database. + * @param action Either `add` or `remove` an element. + * @param key The key of the element in the global cache to update. + * @param value The value to add/remove from the array. + */ + public async insertOrRemoveFromGlobal( + action: 'add' | 'remove', + key: K, + value: Client['cache']['global'][K][0] + ): Promise { + const row = + (await Global.findByPk(this.client.config.environment)) ?? + (await Global.create({ environment: this.client.config.environment })); + const oldValue: any[] = row[key]; + const newValue = addOrRemoveFromArray(action, oldValue, value); + row[key] = newValue; + this.client.cache.global[key] = newValue; + return await row.save().catch((e) => this.handleError('insertOrRemoveFromGlobal', e)); + } + + /** + * Add or remove an element from an array stored in the Shared database. + * @param action Either `add` or `remove` an element. + * @param key The key of the element in the shared cache to update. + * @param value The value to add/remove from the array. + */ + public async insertOrRemoveFromShared>( + action: 'add' | 'remove', + key: K, + value: Client['cache']['shared'][K][0] + ): Promise { + const row = (await Shared.findByPk(0)) ?? (await Shared.create()); + const oldValue: any[] = row[key]; + const newValue = addOrRemoveFromArray(action, oldValue, value); + row[key] = newValue; + this.client.cache.shared[key] = newValue; + return await row.save().catch((e) => this.handleError('insertOrRemoveFromShared', e)); + } + + /** + * Updates an element in the Globals database. + * @param key The key in the global cache to update. + * @param value The value to set the key to. + */ + public async setGlobal( + key: K, + value: Client['cache']['global'][K] + ): Promise { + const row = + (await Global.findByPk(this.client.config.environment)) ?? + (await Global.create({ environment: this.client.config.environment })); + row[key] = value; + this.client.cache.global[key] = value; + return await row.save().catch((e) => this.handleError('setGlobal', e)); + } + + /** + * Updates an element in the Shared database. + * @param key The key in the shared cache to update. + * @param value The value to set the key to. + */ + public async setShared>( + key: K, + value: Client['cache']['shared'][K] + ): Promise { + const row = (await Shared.findByPk(0)) ?? (await Shared.create()); + row[key] = value; + this.client.cache.shared[key] = value; + return await row.save().catch((e) => this.handleError('setShared', e)); + } + + /** + * Send a message in the error logging channel and console for an error. + * @param context + * @param error + */ + public async handleError(context: string, error: Error) { + await this.client.console.error(_.camelCase(context), `An error occurred:\n${formatError(error, false)}`, false); + await this.client.console.channelError({ + embeds: await CommandErrorListener.generateErrorEmbed(this.client, { type: 'unhandledRejection', error: error, context }) + }); + } + + /** + * Fetches a user from discord. + * @param user The user to fetch + * @returns Undefined if the user is not found, otherwise the user. + */ + public async resolveNonCachedUser(user: UserResolvable | undefined | null): Promise { + if (user == null) return undefined; + const resolvedUser = + user instanceof User + ? user + : user instanceof GuildMember + ? user.user + : user instanceof ThreadMember + ? user.user + : user instanceof Message + ? user.author + : undefined; + + return resolvedUser ?? (await this.client.users.fetch(user as Snowflake).catch(() => undefined)); + } + + /** + * Get the pronouns of a discord user from pronoundb.org + * @param user The user to retrieve the promises of. + * @returns The human readable pronouns of the user, or undefined if they do not have any. + */ + public async getPronounsOf(user: User | Snowflake): Promise { + const _user = await this.resolveNonCachedUser(user); + if (!_user) throw new Error(`Cannot find user ${user}`); + const apiRes = (await got + .get(`https://pronoundb.org/api/v1/lookup?platform=discord&id=${_user.id}`) + .json() + .catch(() => undefined)) as { pronouns: PronounCode } | undefined; + + if (!apiRes) return undefined; + assert(apiRes.pronouns); + + return pronounMapping[apiRes.pronouns!]!; + } + + /** + * Uploads an image to imgur. + * @param image The image to upload. + * @returns The url of the imgur. + */ + public async uploadImageToImgur(image: string) { + const clientId = this.client.config.credentials.imgurClientId; + + const resp = (await got + .post('https://api.imgur.com/3/upload', { + headers: { + Authorization: `Client-ID ${clientId}`, + Accept: 'application/json' + }, + form: { + image: image, + type: 'base64' + }, + followRedirect: true + }) + .json() + .catch(() => null)) as { data: { link: string } | undefined }; + + return resp.data?.link ?? null; + } + + /** + * Gets the prefix based off of the message. + * @param message The message to get the prefix from. + * @returns The prefix. + */ + public prefix(message: CommandMessage | SlashMessage): string { + return message.util.isSlash + ? '/' + : this.client.config.isDevelopment + ? 'dev ' + : message.util.parsed?.prefix ?? this.client.config.prefix; + } + + public async resolveMessageLinks(content: string | null): Promise { + const res: MessageLinkParts[] = []; + + if (!content) return res; + + const regex_ = new RegExp(regex.messageLink); + let match: RegExpExecArray | null; + while (((match = regex_.exec(content)), match !== null)) { + const input = match.input; + if (!match.groups || !input) continue; + if (input.startsWith('<') && input.endsWith('>')) continue; + + const { guild_id, channel_id, message_id } = match.groups; + if (!guild_id || !channel_id || !message_id) continue; + + res.push({ guild_id, channel_id, message_id }); + } + + return res; + } + + public async resolveMessagesFromLinks(content: string): Promise { + const res: APIMessage[] = []; + + const links = await this.resolveMessageLinks(content); + if (!links.length) return []; + + for (const { guild_id, channel_id, message_id } of links) { + const guild = this.client.guilds.cache.get(guild_id); + if (!guild) continue; + const channel = guild.channels.cache.get(channel_id); + if (!channel || (!channel.isTextBased() && !channel.isThread())) continue; + + const message = (await this.client.rest + .get(Routes.channelMessage(channel_id, message_id)) + .catch(() => null)) as APIMessage | null; + if (!message) continue; + + res.push(message); + } + + return res; + } + + /** + * Resolves a channel from the config and ensures it is a non-dm-based-text-channel. + * @param channel The channel to retrieve. + */ + public async getConfigChannel( + channel: ConfigChannelKey + ): Promise | null> { + const channels = this.client.config.channels; + if (!(channel in channels)) + throw new TypeError(`Invalid channel provided (${channel}), must be one of ${Object.keys(channels).join(' ')}`); + + const channelId = channels[channel]; + if (channelId === '') return null; + + const res = await this.client.channels.fetch(channelId); + + if (!res?.isTextBased() || res.isDMBased()) return null; + + return res; + } +} + +interface HastebinRes { + key: string; +} + +export interface HasteResults { + url?: string; + error?: 'content too long' | 'substr' | 'unable to post'; +} + +export interface MessageLinkParts { + guild_id: Snowflake; + channel_id: Snowflake; + message_id: Snowflake; +} diff --git a/lib/utils/BushConstants.ts b/lib/utils/BushConstants.ts new file mode 100644 index 0000000..090616c --- /dev/null +++ b/lib/utils/BushConstants.ts @@ -0,0 +1,531 @@ +import deepLock from 'deep-lock'; +import { + ArgumentMatches as AkairoArgumentMatches, + ArgumentTypes as AkairoArgumentTypes, + BuiltInReasons, + CommandHandlerEvents as AkairoCommandHandlerEvents +} from 'discord-akairo/dist/src/util/Constants.js'; +import { Colors, GuildFeature } from 'discord.js'; + +const rawCapeUrl = 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/'; + +/** + * Time units in milliseconds + */ +export const enum Time { + /** + * One millisecond (1 ms). + */ + Millisecond = 1, + + /** + * One second (1,000 ms). + */ + Second = Millisecond * 1000, + + /** + * One minute (60,000 ms). + */ + Minute = Second * 60, + + /** + * One hour (3,600,000 ms). + */ + Hour = Minute * 60, + + /** + * One day (86,400,000 ms). + */ + Day = Hour * 24, + + /** + * One week (604,800,000 ms). + */ + Week = Day * 7, + + /** + * One month (2,629,800,000 ms). + */ + Month = Day * 30.4375, // average of days in a month (including leap years) + + /** + * One year (31,557,600,000 ms). + */ + Year = Day * 365.25 // average with leap years +} + +export const emojis = Object.freeze({ + success: '<:success:837109864101707807>', + warn: '<:warn:848726900876247050>', + error: '<:error:837123021016924261>', + successFull: '<:success_full:850118767576088646>', + warnFull: '<:warn_full:850118767391539312>', + errorFull: '<:error_full:850118767295201350>', + mad: '<:mad:783046135392239626>', + join: '<:join:850198029809614858>', + leave: '<:leave:850198048205307919>', + loading: '', + offlineCircle: '<:offline:787550565382750239>', + dndCircle: '<:dnd:787550487633330176>', + idleCircle: '<:idle:787550520956551218>', + onlineCircle: '<:online:787550449435803658>', + cross: '<:cross:878319362539421777>', + check: '<:check:878320135297961995>' +} as const); + +export const emojisRaw = Object.freeze({ + success: '837109864101707807', + warn: '848726900876247050', + error: '837123021016924261', + successFull: '850118767576088646', + warnFull: '850118767391539312', + errorFull: '850118767295201350', + mad: '783046135392239626', + join: '850198029809614858', + leave: '850198048205307919', + loading: '853419254619963392', + offlineCircle: '787550565382750239', + dndCircle: '787550487633330176', + idleCircle: '787550520956551218', + onlineCircle: '787550449435803658', + cross: '878319362539421777', + check: '878320135297961995' +} as const); + +export const colors = Object.freeze({ + default: 0x1fd8f1, + error: 0xef4947, + warn: 0xfeba12, + success: 0x3bb681, + info: 0x3b78ff, + red: 0xff0000, + blue: 0x0055ff, + aqua: 0x00bbff, + purple: 0x8400ff, + blurple: 0x5440cd, + newBlurple: 0x5865f2, + pink: 0xff00e6, + green: 0x00ff1e, + darkGreen: 0x008f11, + gold: 0xb59400, + yellow: 0xffff00, + white: 0xffffff, + gray: 0xa6a6a6, + lightGray: 0xcfcfcf, + darkGray: 0x7a7a7a, + black: 0x000000, + orange: 0xe86100, + ...Colors +} as const); + +// Somewhat stolen from @Mzato0001 +export const timeUnits = deepLock({ + milliseconds: { + match: / (?:(?-?(?:\d+)?\.?\d+) *(?:milliseconds?|msecs?|ms))/im, + value: Time.Millisecond + }, + seconds: { + match: / (?:(?-?(?:\d+)?\.?\d+) *(?:seconds?|secs?|s))/im, + value: Time.Second + }, + minutes: { + match: / (?:(?-?(?:\d+)?\.?\d+) *(?:minutes?|mins?|m))/im, + value: Time.Minute + }, + hours: { + match: / (?:(?-?(?:\d+)?\.?\d+) *(?:hours?|hrs?|h))/im, + value: Time.Hour + }, + days: { + match: / (?:(?-?(?:\d+)?\.?\d+) *(?:days?|d))/im, + value: Time.Day + }, + weeks: { + match: / (?:(?-?(?:\d+)?\.?\d+) *(?:weeks?|w))/im, + value: Time.Week + }, + months: { + match: / (?:(?-?(?:\d+)?\.?\d+) *(?:months?|mon|mo))/im, + value: Time.Month + }, + years: { + match: / (?:(?-?(?:\d+)?\.?\d+) *(?:years?|y))/im, + value: Time.Year + } +} as const); + +export const regex = deepLock({ + snowflake: /^\d{15,21}$/im, + + discordEmoji: /[a-zA-Z0-9_]+):(?\d{15,21})>/im, + + /* + * Taken with permission from Geek: + * https://github.com/FireDiscordBot/bot/blob/5d1990e5f8b52fcc72261d786aa3c7c7c65ab5e8/lib/util/constants.ts#L276 + */ + /** **This has the global flag, make sure to handle it correctly.** */ + messageLink: + /\d{15,21})\/(?\d{15,21})\/(?\d{15,21})>?/gim +} as const); + +/** + * Maps the response from pronoundb.org to a readable format + */ +export const pronounMapping = Object.freeze({ + unspecified: 'Unspecified', + hh: 'He/Him', + hi: 'He/It', + hs: 'He/She', + ht: 'He/They', + ih: 'It/Him', + ii: 'It/Its', + is: 'It/She', + it: 'It/They', + shh: 'She/He', + sh: 'She/Her', + si: 'She/It', + st: 'She/They', + th: 'They/He', + ti: 'They/It', + ts: 'They/She', + tt: 'They/Them', + any: 'Any pronouns', + other: 'Other pronouns', + ask: 'Ask me my pronouns', + avoid: 'Avoid pronouns, use my name' +} as const); + +/** + * A bunch of mappings + */ +export const mappings = deepLock({ + guilds: { + "Moulberry's Bush": '516977525906341928', + "Moulberry's Tree": '767448775450820639', + 'MB Staff': '784597260465995796', + "IRONM00N's Space Ship": '717176538717749358' + }, + + channels: { + 'neu-support': '714332750156660756', + 'giveaways': '767782084981817344' + }, + + users: { + IRONM00N: '322862723090219008', + Moulberry: '211288288055525376', + nopo: '384620942577369088', + Bestower: '496409778822709251' + }, + + permissions: { + CreateInstantInvite: { name: 'Create Invite', important: false }, + KickMembers: { name: 'Kick Members', important: true }, + BanMembers: { name: 'Ban Members', important: true }, + Administrator: { name: 'Administrator', important: true }, + ManageChannels: { name: 'Manage Channels', important: true }, + ManageGuild: { name: 'Manage Server', important: true }, + AddReactions: { name: 'Add Reactions', important: false }, + ViewAuditLog: { name: 'View Audit Log', important: true }, + PrioritySpeaker: { name: 'Priority Speaker', important: true }, + Stream: { name: 'Video', important: false }, + ViewChannel: { name: 'View Channel', important: false }, + SendMessages: { name: 'Send Messages', important: false }, + SendTTSMessages: { name: 'Send Text-to-Speech Messages', important: true }, + ManageMessages: { name: 'Manage Messages', important: true }, + EmbedLinks: { name: 'Embed Links', important: false }, + AttachFiles: { name: 'Attach Files', important: false }, + ReadMessageHistory: { name: 'Read Message History', important: false }, + MentionEveryone: { name: 'Mention @\u200Beveryone, @\u200Bhere, and All Roles', important: true }, // name has a zero-width space to prevent accidents + UseExternalEmojis: { name: 'Use External Emoji', important: false }, + ViewGuildInsights: { name: 'View Server Insights', important: true }, + Connect: { name: 'Connect', important: false }, + Speak: { name: 'Speak', important: false }, + MuteMembers: { name: 'Mute Members', important: true }, + DeafenMembers: { name: 'Deafen Members', important: true }, + MoveMembers: { name: 'Move Members', important: true }, + UseVAD: { name: 'Use Voice Activity', important: false }, + ChangeNickname: { name: 'Change Nickname', important: false }, + ManageNicknames: { name: 'Change Nicknames', important: true }, + ManageRoles: { name: 'Manage Roles', important: true }, + ManageWebhooks: { name: 'Manage Webhooks', important: true }, + ManageEmojisAndStickers: { name: 'Manage Emojis and Stickers', important: true }, + UseApplicationCommands: { name: 'Use Slash Commands', important: false }, + RequestToSpeak: { name: 'Request to Speak', important: false }, + ManageEvents: { name: 'Manage Events', important: true }, + ManageThreads: { name: 'Manage Threads', important: true }, + CreatePublicThreads: { name: 'Create Public Threads', important: false }, + CreatePrivateThreads: { name: 'Create Private Threads', important: false }, + UseExternalStickers: { name: 'Use External Stickers', important: false }, + SendMessagesInThreads: { name: 'Send Messages In Threads', important: false }, + StartEmbeddedActivities: { name: 'Start Activities', important: false }, + ModerateMembers: { name: 'Timeout Members', important: true }, + UseEmbeddedActivities: { name: 'Use Activities', important: false } + }, + + // prettier-ignore + features: { + [GuildFeature.Verified]: { name: 'Verified', important: true, emoji: '<:verified:850795049817473066>', weight: 0 }, + [GuildFeature.Partnered]: { name: 'Partnered', important: true, emoji: '<:partneredServer:850794851955507240>', weight: 1 }, + [GuildFeature.MoreStickers]: { name: 'More Stickers', important: true, emoji: null, weight: 2 }, + MORE_EMOJIS: { name: 'More Emoji', important: true, emoji: '<:moreEmoji:850786853497602080>', weight: 3 }, + [GuildFeature.Featurable]: { name: 'Featurable', important: true, emoji: '<:featurable:850786776372084756>', weight: 4 }, + [GuildFeature.RelayEnabled]: { name: 'Relay Enabled', important: true, emoji: '<:relayEnabled:850790531441229834>', weight: 5 }, + [GuildFeature.Discoverable]: { name: 'Discoverable', important: true, emoji: '<:discoverable:850786735360966656>', weight: 6 }, + ENABLED_DISCOVERABLE_BEFORE: { name: 'Enabled Discovery Before', important: false, emoji: '<:enabledDiscoverableBefore:850786754670624828>', weight: 7 }, + [GuildFeature.MonetizationEnabled]: { name: 'Monetization Enabled', important: true, emoji: null, weight: 8 }, + [GuildFeature.TicketedEventsEnabled]: { name: 'Ticketed Events Enabled', important: true, emoji: null, weight: 9 }, + [GuildFeature.PreviewEnabled]: { name: 'Preview Enabled', important: true, emoji: '<:previewEnabled:850790508266913823>', weight: 10 }, + COMMERCE: { name: 'Store Channels', important: true, emoji: '<:storeChannels:850786692432396338>', weight: 11 }, + [GuildFeature.VanityURL]: { name: 'Vanity URL', important: false, emoji: '<:vanityURL:850790553079644160>', weight: 12 }, + [GuildFeature.VIPRegions]: { name: 'VIP Regions', important: false, emoji: '<:VIPRegions:850794697496854538>', weight: 13 }, + [GuildFeature.AnimatedIcon]: { name: 'Animated Icon', important: false, emoji: '<:animatedIcon:850774498071412746>', weight: 14 }, + [GuildFeature.Banner]: { name: 'Banner', important: false, emoji: '<:banner:850786673150787614>', weight: 15 }, + [GuildFeature.InviteSplash]: { name: 'Invite Splash', important: false, emoji: '<:inviteSplash:850786798246559754>', weight: 16 }, + [GuildFeature.PrivateThreads]: { name: 'Private Threads', important: false, emoji: '<:privateThreads:869763711894700093>', weight: 17 }, + THREE_DAY_THREAD_ARCHIVE: { name: 'Three Day Thread Archive', important: false, emoji: '<:threeDayThreadArchive:869767841652564008>', weight: 19 }, + SEVEN_DAY_THREAD_ARCHIVE: { name: 'Seven Day Thread Archive', important: false, emoji: '<:sevenDayThreadArchive:869767896123998288>', weight: 20 }, + [GuildFeature.RoleIcons]: { name: 'Role Icons', important: false, emoji: '<:roleIcons:876993381929222175>', weight: 21 }, + [GuildFeature.News]: { name: 'Announcement Channels', important: false, emoji: '<:announcementChannels:850790491796013067>', weight: 22 }, + [GuildFeature.MemberVerificationGateEnabled]: { name: 'Membership Verification Gate', important: false, emoji: '<:memberVerificationGateEnabled:850786829984858212>', weight: 23 }, + [GuildFeature.WelcomeScreenEnabled]: { name: 'Welcome Screen Enabled', important: false, emoji: '<:welcomeScreenEnabled:850790575875817504>', weight: 24 }, + [GuildFeature.Community]: { name: 'Community', important: false, emoji: '<:community:850786714271875094>', weight: 25 }, + THREADS_ENABLED: {name: 'Threads Enabled', important: false, emoji: '<:threadsEnabled:869756035345317919>', weight: 26 }, + THREADS_ENABLED_TESTING: {name: 'Threads Enabled Testing', important: false, emoji: null, weight: 27 }, + [GuildFeature.AnimatedBanner]: { name: 'Animated Banner', important: false, emoji: null, weight: 28 }, + [GuildFeature.HasDirectoryEntry]: { name: 'Has Directory Entry', important: true, emoji: null, weight: 29 }, + [GuildFeature.Hub]: { name: 'Hub', important: true, emoji: null, weight: 30 }, + [GuildFeature.LinkedToHub]: { name: 'Linked To Hub', important: true, emoji: null, weight: 31 }, + }, + + regions: { + 'automatic': ':united_nations: Automatic', + 'brazil': ':flag_br: Brazil', + 'europe': ':flag_eu: Europe', + 'hongkong': ':flag_hk: Hongkong', + 'india': ':flag_in: India', + 'japan': ':flag_jp: Japan', + 'russia': ':flag_ru: Russia', + 'singapore': ':flag_sg: Singapore', + 'southafrica': ':flag_za: South Africa', + 'sydney': ':flag_au: Sydney', + 'us-central': ':flag_us: US Central', + 'us-east': ':flag_us: US East', + 'us-south': ':flag_us: US South', + 'us-west': ':flag_us: US West' + }, + + otherEmojis: { + ServerBooster1: '<:serverBooster1:848740052091142145>', + ServerBooster2: '<:serverBooster2:848740090506510388>', + ServerBooster3: '<:serverBooster3:848740124992077835>', + ServerBooster6: '<:serverBooster6:848740155245461514>', + ServerBooster9: '<:serverBooster9:848740188846030889>', + ServerBooster12: '<:serverBooster12:848740304365551668>', + ServerBooster15: '<:serverBooster15:848740354890137680>', + ServerBooster18: '<:serverBooster18:848740402886606868>', + ServerBooster24: '<:serverBooster24:848740444628320256>', + Nitro: '<:nitro:848740498054971432>', + Booster: '<:booster:848747775020892200>', + Owner: '<:owner:848746439311753286>', + Admin: '<:admin:848963914628333598>', + Superuser: '<:superUser:848947986326224926>', + Developer: '<:developer:848954538111139871>', + Bot: '<:bot:1006929813203853427>', + BushVerified: '<:verfied:853360152090771497>', + BoostTier1: '<:boostitle:853363736679940127>', + BoostTier2: '<:boostitle:853363752728789075>', + BoostTier3: '<:boostitle:853363769132056627>', + ChannelText: '<:text:853375537791893524>', + ChannelNews: '<:announcements:853375553531674644>', + ChannelVoice: '<:voice:853375566735212584>', + ChannelStage: '<:stage:853375583521210468>', + // ChannelStore: '<:store:853375601175691266>', + ChannelCategory: '<:category:853375615260819476>', + ChannelThread: '<:thread:865033845753249813>' + }, + + userFlags: { + Staff: '<:discordEmployee:848742947826434079>', + Partner: '<:partneredServerOwner:848743051593777152>', + Hypesquad: '<:hypeSquadEvents:848743108283072553>', + BugHunterLevel1: '<:bugHunter:848743239850393640>', + HypeSquadOnlineHouse1: '<:hypeSquadBravery:848742910563844127>', + HypeSquadOnlineHouse2: '<:hypeSquadBrilliance:848742840649646101>', + HypeSquadOnlineHouse3: '<:hypeSquadBalance:848742877537370133>', + PremiumEarlySupporter: '<:earlySupporter:848741030102171648>', + TeamPseudoUser: 'TeamPseudoUser', + BugHunterLevel2: '<:bugHunterGold:848743283080822794>', + VerifiedBot: '<:verifiedbot_rebrand1:938928232667947028><:verifiedbot_rebrand2:938928355707879475>', + VerifiedDeveloper: '<:earlyVerifiedBotDeveloper:848741079875846174>', + CertifiedModerator: '<:discordCertifiedModerator:877224285901582366>', + BotHTTPInteractions: 'BotHTTPInteractions', + Spammer: 'Spammer', + Quarantined: 'Quarantined' + }, + + status: { + online: '<:online:848937141639577690>', + idle: '<:idle:848937158261211146>', + dnd: '<:dnd:848937173780135986>', + offline: '<:offline:848939387277672448>', + streaming: '<:streaming:848937187479519242>' + }, + + maybeNitroDiscrims: ['1111', '2222', '3333', '4444', '5555', '6666', '6969', '7777', '8888', '9999'], + + capes: [ + /* supporter capes */ + { name: 'patreon1', purchasable: false /* moulberry no longer offers */ }, + { name: 'patreon2', purchasable: false /* moulberry no longer offers */ }, + { name: 'fade', custom: `${rawCapeUrl}fade.gif`, purchasable: true }, + { name: 'lava', custom: `${rawCapeUrl}lava.gif`, purchasable: true }, + { name: 'mcworld', custom: `${rawCapeUrl}mcworld_compressed.gif`, purchasable: true }, + { name: 'negative', custom: `${rawCapeUrl}negative_compressed.gif`, purchasable: true }, + { name: 'space', custom: `${rawCapeUrl}space_compressed.gif`, purchasable: true }, + { name: 'void', custom: `${rawCapeUrl}void.gif`, purchasable: true }, + { name: 'tunnel', custom: `${rawCapeUrl}tunnel.gif`, purchasable: true }, + /* Staff capes */ + { name: 'contrib' }, + { name: 'mbstaff' }, + { name: 'ironmoon' }, + { name: 'gravy' }, + { name: 'nullzee' }, + /* partner capes */ + { name: 'thebakery' }, + { name: 'dsm' }, + { name: 'packshq' }, + { name: 'furf' }, + { name: 'skytils' }, + { name: 'sbp' }, + { name: 'subreddit_light' }, + { name: 'subreddit_dark' }, + { name: 'skyclient' }, + { name: 'sharex' }, + { name: 'sharex_white' }, + /* streamer capes */ + { name: 'alexxoffi' }, + { name: 'jakethybro' }, + { name: 'krusty' }, + { name: 'krusty_day' }, + { name: 'krusty_night' }, + { name: 'krusty_sunset' }, + { name: 'soldier' }, + { name: 'zera' }, + { name: 'secondpfirsisch' }, + { name: 'stormy_lh' } + ].map((value, index) => ({ ...value, index })), + + roleMap: [ + { name: '*', id: '792453550768390194' }, + { name: 'Admin Perms', id: '746541309853958186' }, + { name: 'Sr. Moderator', id: '782803470205190164' }, + { name: 'Moderator', id: '737308259823910992' }, + { name: 'Helper', id: '737440116230062091' }, + { name: 'Trial Helper', id: '783537091946479636' }, + { name: 'Contributor', id: '694431057532944425' }, + { name: 'Giveaway Donor', id: '784212110263451649' }, + { name: 'Giveaway (200m)', id: '810267756426690601' }, + { name: 'Giveaway (100m)', id: '801444430522613802' }, + { name: 'Giveaway (50m)', id: '787497512981757982' }, + { name: 'Giveaway (25m)', id: '787497515771232267' }, + { name: 'Giveaway (10m)', id: '787497518241153025' }, + { name: 'Giveaway (5m)', id: '787497519768403989' }, + { name: 'Giveaway (1m)', id: '787497521084891166' }, + { name: 'Suggester', id: '811922322767609877' }, + { name: 'Partner', id: '767324547312779274' }, + { name: 'Level Locked', id: '784248899044769792' }, + { name: 'No Files', id: '786421005039173633' }, + { name: 'No Reactions', id: '786421270924361789' }, + { name: 'No Links', id: '786421269356740658' }, + { name: 'No Bots', id: '786804858765312030' }, + { name: 'No VC', id: '788850482554208267' }, + { name: 'No Giveaways', id: '808265422334984203' }, + { name: 'No Support', id: '790247359824396319' } + ], + + roleWhitelist: { + 'Partner': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Suggester': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper', 'Trial Helper', 'Contributor'], + 'Level Locked': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'No Files': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'No Reactions': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'No Links': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'No Bots': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'No VC': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'No Giveaways': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper'], + 'No Support': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway Donor': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (200m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (100m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (50m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (25m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (10m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (5m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (1m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'] + } +} as const); + +export const ArgumentMatches = Object.freeze({ + ...AkairoArgumentMatches +} as const); + +export const ArgumentTypes = Object.freeze({ + ...AkairoArgumentTypes, + DURATION: 'duration', + CONTENT_WITH_DURATION: 'contentWithDuration', + PERMISSION: 'permission', + SNOWFLAKE: 'snowflake', + DISCORD_EMOJI: 'discordEmoji', + ROLE_WITH_DURATION: 'roleWithDuration', + ABBREVIATED_NUMBER: 'abbreviatedNumber', + GLOBAL_USER: 'globalUser' +} as const); + +export const BlockedReasons = Object.freeze({ + ...BuiltInReasons, + DISABLED_GUILD: 'disabledGuild', + DISABLED_GLOBAL: 'disabledGlobal', + ROLE_BLACKLIST: 'roleBlacklist', + USER_GUILD_BLACKLIST: 'userGuildBlacklist', + USER_GLOBAL_BLACKLIST: 'userGlobalBlacklist', + RESTRICTED_GUILD: 'restrictedGuild', + CHANNEL_GUILD_BLACKLIST: 'channelGuildBlacklist', + CHANNEL_GLOBAL_BLACKLIST: 'channelGlobalBlacklist', + RESTRICTED_CHANNEL: 'restrictedChannel' +} as const); + +export const CommandHandlerEvents = Object.freeze({ + ...AkairoCommandHandlerEvents +} as const); + +export const moulberryBushRoleMap = deepLock([ + { name: '*', id: '792453550768390194' }, + { name: 'Admin Perms', id: '746541309853958186' }, + { name: 'Sr. Moderator', id: '782803470205190164' }, + { name: 'Moderator', id: '737308259823910992' }, + { name: 'Helper', id: '737440116230062091' }, + { name: 'Trial Helper', id: '783537091946479636' }, + { name: 'Contributor', id: '694431057532944425' }, + { name: 'Giveaway Donor', id: '784212110263451649' }, + { name: 'Giveaway (200m)', id: '810267756426690601' }, + { name: 'Giveaway (100m)', id: '801444430522613802' }, + { name: 'Giveaway (50m)', id: '787497512981757982' }, + { name: 'Giveaway (25m)', id: '787497515771232267' }, + { name: 'Giveaway (10m)', id: '787497518241153025' }, + { name: 'Giveaway (5m)', id: '787497519768403989' }, + { name: 'Giveaway (1m)', id: '787497521084891166' }, + { name: 'Suggester', id: '811922322767609877' }, + { name: 'Partner', id: '767324547312779274' }, + { name: 'Level Locked', id: '784248899044769792' }, + { name: 'No Files', id: '786421005039173633' }, + { name: 'No Reactions', id: '786421270924361789' }, + { name: 'No Links', id: '786421269356740658' }, + { name: 'No Bots', id: '786804858765312030' }, + { name: 'No VC', id: '788850482554208267' }, + { name: 'No Giveaways', id: '808265422334984203' }, + { name: 'No Support', id: '790247359824396319' } +] as const); + +export type PronounCode = keyof typeof pronounMapping; +export type Pronoun = typeof pronounMapping[PronounCode]; diff --git a/lib/utils/BushLogger.ts b/lib/utils/BushLogger.ts new file mode 100644 index 0000000..4acda69 --- /dev/null +++ b/lib/utils/BushLogger.ts @@ -0,0 +1,315 @@ +import chalk from 'chalk'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { bold, Client, EmbedBuilder, escapeMarkdown, PartialTextBasedChannelFields, type Message } from 'discord.js'; +import { stripVTControlCharacters as stripColor } from 'node:util'; +import repl, { REPLServer, REPL_MODE_STRICT } from 'repl'; +import { WriteStream } from 'tty'; +import { type SendMessageType } from '../extensions/discord-akairo/BushClient.js'; +import { colors } from './BushConstants.js'; +import { inspect } from './BushUtils.js'; + +let REPL: REPLServer; +let replGone = false; + +export function init() { + const kFormatForStdout = Object.getOwnPropertySymbols(console).find((sym) => sym.toString() === 'Symbol(kFormatForStdout)')!; + const kFormatForStderr = Object.getOwnPropertySymbols(console).find((sym) => sym.toString() === 'Symbol(kFormatForStderr)')!; + + REPL = repl.start({ + useColors: true, + terminal: true, + useGlobal: true, + replMode: REPL_MODE_STRICT, + breakEvalOnSigint: true, + ignoreUndefined: true + }); + + const apply = (stream: WriteStream, symbol: symbol): ProxyHandler['apply'] => + function apply(target, thisArg, args) { + if (stream.isTTY) { + stream.moveCursor(0, -1); + stream.write('\n'); + stream.clearLine(0); + } + + const ret = target(...args); + + if (stream.isTTY) { + const formatted = (console as any)[symbol](args) as string; + + stream.moveCursor(0, formatted.split('\n').length); + if (!replGone) { + REPL.displayPrompt(true); + } + } + + return ret; + }; + + global.console.log = new Proxy(console.log, { + apply: apply(process.stdout, kFormatForStdout) + }); + + global.console.warn = new Proxy(console.warn, { + apply: apply(process.stderr, kFormatForStderr) + }); + + REPL.on('exit', () => { + replGone = true; + process.exit(0); + }); +} + +/** + * Parses the content surrounding by `<<>>` and emphasizes it with the given color or by making it bold. + * @param content The content to parse. + * @param color The color to emphasize the content with. + * @param discordFormat Whether or not to format the content for discord. + * @returns The formatted content. + */ +function parseFormatting( + content: any, + color: 'blueBright' | 'blackBright' | 'redBright' | 'yellowBright' | 'greenBright' | '', + discordFormat = false +): string | typeof content { + if (typeof content !== 'string') return content; + return content + .split(/<<|>>/) + .map((value, index) => { + if (discordFormat) { + return index % 2 === 0 ? escapeMarkdown(value) : bold(escapeMarkdown(value)); + } else { + return index % 2 === 0 || !color ? value : chalk[color](value); + } + }) + .join(''); +} + +/** + * Inspects the content and returns a string. + * @param content The content to inspect. + * @param depth The depth the content will inspected. Defaults to `2`. + * @param colors Whether or not to use colors in the output. Defaults to `true`. + * @returns The inspected content. + */ +function inspectContent(content: any, depth = 2, colors = true): string { + if (typeof content !== 'string') { + return inspect(content, { depth, colors }); + } + return content; +} + +/** + * Generates a formatted timestamp for logging. + * @returns The formatted timestamp. + */ +function getTimeStamp(): string { + const now = new Date(); + const minute = pad(now.getMinutes()); + const hour = pad(now.getHours()); + const date = `${pad(now.getMonth() + 1)}/${pad(now.getDate())}`; + return `${date} ${hour}:${minute}`; +} + +/** + * Pad a two-digit number. + */ +function pad(num: number) { + return num.toString().padStart(2, '0'); +} + +/** + * Custom logging utility for the bot. + */ +export class BushLogger { + /** + * @param client The client. + */ + public constructor(public client: Client) {} + + /** + * Logs information. Highlight information by surrounding it in `<<>>`. + * @param header The header displayed before the content, displayed in cyan. + * @param content The content to log, highlights displayed in bright blue. + * @param sendChannel Should this also be logged to discord? Defaults to false. + * @param depth The depth the content will inspected. Defaults to 0. + */ + public get log() { + return this.info; + } + + /** + * Sends a message to the log channel. + * @param message The parameter to pass to {@link PartialTextBasedChannelFields.send}. + * @returns The message sent. + */ + public async channelLog(message: SendMessageType): Promise { + const channel = await this.client.utils.getConfigChannel('log'); + if (channel === null) return null; + return await channel.send(message).catch(() => null); + } + + /** + * Sends a message to the error channel. + * @param message The parameter to pass to {@link PartialTextBasedChannelFields.send}. + * @returns The message sent. + */ + public async channelError(message: SendMessageType): Promise { + const channel = await this.client.utils.getConfigChannel('error'); + if (!channel) { + void this.error( + 'BushLogger', + `Could not find error channel, was originally going to send: \n${inspect(message, { + colors: true + })}\n${new Error().stack?.substring(8)}`, + false + ); + return null; + } + return await channel.send(message); + } + + /** + * Logs debug information. Only works in dev is enabled in the config. + * @param content The content to log. + * @param depth The depth the content will inspected. Defaults to `0`. + */ + public debug(content: any, depth = 0): void { + if (!this.client.config.isDevelopment) return; + const newContent = inspectContent(content, depth, true); + console.log(`${chalk.bgMagenta(getTimeStamp())} ${chalk.magenta('[Debug]')} ${newContent}`); + } + + /** + * Logs raw debug information. Only works in dev is enabled in the config. + * @param content The content to log. + */ + public debugRaw(...content: any): void { + if (!this.client.config.isDevelopment) return; + console.log(`${chalk.bgMagenta(getTimeStamp())} ${chalk.magenta('[Debug]')}`, ...content); + } + + /** + * Logs verbose information. Highlight information by surrounding it in `<<>>`. + * @param header The header printed before the content, displayed in grey. + * @param content The content to log, highlights displayed in bright black. + * @param sendChannel Should this also be logged to discord? Defaults to `false`. + * @param depth The depth the content will inspected. Defaults to `0`. + */ + public async verbose(header: string, content: any, sendChannel = false, depth = 0): Promise { + if (!this.client.config.logging.verbose) return; + const newContent = inspectContent(content, depth, true); + console.log(`${chalk.bgGrey(getTimeStamp())} ${chalk.grey(`[${header}]`)} ${parseFormatting(newContent, 'blackBright')}`); + if (!sendChannel) return; + const embed = new EmbedBuilder() + .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) + .setColor(colors.gray) + .setTimestamp(); + await this.channelLog({ embeds: [embed] }); + } + + /** + * Logs very verbose information. Highlight information by surrounding it in `<<>>`. + * @param header The header printed before the content, displayed in purple. + * @param content The content to log, highlights displayed in bright black. + * @param depth The depth the content will inspected. Defaults to `0`. + */ + public async superVerbose(header: string, content: any, depth = 0): Promise { + if (!this.client.config.logging.verbose) return; + const newContent = inspectContent(content, depth, true); + console.log( + `${chalk.bgHex('#949494')(getTimeStamp())} ${chalk.hex('#949494')(`[${header}]`)} ${chalk.hex('#b3b3b3')(newContent)}` + ); + } + + /** + * Logs raw very verbose information. + * @param header The header printed before the content, displayed in purple. + * @param content The content to log. + */ + public async superVerboseRaw(header: string, ...content: any[]): Promise { + if (!this.client.config.logging.verbose) return; + console.log(`${chalk.bgHex('#a3a3a3')(getTimeStamp())} ${chalk.hex('#a3a3a3')(`[${header}]`)}`, ...content); + } + + /** + * Logs information. Highlight information by surrounding it in `<<>>`. + * @param header The header displayed before the content, displayed in cyan. + * @param content The content to log, highlights displayed in bright blue. + * @param sendChannel Should this also be logged to discord? Defaults to `false`. + * @param depth The depth the content will inspected. Defaults to `0`. + */ + public async info(header: string, content: any, sendChannel = true, depth = 0): Promise { + if (!this.client.config.logging.info) return; + const newContent = inspectContent(content, depth, true); + console.log(`${chalk.bgCyan(getTimeStamp())} ${chalk.cyan(`[${header}]`)} ${parseFormatting(newContent, 'blueBright')}`); + if (!sendChannel) return; + const embed = new EmbedBuilder() + .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) + .setColor(colors.info) + .setTimestamp(); + await this.channelLog({ embeds: [embed] }); + } + + /** + * Logs warnings. Highlight information by surrounding it in `<<>>`. + * @param header The header displayed before the content, displayed in yellow. + * @param content The content to log, highlights displayed in bright yellow. + * @param sendChannel Should this also be logged to discord? Defaults to `false`. + * @param depth The depth the content will inspected. Defaults to `0`. + */ + public async warn(header: string, content: any, sendChannel = true, depth = 0): Promise { + const newContent = inspectContent(content, depth, true); + console.warn( + `${chalk.bgYellow(getTimeStamp())} ${chalk.yellow(`[${header}]`)} ${parseFormatting(newContent, 'yellowBright')}` + ); + + if (!sendChannel) return; + const embed = new EmbedBuilder() + .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) + .setColor(colors.warn) + .setTimestamp(); + await this.channelError({ embeds: [embed] }); + } + + /** + * Logs errors. Highlight information by surrounding it in `<<>>`. + * @param header The header displayed before the content, displayed in bright red. + * @param content The content to log, highlights displayed in bright red. + * @param sendChannel Should this also be logged to discord? Defaults to `false`. + * @param depth The depth the content will inspected. Defaults to `0`. + */ + public async error(header: string, content: any, sendChannel = true, depth = 0): Promise { + const newContent = inspectContent(content, depth, true); + console.warn( + `${chalk.bgRedBright(getTimeStamp())} ${chalk.redBright(`[${header}]`)} ${parseFormatting(newContent, 'redBright')}` + ); + if (!sendChannel) return; + const embed = new EmbedBuilder() + .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) + .setColor(colors.error) + .setTimestamp(); + await this.channelError({ embeds: [embed] }); + return; + } + + /** + * Logs successes. Highlight information by surrounding it in `<<>>`. + * @param header The header displayed before the content, displayed in green. + * @param content The content to log, highlights displayed in bright green. + * @param sendChannel Should this also be logged to discord? Defaults to `false`. + * @param depth The depth the content will inspected. Defaults to `0`. + */ + public async success(header: string, content: any, sendChannel = true, depth = 0): Promise { + const newContent = inspectContent(content, depth, true); + console.log( + `${chalk.bgGreen(getTimeStamp())} ${chalk.greenBright(`[${header}]`)} ${parseFormatting(newContent, 'greenBright')}` + ); + if (!sendChannel) return; + const embed = new EmbedBuilder() + .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) + .setColor(colors.success) + .setTimestamp(); + await this.channelLog({ embeds: [embed] }).catch(() => {}); + } +} diff --git a/lib/utils/BushUtils.ts b/lib/utils/BushUtils.ts new file mode 100644 index 0000000..34ea461 --- /dev/null +++ b/lib/utils/BushUtils.ts @@ -0,0 +1,613 @@ +import { + Arg, + BushClient, + CommandMessage, + SlashEditMessageType, + SlashSendMessageType, + timeUnits, + type BaseBushArgumentType, + type BushInspectOptions, + type SlashMessage +} from '#lib'; +import { humanizeDuration as humanizeDurationMod } from '@notenoughupdates/humanize-duration'; +import assert from 'assert/strict'; +import cp from 'child_process'; +import deepLock from 'deep-lock'; +import { Util as AkairoUtil } from 'discord-akairo'; +import { + Constants as DiscordConstants, + EmbedBuilder, + Message, + OAuth2Scopes, + PermissionFlagsBits, + PermissionsBitField, + type APIEmbed, + type APIMessage, + type CommandInteraction, + type InteractionReplyOptions, + type PermissionsString +} from 'discord.js'; +import got from 'got'; +import { DeepWritable } from 'ts-essentials'; +import { inspect as inspectUtil, promisify } from 'util'; +import * as Format from './Format.js'; + +export type StripPrivate = { [K in keyof T]: T[K] extends Record ? StripPrivate : T[K] }; +export type ValueOf = T[keyof T]; + +/** + * Capitalizes the first letter of the given text + * @param text The text to capitalize + * @returns The capitalized text + */ +export function capitalize(text: string): string { + return text.charAt(0).toUpperCase() + text.slice(1); +} + +export const exec = promisify(cp.exec); + +/** + * Runs a shell command and gives the output + * @param command The shell command to run + * @returns The stdout and stderr of the shell command + */ +export async function shell(command: string): Promise<{ stdout: string; stderr: string }> { + return await exec(command); +} + +/** + * Appends the correct ordinal to the given number + * @param n The number to append an ordinal to + * @returns The number with the ordinal + */ +export function ordinal(n: number): string { + const s = ['th', 'st', 'nd', 'rd'], + v = n % 100; + return n + (s[(v - 20) % 10] || s[v] || s[0]); +} + +/** + * Chunks an array to the specified size + * @param arr The array to chunk + * @param perChunk The amount of items per chunk + * @returns The chunked array + */ +export function chunk(arr: T[], perChunk: number): T[][] { + return arr.reduce((all, one, i) => { + const ch: number = Math.floor(i / perChunk); + (all as any[])[ch] = [].concat(all[ch] || [], one as any); + return all; + }, []); +} + +/** + * Fetches a user's uuid from the mojang api. + * @param username The username to get the uuid of. + * @returns The the uuid of the user. + */ +export async function mcUUID(username: string, dashed = false): Promise { + const apiRes = (await got.get(`https://api.ashcon.app/mojang/v2/user/${username}`).json()) as UuidRes; + + return dashed ? apiRes.uuid : apiRes.uuid.replace(/-/g, ''); +} + +export interface UuidRes { + uuid: string; + username: string; + username_history?: { username: string }[] | null; + textures: { + custom: boolean; + slim: boolean; + skin: { + url: string; + data: string; + }; + raw: { + value: string; + signature: string; + }; + }; + created_at: string; +} + +/** + * Generate defaults for {@link inspect}. + * @param options The options to create defaults with. + * @returns The default options combined with the specified options. + */ +function getDefaultInspectOptions(options?: BushInspectOptions): BushInspectOptions { + return { + showHidden: options?.showHidden ?? false, + depth: options?.depth ?? 2, + colors: options?.colors ?? false, + customInspect: options?.customInspect ?? true, + showProxy: options?.showProxy ?? false, + maxArrayLength: options?.maxArrayLength ?? Infinity, + maxStringLength: options?.maxStringLength ?? Infinity, + breakLength: options?.breakLength ?? 80, + compact: options?.compact ?? 3, + sorted: options?.sorted ?? false, + getters: options?.getters ?? true, + numericSeparator: options?.numericSeparator ?? true + }; +} + +/** + * Uses {@link inspect} with custom defaults. + * @param object - The object you would like to inspect. + * @param options - The options you would like to use to inspect the object. + * @returns The inspected object. + */ +export function inspect(object: any, options?: BushInspectOptions): string { + const optionsWithDefaults = getDefaultInspectOptions(options); + + if (!optionsWithDefaults.inspectStrings && typeof object === 'string') return object; + + return inspectUtil(object, optionsWithDefaults); +} + +/** + * Responds to a slash command interaction. + * @param interaction The interaction to respond to. + * @param responseOptions The options for the response. + * @returns The message sent. + */ +export async function slashRespond( + interaction: CommandInteraction, + responseOptions: SlashSendMessageType | SlashEditMessageType +): Promise { + const newResponseOptions = typeof responseOptions === 'string' ? { content: responseOptions } : responseOptions; + if (interaction.replied || interaction.deferred) { + delete (newResponseOptions as InteractionReplyOptions).ephemeral; // Cannot change a preexisting message to be ephemeral + return (await interaction.editReply(newResponseOptions)) as Message | APIMessage; + } else { + await interaction.reply(newResponseOptions); + return await interaction.fetchReply().catch(() => undefined); + } +} + +/** + * Takes an array and combines the elements using the supplied conjunction. + * @param array The array to combine. + * @param conjunction The conjunction to use. + * @param ifEmpty What to return if the array is empty. + * @returns The combined elements or `ifEmpty`. + * + * @example + * const permissions = oxford(['Administrator', 'SendMessages', 'ManageMessages'], 'and', 'none'); + * console.log(permissions); // Administrator, SendMessages and ManageMessages + */ +export function oxford(array: string[], conjunction: string, ifEmpty?: string): string | undefined { + const l = array.length; + if (!l) return ifEmpty; + if (l < 2) return array[0]; + if (l < 3) return array.join(` ${conjunction} `); + array = array.slice(); + array[l - 1] = `${conjunction} ${array[l - 1]}`; + return array.join(', '); +} + +/** + * Add or remove an item from an array. All duplicates will be removed. + * @param action Either `add` or `remove` an element. + * @param array The array to add/remove an element from. + * @param value The element to add/remove from the array. + */ +export function addOrRemoveFromArray(action: 'add' | 'remove', array: T[], value: T): T[] { + const set = new Set(array); + action === 'add' ? set.add(value) : set.delete(value); + return [...set]; +} + +/** + * Remove an item from an array. All duplicates will be removed. + * @param array The array to remove an element from. + * @param value The element to remove from the array. + */ +export function removeFromArray(array: T[], value: T): T[] { + return addOrRemoveFromArray('remove', array, value); +} + +/** + * Add an item from an array. All duplicates will be removed. + * @param array The array to add an element to. + * @param value The element to add to the array. + */ +export function addToArray(array: T[], value: T): T[] { + return addOrRemoveFromArray('add', array, value); +} + +/** + * Surrounds a string to the begging an end of each element in an array. + * @param array The array you want to surround. + * @param surroundChar1 The character placed in the beginning of the element. + * @param surroundChar2 The character placed in the end of the element. Defaults to `surroundChar1`. + */ +export function surroundArray(array: string[], surroundChar1: string, surroundChar2?: string): string[] { + return array.map((a) => `${surroundChar1}${a}${surroundChar2 ?? surroundChar1}`); +} + +/** + * Gets the duration from a specified string. + * @param content The string to look for a duration in. + * @param remove Whether or not to remove the duration from the original string. + * @returns The {@link ParsedDuration}. + */ +export function parseDuration(content: string, remove = true): ParsedDuration { + if (!content) return { duration: 0, content: null }; + + // eslint-disable-next-line prefer-const + let duration: number | null = null; + // Try to reduce false positives by requiring a space before the duration, this makes sure it still matches if it is + // in the beginning of the argument + let contentWithoutTime = ` ${content}`; + + for (const unit in timeUnits) { + const regex = timeUnits[unit as keyof typeof timeUnits].match; + const match = regex.exec(contentWithoutTime); + const value = Number(match?.groups?.[unit]); + if (!isNaN(value)) duration! += value * timeUnits[unit as keyof typeof timeUnits].value; + + if (remove) contentWithoutTime = contentWithoutTime.replace(regex, ''); + } + // remove the space added earlier + if (contentWithoutTime.startsWith(' ')) contentWithoutTime.replace(' ', ''); + return { duration, content: contentWithoutTime }; +} + +export interface ParsedDuration { + duration: number | null; + content: string | null; +} + +/** + * Converts a duration in milliseconds to a human readable form. + * @param duration The duration in milliseconds to convert. + * @param largest The maximum number of units to display for the duration. + * @param round Whether or not to round the smallest unit displayed. + * @returns A humanized string of the duration. + */ +export function humanizeDuration(duration: number, largest?: number, round = true): string { + if (largest) return humanizeDurationMod(duration, { language: 'en', maxDecimalPoints: 2, largest, round })!; + else return humanizeDurationMod(duration, { language: 'en', maxDecimalPoints: 2, round })!; +} + +/** + * Creates a formatted relative timestamp from a duration in milliseconds. + * @param duration The duration in milliseconds. + * @returns The formatted relative timestamp. + */ +export function timestampDuration(duration: number): string { + return ``; +} + +/** + * Creates a timestamp from a date. + * @param date The date to create a timestamp from. + * @param style The style of the timestamp. + * @returns The formatted timestamp. + * + * @see + * **Styles:** + * - **t**: Short Time ex. `16:20` + * - **T**: Long Time ex. `16:20:30 ` + * - **d**: Short Date ex. `20/04/2021` + * - **D**: Long Date ex. `20 April 2021` + * - **f**: Short Date/Time ex. `20 April 2021 16:20` + * - **F**: Long Date/Time ex. `Tuesday, 20 April 2021 16:20` + * - **R**: Relative Time ex. `2 months ago` + */ +export function timestamp( + date: D, + style: TimestampStyle = 'f' +): D extends Date ? string : undefined { + if (!date) return date as unknown as D extends Date ? string : undefined; + return `` as unknown as D extends Date ? string : undefined; +} + +export type TimestampStyle = 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R'; + +/** + * Creates a human readable representation between a date and the current time. + * @param date The date to be compared with the current time. + * @param largest The maximum number of units to display for the duration. + * @param round Whether or not to round the smallest unit displayed. + * @returns A humanized string of the delta. + */ +export function dateDelta(date: Date, largest = 3, round = true): string { + return humanizeDuration(new Date().getTime() - date.getTime(), largest, round); +} + +/** + * Combines {@link timestamp} and {@link dateDelta} + * @param date The date to be compared with the current time. + * @param style The style of the timestamp. + * @returns The formatted timestamp. + * + * @see + * **Styles:** + * - **t**: Short Time ex. `16:20` + * - **T**: Long Time ex. `16:20:30 ` + * - **d**: Short Date ex. `20/04/2021` + * - **D**: Long Date ex. `20 April 2021` + * - **f**: Short Date/Time ex. `20 April 2021 16:20` + * - **F**: Long Date/Time ex. `Tuesday, 20 April 2021 16:20` + * - **R**: Relative Time ex. `2 months ago` + */ +export function timestampAndDelta(date: Date, style: TimestampStyle = 'D'): string { + return `${timestamp(date, style)} (${dateDelta(date)} ago)`; +} + +/** + * Convert a hex code to an rbg value. + * @param hex The hex code to convert. + * @returns The rbg value. + */ +export function hexToRgb(hex: string): string { + const arrBuff = new ArrayBuffer(4); + const vw = new DataView(arrBuff); + vw.setUint32(0, parseInt(hex, 16), false); + const arrByte = new Uint8Array(arrBuff); + + return `${arrByte[1]}, ${arrByte[2]}, ${arrByte[3]}`; +} + +/** + * Wait an amount in milliseconds. + * @returns A promise that resolves after the specified amount of milliseconds + */ +export const sleep = promisify(setTimeout); + +/** + * List the methods of an object. + * @param obj The object to get the methods of. + * @returns A string with each method on a new line. + */ +export function getMethods(obj: Record): string { + // modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class/31055217#31055217 + let props: string[] = []; + let obj_: Record = new Object(obj); + + do { + const l = Object.getOwnPropertyNames(obj_) + .concat(Object.getOwnPropertySymbols(obj_).map((s) => s.toString())) + .sort() + .filter( + (p, i, arr) => + typeof Object.getOwnPropertyDescriptor(obj_, p)?.['get'] !== 'function' && // ignore getters + typeof Object.getOwnPropertyDescriptor(obj_, p)?.['set'] !== 'function' && // ignore setters + typeof obj_[p] === 'function' && // only the methods + p !== 'constructor' && // not the constructor + (i == 0 || p !== arr[i - 1]) && // not overriding in this prototype + props.indexOf(p) === -1 // not overridden in a child + ); + + const reg = /\(([\s\S]*?)\)/; + props = props.concat( + l.map( + (p) => + `${obj_[p] && obj_[p][Symbol.toStringTag] === 'AsyncFunction' ? 'async ' : ''}function ${p}(${ + reg.exec(obj_[p].toString())?.[1] + ? reg + .exec(obj_[p].toString())?.[1] + .split(', ') + .map((arg) => arg.split('=')[0].trim()) + .join(', ') + : '' + });` + ) + ); + } while ( + (obj_ = Object.getPrototypeOf(obj_)) && // walk-up the prototype chain + Object.getPrototypeOf(obj_) // not the the Object prototype methods (hasOwnProperty, etc...) + ); + + return props.join('\n'); +} + +/** + * List the symbols of an object. + * @param obj The object to get the symbols of. + * @returns An array of the symbols of the object. + */ +export function getSymbols(obj: Record): symbol[] { + let symbols: symbol[] = []; + let obj_: Record = new Object(obj); + + do { + const l = Object.getOwnPropertySymbols(obj_).sort(); + + symbols = [...symbols, ...l]; + } while ( + (obj_ = Object.getPrototypeOf(obj_)) && // walk-up the prototype chain + Object.getPrototypeOf(obj_) // not the the Object prototype methods (hasOwnProperty, etc...) + ); + + return symbols; +} + +/** + * Checks if a user has a certain guild permission (doesn't check channel permissions). + * @param message The message to check the user from. + * @param permissions The permissions to check for. + * @returns The missing permissions or null if none are missing. + */ +export function userGuildPermCheck( + message: CommandMessage | SlashMessage, + permissions: typeof PermissionFlagsBits[keyof typeof PermissionFlagsBits][] +): PermissionsString[] | null { + if (!message.inGuild()) return null; + const missing = message.member?.permissions.missing(permissions) ?? []; + + return missing.length ? missing : null; +} + +/** + * Check if the client has certain permissions in the guild (doesn't check channel permissions). + * @param message The message to check the client user from. + * @param permissions The permissions to check for. + * @returns The missing permissions or null if none are missing. + */ +export function clientGuildPermCheck(message: CommandMessage | SlashMessage, permissions: bigint[]): PermissionsString[] | null { + const missing = message.guild?.members.me?.permissions.missing(permissions) ?? []; + + return missing.length ? missing : null; +} + +/** + * Check if the client has permission to send messages in the channel as well as check if they have other permissions + * in the guild (or the channel if `checkChannel` is `true`). + * @param message The message to check the client user from. + * @param permissions The permissions to check for. + * @param checkChannel Whether to check the channel permissions instead of the guild permissions. + * @returns The missing permissions or null if none are missing. + */ +export function clientSendAndPermCheck( + message: CommandMessage | SlashMessage, + permissions: bigint[] = [], + checkChannel = false +): PermissionsString[] | null { + if (!message.inGuild() || !message.channel) return null; + + const missing: PermissionsString[] = []; + const sendPerm = message.channel.isThread() ? 'SendMessages' : 'SendMessagesInThreads'; + + // todo: remove once forum channels are fixed + if (message.channel.parent === null && message.channel.isThread()) return null; + + if (!message.guild.members.me!.permissionsIn(message.channel!.id).has(sendPerm)) missing.push(sendPerm); + + missing.push( + ...(checkChannel + ? message.guild!.members.me!.permissionsIn(message.channel!.id!).missing(permissions) + : clientGuildPermCheck(message, permissions) ?? []) + ); + + return missing.length ? missing : null; +} + +export { deepLock as deepFreeze }; +export { Arg as arg }; +export { Format as format }; +export { DiscordConstants as discordConstants }; +export { AkairoUtil as akairo }; + +/** + * The link to invite the bot with all permissions. + */ +export function invite(client: BushClient) { + return client.generateInvite({ + permissions: + PermissionsBitField.All - + PermissionFlagsBits.UseEmbeddedActivities - + PermissionFlagsBits.ViewGuildInsights - + PermissionFlagsBits.Stream, + scopes: [OAuth2Scopes.Bot, OAuth2Scopes.ApplicationsCommands] + }); +} + +/** + * Asset multiple statements at a time. + * @param args + */ +export function assertAll(...args: any[]): void { + for (let i = 0; i < args.length; i++) { + assert(args[i], `assertAll index ${i} failed`); + } +} + +/** + * Casts a string to a duration and reason for slash commands. + * @param arg The argument received. + * @param message The message that triggered the command. + * @returns The casted argument. + */ +export async function castDurationContent( + arg: string | ParsedDuration | null, + message: CommandMessage | SlashMessage +): Promise { + const res = typeof arg === 'string' ? await Arg.cast('contentWithDuration', message, arg) : arg; + + return { duration: res?.duration ?? 0, content: res?.content ?? '' }; +} + +export interface ParsedDurationRes { + duration: number; + content: string; +} + +/** + * Casts a string to a the specified argument type. + * @param type The type of the argument to cast to. + * @param arg The argument received. + * @param message The message that triggered the command. + * @returns The casted argument. + */ +export async function cast( + type: T, + arg: BaseBushArgumentType[T] | string, + message: CommandMessage | SlashMessage +) { + return typeof arg === 'string' ? await Arg.cast(type, message, arg) : arg; +} + +/** + * Overflows the description of an embed into multiple embeds. + * @param embed The options to be applied to the (first) embed. + * @param lines Each line of the description as an element in an array. + */ +export function overflowEmbed(embed: Omit, lines: string[], maxLength = 4096): EmbedBuilder[] { + const embeds: EmbedBuilder[] = []; + + const makeEmbed = () => { + embeds.push(new EmbedBuilder().setColor(embed.color ?? null)); + return embeds.at(-1)!; + }; + + for (const line of lines) { + let current = embeds.length ? embeds.at(-1)! : makeEmbed(); + let joined = current.data.description ? `${current.data.description}\n${line}` : line; + if (joined.length > maxLength) { + current = makeEmbed(); + joined = line; + } + + current.setDescription(joined); + } + + if (!embeds.length) makeEmbed(); + + if (embed.author) embeds.at(0)?.setAuthor(embed.author); + if (embed.title) embeds.at(0)?.setTitle(embed.title); + if (embed.url) embeds.at(0)?.setURL(embed.url); + if (embed.fields) embeds.at(-1)?.setFields(embed.fields); + if (embed.thumbnail) embeds.at(-1)?.setThumbnail(embed.thumbnail.url); + if (embed.footer) embeds.at(-1)?.setFooter(embed.footer); + if (embed.image) embeds.at(-1)?.setImage(embed.image.url); + if (embed.timestamp) embeds.at(-1)?.setTimestamp(new Date(embed.timestamp)); + + return embeds; +} + +/** + * Formats an error into a string. + * @param error The error to format. + * @param colors Whether to use colors in the output. + * @returns The formatted error. + */ +export function formatError(error: Error | any, colors = false): string { + if (!error) return error; + if (typeof error !== 'object') return String.prototype.toString.call(error); + if ( + getSymbols(error) + .map((s) => s.toString()) + .includes('Symbol(nodejs.util.inspect.custom)') + ) + return inspect(error, { colors }); + + return error.stack; +} + +export function deepWriteable(obj: T): DeepWritable { + return obj as DeepWritable; +} diff --git a/lib/utils/Format.ts b/lib/utils/Format.ts new file mode 100644 index 0000000..debaf4b --- /dev/null +++ b/lib/utils/Format.ts @@ -0,0 +1,119 @@ +import { type CodeBlockLang } from '#lib'; +import { + bold as discordBold, + codeBlock as discordCodeBlock, + escapeBold as discordEscapeBold, + escapeCodeBlock as discordEscapeCodeBlock, + escapeInlineCode as discordEscapeInlineCode, + escapeItalic as discordEscapeItalic, + escapeMarkdown, + escapeSpoiler as discordEscapeSpoiler, + escapeStrikethrough as discordEscapeStrikethrough, + escapeUnderline as discordEscapeUnderline, + inlineCode as discordInlineCode, + italic as discordItalic, + spoiler as discordSpoiler, + strikethrough as discordStrikethrough, + underscore as discordUnderscore +} from 'discord.js'; + +/** + * Wraps the content inside a codeblock with no language. + * @param content The content to wrap. + */ +export function codeBlock(content: string): string; + +/** + * Wraps the content inside a codeblock with the specified language. + * @param language The language for the codeblock. + * @param content The content to wrap. + */ +export function codeBlock(language: CodeBlockLang, content: string): string; +export function codeBlock(languageOrContent: string, content?: string): string { + return typeof content === 'undefined' + ? discordCodeBlock(discordEscapeCodeBlock(`${languageOrContent}`)) + : discordCodeBlock(`${languageOrContent}`, discordEscapeCodeBlock(`${content}`)); +} + +/** + * Wraps the content inside \`backticks\`, which formats it as inline code. + * @param content The content to wrap. + */ +export function inlineCode(content: string): string { + return discordInlineCode(discordEscapeInlineCode(`${content}`)); +} + +/** + * Formats the content into italic text. + * @param content The content to wrap. + */ +export function italic(content: string): string { + return discordItalic(discordEscapeItalic(`${content}`)); +} + +/** + * Formats the content into bold text. + * @param content The content to wrap. + */ +export function bold(content: string): string { + return discordBold(discordEscapeBold(`${content}`)); +} + +/** + * Formats the content into underscored text. + * @param content The content to wrap. + */ +export function underscore(content: string): string { + return discordUnderscore(discordEscapeUnderline(`${content}`)); +} + +/** + * Formats the content into strike-through text. + * @param content The content to wrap. + */ +export function strikethrough(content: string): string { + return discordStrikethrough(discordEscapeStrikethrough(`${content}`)); +} + +/** + * Wraps the content inside spoiler (hidden text). + * @param content The content to wrap. + */ +export function spoiler(content: string): string { + return discordSpoiler(discordEscapeSpoiler(`${content}`)); +} + +/** + * Formats input: makes it bold and escapes any other markdown + * @param text The input + */ +export function input(text: string): string { + return bold(sanitizeInputForDiscord(`${text}`)); +} + +/** + * Formats input for logs: makes it highlighted + * @param text The input + */ +export function inputLog(text: string): string { + return `<<${sanitizeWtlAndControl(`${text}`)}>>`; +} + +/** + * Removes all characters in a string that are either control characters or change the direction of text etc. + * @param str The string you would like sanitized + */ +export function sanitizeWtlAndControl(str: string) { + // eslint-disable-next-line no-control-regex + return `${str}`.replace(/[\u0000-\u001F\u007F-\u009F\u200B]/g, ''); +} + +/** + * Removed wtl and control characters and escapes any other markdown + * @param text The input + */ +export function sanitizeInputForDiscord(text: string): string { + return escapeMarkdown(sanitizeWtlAndControl(`${text}`)); +} + +export { escapeMarkdown } from 'discord.js'; diff --git a/lib/utils/Minecraft.ts b/lib/utils/Minecraft.ts new file mode 100644 index 0000000..bb5fbfe --- /dev/null +++ b/lib/utils/Minecraft.ts @@ -0,0 +1,351 @@ +/* eslint-disable */ + +import { Byte, Int, parse } from '@ironm00n/nbt-ts'; +import { BitField } from 'discord.js'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export enum FormattingCodes { + Black = '§0', + DarkBlue = '§1', + DarkGreen = '§2', + DarkAqua = '§3', + DarkRed = '§4', + DarkPurple = '§5', + Gold = '§6', + Gray = '§7', + DarkGray = '§8', + Blue = '§9', + Green = '§a', + Aqua = '§b', + Red = '§c', + LightPurple = '§d', + Yellow = '§e', + White = '§f', + + Obfuscated = '§k', + Bold = '§l', + Strikethrough = '§m', + Underline = '§n', + Italic = '§o', + Reset = '§r' +} + +// https://minecraft.fandom.com/wiki/Formatting_codes +export const formattingInfo = { + [FormattingCodes.Black]: { + foreground: 'rgb(0, 0, 0)', + foregroundDarker: 'rgb(0, 0, 0)', + background: 'rgb(0, 0, 0)', + backgroundDarker: 'rgb(0, 0, 0)', + ansi: '\u001b[0;30m' + }, + [FormattingCodes.DarkBlue]: { + foreground: 'rgb(0, 0, 170)', + foregroundDarker: 'rgb(0, 0, 118)', + background: 'rgb(0, 0, 42)', + backgroundDarker: 'rgb(0, 0, 29)', + ansi: '\u001b[0;34m' + }, + [FormattingCodes.DarkGreen]: { + foreground: 'rgb(0, 170, 0)', + foregroundDarker: 'rgb(0, 118, 0)', + background: 'rgb(0, 42, 0)', + backgroundDarker: 'rgb(0, 29, 0)', + ansi: '\u001b[0;32m' + }, + [FormattingCodes.DarkAqua]: { + foreground: 'rgb(0, 170, 170)', + foregroundDarker: 'rgb(0, 118, 118)', + background: 'rgb(0, 42, 42)', + backgroundDarker: 'rgb(0, 29, 29)', + ansi: '\u001b[0;36m' + }, + [FormattingCodes.DarkRed]: { + foreground: 'rgb(170, 0, 0)', + foregroundDarker: 'rgb(118, 0, 0)', + background: 'rgb(42, 0, 0)', + backgroundDarker: 'rgb(29, 0, 0)', + ansi: '\u001b[0;31m' + }, + [FormattingCodes.DarkPurple]: { + foreground: 'rgb(170, 0, 170)', + foregroundDarker: 'rgb(118, 0, 118)', + background: 'rgb(42, 0, 42)', + backgroundDarker: 'rgb(29, 0, 29)', + ansi: '\u001b[0;35m' + }, + [FormattingCodes.Gold]: { + foreground: 'rgb(255, 170, 0)', + foregroundDarker: 'rgb(178, 118, 0)', + background: 'rgb(42, 42, 0)', + backgroundDarker: 'rgb(29, 29, 0)', + ansi: '\u001b[0;33m' + }, + [FormattingCodes.Gray]: { + foreground: 'rgb(170, 170, 170)', + foregroundDarker: 'rgb(118, 118, 118)', + background: 'rgb(42, 42, 42)', + backgroundDarker: 'rgb(29, 29, 29)', + ansi: '\u001b[0;37m' + }, + [FormattingCodes.DarkGray]: { + foreground: 'rgb(85, 85, 85)', + foregroundDarker: 'rgb(59, 59, 59)', + background: 'rgb(21, 21, 21)', + backgroundDarker: 'rgb(14, 14, 14)', + ansi: '\u001b[0;90m' + }, + [FormattingCodes.Blue]: { + foreground: 'rgb(85, 85, 255)', + foregroundDarker: 'rgb(59, 59, 178)', + background: 'rgb(21, 21, 63)', + backgroundDarker: 'rgb(14, 14, 44)', + ansi: '\u001b[0;94m' + }, + [FormattingCodes.Green]: { + foreground: 'rgb(85, 255, 85)', + foregroundDarker: 'rgb(59, 178, 59)', + background: 'rgb(21, 63, 21)', + backgroundDarker: 'rgb(14, 44, 14)', + ansi: '\u001b[0;92m' + }, + [FormattingCodes.Aqua]: { + foreground: 'rgb(85, 255, 255)', + foregroundDarker: 'rgb(59, 178, 178)', + background: 'rgb(21, 63, 63)', + backgroundDarker: 'rgb(14, 44, 44)', + ansi: '\u001b[0;96m' + }, + [FormattingCodes.Red]: { + foreground: 'rgb(255, 85, 85)', + foregroundDarker: 'rgb(178, 59, 59)', + background: 'rgb(63, 21, 21)', + backgroundDarker: 'rgb(44, 14, 14)', + ansi: '\u001b[0;91m' + }, + [FormattingCodes.LightPurple]: { + foreground: 'rgb(255, 85, 255)', + foregroundDarker: 'rgb(178, 59, 178)', + background: 'rgb(63, 21, 63)', + backgroundDarker: 'rgb(44, 14, 44)', + ansi: '\u001b[0;95m' + }, + [FormattingCodes.Yellow]: { + foreground: 'rgb(255, 255, 85)', + foregroundDarker: 'rgb(178, 178, 59)', + background: 'rgb(63, 63, 21)', + backgroundDarker: 'rgb(44, 44, 14)', + ansi: '\u001b[0;93m' + }, + [FormattingCodes.White]: { + foreground: 'rgb(255, 255, 255)', + foregroundDarker: 'rgb(178, 178, 178)', + background: 'rgb(63, 63, 63)', + backgroundDarker: 'rgb(44, 44, 44)', + ansi: '\u001b[0;97m' + }, + + [FormattingCodes.Obfuscated]: { ansi: '\u001b[8m' }, + [FormattingCodes.Bold]: { ansi: '\u001b[1m' }, + [FormattingCodes.Strikethrough]: { ansi: '\u001b[9m' }, + [FormattingCodes.Underline]: { ansi: '\u001b[4m' }, + [FormattingCodes.Italic]: { ansi: '\u001b[3m' }, + [FormattingCodes.Reset]: { ansi: '\u001b[0m' } +} as const; + +export type McItemId = Lowercase; +export type SbItemId = Uppercase; +export type MojangJson = string; +export type SbRecipeItem = `${SbItemId}:${number}` | ''; +export type SbRecipe = { + [Location in `${'A' | 'B' | 'C'}${1 | 2 | 3}`]: SbRecipeItem; +}; +export type InfoType = 'WIKI_URL' | ''; + +export type Slayer = `${'WOLF' | 'BLAZE' | 'EMAN'}_${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`; + +export interface RawNeuItem { + itemid: McItemId; + displayname: string; + nbttag: MojangJson; + damage: number; + lore: string[]; + recipe?: SbRecipe; + internalname: SbItemId; + modver: string; + infoType: InfoType; + info?: string[]; + crafttext: string; + vanilla?: boolean; + useneucraft?: boolean; + slayer_req?: Slayer; + clickcommand?: string; + x?: number; + y?: number; + z?: number; + island?: string; + recipes?: { type: string; cost: any[]; result: SbItemId }[]; + /** @deprecated */ + parent?: SbItemId; + noseal?: boolean; +} + +export enum HideFlagsBits { + Enchantments = 1, + AttributeModifiers = 2, + Unbreakable = 4, + CanDestroy = 8, + CanPlaceOn = 16, + /** + * potion effects, shield pattern info, "StoredEnchantments", written book + * "generation" and "author", "Explosion", "Fireworks", and map tooltips + */ + OtherInformation = 32, + Dyed = 64 +} + +export type HideFlagsString = keyof typeof HideFlagsBits; + +export class HideFlags extends BitField { + public static override Flags = HideFlagsBits; +} + +export const formattingCode = new RegExp( + `§[${Object.values(FormattingCodes) + .filter((v) => v.startsWith('§')) + .map((v) => v.substring(1)) + .join('')}]` +); + +export function removeMCFormatting(str: string) { + return str.replaceAll(formattingCode, ''); +} + +const repo = path.join(__dirname, '..', '..', '..', 'neu-item-repo-dangerous'); + +export interface NbtTag { + overrideMeta?: Byte; + Unbreakable?: Int; + ench?: string[]; + HideFlags?: HideFlags; + SkullOwner?: SkullOwner; + display?: NbtTagDisplay; + ExtraAttributes?: ExtraAttributes; +} + +export interface SkullOwner { + Id?: string; + Properties?: { + textures?: { Value?: string }[]; + }; +} + +export interface NbtTagDisplay { + Lore?: string[]; + color?: Int; + Name?: string; +} + +export type RuneId = string; + +export interface ExtraAttributes { + originTag?: Origin; + id?: string; + generator_tier?: Int; + boss_tier?: Int; + enchantments?: { hardened_mana?: Int }; + dungeon_item_level?: Int; + runes?: { [key: RuneId]: Int }; + petInfo?: PetInfo; +} + +export interface PetInfo { + type: 'ZOMBIE'; + active: boolean; + exp: number; + tier: 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'; + hideInfo: boolean; + candyUsed: number; +} + +export type Origin = 'SHOP_PURCHASE'; + +const neuConstantsPath = path.join(repo, 'constants'); +const neuPetsPath = path.join(neuConstantsPath, 'pets.json'); +const neuPets = (await import(neuPetsPath, { assert: { type: 'json' } })) as PetsConstants; +const neuPetNumsPath = path.join(neuConstantsPath, 'petnums.json'); +const neuPetNums = (await import(neuPetNumsPath, { assert: { type: 'json' } })) as PetNums; + +export interface PetsConstants { + pet_rarity_offset: Record; + pet_levels: number[]; + custom_pet_leveling: Record; + pet_types: Record; +} + +export interface PetNums { + [key: string]: { + [key: string]: { + '1': { + otherNums: number[]; + statNums: Record; + }; + '100': { + otherNums: number[]; + statNums: Record; + }; + 'stats_levelling_curve'?: `${number};${number};${number}`; + }; + }; +} + +export class NeuItem { + public itemId: McItemId; + public displayName: string; + public nbtTag: NbtTag; + public internalName: SbItemId; + public lore: string[]; + + public constructor(raw: RawNeuItem) { + this.itemId = raw.itemid; + this.nbtTag = parse(raw.nbttag); + this.displayName = raw.displayname; + this.internalName = raw.internalname; + this.lore = raw.lore; + + this.petLoreReplacements(); + } + + private petLoreReplacements(level = -1) { + if (/.*?;[0-5]$/.test(this.internalName) && this.displayName.includes('LVL')) { + const maxLevel = neuPets?.custom_pet_leveling?.[this.internalName]?.max_level ?? 100; + this.displayName = this.displayName.replace('LVL', `1➡${maxLevel}`); + + const nums = neuPetNums[this.internalName]; + if (!nums) throw new Error(`Pet (${this.internalName}) has no pet nums.`); + + const teir = ['COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY', 'MYTHIC'][+this.internalName.at(-1)!]; + const petInfoTier = nums[teir]; + if (!petInfoTier) throw new Error(`Pet (${this.internalName}) has no pet nums for ${teir} rarity.`); + + const curve = petInfoTier?.stats_levelling_curve?.split(';'); + + // todo: finish copying from neu + + const minStatsLevel = parseInt(curve?.[0] ?? '0'); + const maxStatsLevel = parseInt(curve?.[0] ?? '100'); + + const lore = ''; + } + } +} + +export function mcToAnsi(str: string) { + for (const format in formattingInfo) { + str = str.replaceAll(format, formattingInfo[format as keyof typeof formattingInfo].ansi); + } + return `${str}\u001b[0m`; +} diff --git a/lib/utils/Minecraft_Test.ts b/lib/utils/Minecraft_Test.ts new file mode 100644 index 0000000..26ca648 --- /dev/null +++ b/lib/utils/Minecraft_Test.ts @@ -0,0 +1,86 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { mcToAnsi, RawNeuItem } from './Minecraft.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const repo = path.join(__dirname, '..', '..', '..', '..', '..', 'neu-item-repo-dangerous'); +const itemPath = path.join(repo, 'items'); +const items = await fs.readdir(itemPath); + +// for (let i = 0; i < 5; i++) { +for (const path_ of items) { + // const randomItem = items[Math.floor(Math.random() * items.length)]; + // console.log(randomItem); + const item = (await import(path.join(itemPath, /* randomItem */ path_), { assert: { type: 'json' } })).default as RawNeuItem; + if (/.*?((_MONSTER)|(_NPC)|(_ANIMAL)|(_MINIBOSS)|(_BOSS)|(_SC))$/.test(item.internalname)) continue; + if (!/.*?;[0-5]$/.test(item.internalname)) continue; + /* console.log(path_); + console.dir(item, { depth: Infinity }); */ + + /* console.log('==========='); */ + // const nbt = parse(item.nbttag) as NbtTag; + + // if (nbt?.SkullOwner?.Properties?.textures?.[0]?.Value) { + // nbt.SkullOwner.Properties.textures[0].Value = parse( + // Buffer.from(nbt.SkullOwner.Properties.textures[0].Value, 'base64').toString('utf-8') + // ) as string; + // } + + // if (nbt.ExtraAttributes?.petInfo) { + // nbt.ExtraAttributes.petInfo = JSON.parse(nbt.ExtraAttributes.petInfo as any as string); + // } + + // delete nbt.display?.Lore; + + // console.dir(nbt, { depth: Infinity }); + // console.log('==========='); + + /* if (nbt?.display && nbt.display.Name !== item.displayname) + console.log(`${path_} display name mismatch: ${mcToAnsi(nbt.display.Name)} != ${mcToAnsi(item.displayname)}`); + + if (nbt?.ExtraAttributes && nbt?.ExtraAttributes.id !== item.internalname) + console.log(`${path_} internal name mismatch: ${mcToAnsi(nbt?.ExtraAttributes.id)} != ${mcToAnsi(item.internalname)}`); */ + + // console.log('==========='); + + console.log(mcToAnsi(item.displayname)); + console.log(item.lore.map((l) => mcToAnsi(l)).join('\n')); + + /* const keys = [ + 'itemid', + 'displayname', + 'nbttag', + 'damage', + 'lore', + 'recipe', + 'internalname', + 'modver', + 'infoType', + 'info', + 'crafttext', + 'vanilla', + 'useneucraft', + 'slayer_req', + 'clickcommand', + 'x', + 'y', + 'z', + 'island', + 'recipes', + 'parent', + 'noseal' + ]; + + Object.keys(item).forEach((k) => { + if (!keys.includes(k)) throw new Error(`Unknown key: ${k}`); + }); + + if ( + 'slayer_req' in item && + !new Array(10).flatMap((_, i) => ['WOLF', 'BLAZE', 'EMAN'].map((e) => e + (i + 1)).includes(item.slayer_req!)) + ) + throw new Error(`Unknown slayer req: ${item.slayer_req!}`); */ + + /* console.log('=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-'); */ +} diff --git a/misc/test.js b/misc/test.js new file mode 100644 index 0000000..d0840ec --- /dev/null +++ b/misc/test.js @@ -0,0 +1,365 @@ +/* eslint-disable */ +// @ts-check + +import { createCanvas, registerFont } from 'canvas'; +import fs from 'fs/promises'; +import path, { dirname, join } from 'path'; +import tinycolor from 'tinycolor2'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +registerFont(join(__dirname, 'assets', 'Faithful.ttf'), { family: 'ComplianceSans' }); +registerFont(join(dirname(fileURLToPath(import.meta.url)), 'assets', 'Roboto-Regular.ttf'), { family: 'Roboto' }); + +/** @typedef {string} McItemId */ +/** @typedef {string} SbItemId */ +/** @typedef {string} MojangJson */ +/** @typedef {`${SbItemId}:${number}` | ''} SbRecipeItem */ +/** @typedef {{[Location in `${'A' | 'B' | 'C'}${1 | 2 | 3}`]: SbRecipeItem;}} SbRecipe */ +/** @typedef {'WIKI_URL' | ''} InfoType */ +/** @typedef {`${'WOLF' | 'BLAZE' | 'EMAN'}_${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`} Slayer */ +/** @typedef {'0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'a'|'b'|'c'|'d'|'e'|'f'} code */ +/** + * @typedef RawNeuItem + * @property {McItemId} itemid + * @property {string} displayname + * @property {MojangJson} nbttag + * @property {number} damage + * @property {string[]} lore + * @property {SbRecipe} [recipe] + * @property {SbItemId} internalname + * @property {InfoType} infoType + * @property {string[]} [info] + * @property {string} crafttext + * @property {boolean} [vanilla] + * @property {boolean} [useneucraft] + * @property {Slayer} [slayer_req] + * @property {string} [clickcommand] + * @property {number} [x] + * @property {number} [y] + * @property {number} [z] + * @property {string} [island] + * @property {{ type: string; cost: any[]; result: SbItemId }[]} [recipes] + * @property {SbItemId} [parent] + * @property {boolean} [noseal] + */ + +const FormattingCodes = { + Black: '§0', + DarkBlue: '§1', + DarkGreen: '§2', + DarkAqua: '§3', + DarkRed: '§4', + DarkPurple: '§5', + Gold: '§6', + Gray: '§7', + DarkGray: '§8', + Blue: '§9', + Green: '§a', + Aqua: '§b', + Red: '§c', + LightPurple: '§d', + Yellow: '§e', + White: '§f', + + Obfuscated: '§k', + Bold: '§l', + Strikethrough: '§m', + Underline: '§n', + Italic: '§o', + Reset: '§r' +}; + +const formattingInfo = { + [FormattingCodes.Black]: { + foreground: 'rgb(0, 0, 0)', + foregroundDarker: 'rgb(0, 0, 0)', + background: 'rgb(0, 0, 0)', + backgroundDarker: 'rgb(0, 0, 0)', + ansi: '\u001b[0;30m' + }, + [FormattingCodes.DarkBlue]: { + foreground: 'rgb(0, 0, 170)', + foregroundDarker: 'rgb(0, 0, 118)', + background: 'rgb(0, 0, 42)', + backgroundDarker: 'rgb(0, 0, 29)', + ansi: '\u001b[0;34m' + }, + [FormattingCodes.DarkGreen]: { + foreground: 'rgb(0, 170, 0)', + foregroundDarker: 'rgb(0, 118, 0)', + background: 'rgb(0, 42, 0)', + backgroundDarker: 'rgb(0, 29, 0)', + ansi: '\u001b[0;32m' + }, + [FormattingCodes.DarkAqua]: { + foreground: 'rgb(0, 170, 170)', + foregroundDarker: 'rgb(0, 118, 118)', + background: 'rgb(0, 42, 42)', + backgroundDarker: 'rgb(0, 29, 29)', + ansi: '\u001b[0;36m' + }, + [FormattingCodes.DarkRed]: { + foreground: 'rgb(170, 0, 0)', + foregroundDarker: 'rgb(118, 0, 0)', + background: 'rgb(42, 0, 0)', + backgroundDarker: 'rgb(29, 0, 0)', + ansi: '\u001b[0;31m' + }, + [FormattingCodes.DarkPurple]: { + foreground: 'rgb(170, 0, 170)', + foregroundDarker: 'rgb(118, 0, 118)', + background: 'rgb(42, 0, 42)', + backgroundDarker: 'rgb(29, 0, 29)', + ansi: '\u001b[0;35m' + }, + [FormattingCodes.Gold]: { + foreground: 'rgb(255, 170, 0)', + foregroundDarker: 'rgb(178, 118, 0)', + background: 'rgb(42, 42, 0)', + backgroundDarker: 'rgb(29, 29, 0)', + ansi: '\u001b[0;33m' + }, + [FormattingCodes.Gray]: { + foreground: 'rgb(170, 170, 170)', + foregroundDarker: 'rgb(118, 118, 118)', + background: 'rgb(42, 42, 42)', + backgroundDarker: 'rgb(29, 29, 29)', + ansi: '\u001b[0;37m' + }, + [FormattingCodes.DarkGray]: { + foreground: 'rgb(85, 85, 85)', + foregroundDarker: 'rgb(59, 59, 59)', + background: 'rgb(21, 21, 21)', + backgroundDarker: 'rgb(14, 14, 14)', + ansi: '\u001b[0;90m' + }, + [FormattingCodes.Blue]: { + foreground: 'rgb(85, 85, 255)', + foregroundDarker: 'rgb(59, 59, 178)', + background: 'rgb(21, 21, 63)', + backgroundDarker: 'rgb(14, 14, 44)', + ansi: '\u001b[0;94m' + }, + [FormattingCodes.Green]: { + foreground: 'rgb(85, 255, 85)', + foregroundDarker: 'rgb(59, 178, 59)', + background: 'rgb(21, 63, 21)', + backgroundDarker: 'rgb(14, 44, 14)', + ansi: '\u001b[0;92m' + }, + [FormattingCodes.Aqua]: { + foreground: 'rgb(85, 255, 255)', + foregroundDarker: 'rgb(59, 178, 178)', + background: 'rgb(21, 63, 63)', + backgroundDarker: 'rgb(14, 44, 44)', + ansi: '\u001b[0;96m' + }, + [FormattingCodes.Red]: { + foreground: 'rgb(255, 85, 85)', + foregroundDarker: 'rgb(178, 59, 59)', + background: 'rgb(63, 21, 21)', + backgroundDarker: 'rgb(44, 14, 14)', + ansi: '\u001b[0;91m' + }, + [FormattingCodes.LightPurple]: { + foreground: 'rgb(255, 85, 255)', + foregroundDarker: 'rgb(178, 59, 178)', + background: 'rgb(63, 21, 63)', + backgroundDarker: 'rgb(44, 14, 44)', + ansi: '\u001b[0;95m' + }, + [FormattingCodes.Yellow]: { + foreground: 'rgb(255, 255, 85)', + foregroundDarker: 'rgb(178, 178, 59)', + background: 'rgb(63, 63, 21)', + backgroundDarker: 'rgb(44, 44, 14)', + ansi: '\u001b[0;93m' + }, + [FormattingCodes.White]: { + foreground: 'rgb(255, 255, 255)', + foregroundDarker: 'rgb(178, 178, 178)', + background: 'rgb(63, 63, 63)', + backgroundDarker: 'rgb(44, 44, 44)', + ansi: '\u001b[0;97m' + }, + + [FormattingCodes.Obfuscated]: { ansi: '\u001b[8m' }, + [FormattingCodes.Bold]: { ansi: '\u001b[1m' }, + [FormattingCodes.Strikethrough]: { ansi: '\u001b[9m' }, + [FormattingCodes.Underline]: { ansi: '\u001b[4m' }, + [FormattingCodes.Italic]: { ansi: '\u001b[3m' }, + [FormattingCodes.Reset]: { ansi: '\u001b[0m' } +}; + +/** + * stolen from NEU + * @param {string} displayname + * @returns {code} + */ +function getPrimaryColourCode(displayname) { + let lastColourCode = -99; + let currentColour = 0; + const mostCommon = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (let i = 0; i < displayname.length; i++) { + const c = displayname.charAt(i); + if (c === '\u00A7') { + lastColourCode = i; + } else if (lastColourCode === i - 1) { + const colIndex = '0123456789abcdef'.indexOf(c); + if (colIndex >= 0) { + currentColour = colIndex; + } else { + currentColour = 0; + } + } else if ('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf(c) >= 0) { + if (currentColour > 0) { + mostCommon[currentColour] = mostCommon[currentColour]++; + } + } + } + let mostCommonCount = 0; + for (let index = 0; index < mostCommon.length; index++) { + if (mostCommon[index] > mostCommonCount) { + mostCommonCount = mostCommon[index]; + currentColour = index; + } + } + + /** @type {code} */ + // @ts-ignore + const code = '0123456789abcdef'.charAt(currentColour); + return code; +} + +/** + * @param {number} decimal + */ +function decimalToHex(decimal) { + return decimal.toString(16).padStart(6, '0'); +} + +/** + * @param {RawNeuItem} item + * @returns {Buffer} + */ +function tooltip(item) { + const background = '#100010'; + + const width = 1920; + const height = 1080; + const scale = 10; + + const itemRender = createCanvas(width, height), + ctx = itemRender.getContext('2d'); + + // ctx.fillStyle = '#000'; + // ctx.fillRect(0, 0, width, height); + + // ctx.globalAlpha = 0.94; + ctx.fillStyle = background; + + // top outside + ctx.fillRect(scale, 0, width - 2 * scale, scale); + + // bottom outside + ctx.fillRect(scale, height - scale, width - 2 * scale, scale); + + // left outside + ctx.fillRect(0, scale, scale, height - 2 * scale); + + // right outside + ctx.fillRect(width - scale, scale, scale, height - 2 * scale); + + // middle + ctx.fillRect(2 * scale, 2 * scale, width - 4 * scale, height - 4 * scale); + + // ctx.globalAlpha = 0.78; + + const borderColorStart = parseInt(new tinycolor(getPrimaryColour(item.displayname)).toHex(), 16); + const borderColorEnd = ((borderColorStart & 0xfefefe) >> 1) | (borderColorStart & 0xff000000); + + const borderColorStartStr = `#${decimalToHex(borderColorStart)}`; + const borderColorEndStr = `#${decimalToHex(borderColorEnd)}`; + + console.log(borderColorStartStr, borderColorEndStr); + + ctx.fillStyle = borderColorStartStr; + + // top highlight + ctx.fillRect(scale, scale, width - 2 * scale, scale); + + // left highlight + ctx.fillRect(scale, 2 * scale, scale, height - 3 * scale); + + // bottom highlight + { + const x = 2 * scale, + y = height - 2 * scale, + w = width - 3 * scale, + h = scale; + const gradient = ctx.createLinearGradient(x, y, x + w, y + h); + gradient.addColorStop(0, borderColorStartStr); + gradient.addColorStop(1, borderColorEndStr); + ctx.fillStyle = gradient; + + ctx.fillRect(x, y, w, h); + } + + // right highlight + { + const x = width - 2 * scale, + y = 2 * scale, + w = scale, + h = height - 4 * scale; + const gradient = ctx.createLinearGradient(x, y, x + w, y + h); + gradient.addColorStop(0, borderColorStartStr); + gradient.addColorStop(1, borderColorEndStr); + ctx.fillStyle = gradient; + + ctx.fillRect(x, y, w, h); + } + + ctx.font = `50px ComplianceSans`; + ctx.fillText(stripCodes(item.displayname), scale * 4, scale * 7); + + for (let i = 0; i < item.lore.length; i++) { + const line = item.lore[i]; + + ctx.fillStyle = `#${decimalToHex(parseInt(new tinycolor(getPrimaryColour(line)).toHex(), 16))}`; + ctx.fillText(stripCodes(line), scale * 4, scale * (7 + (i + 1) * 5)); + } + + return itemRender.toBuffer('image/png'); +} + +/** + * @param {string} displayname + */ +function getPrimaryColour(displayname) { + const code = getPrimaryColourCode(displayname); + return formattingInfo[`§${code}`].foregroundDarker; +} + +/** + * @param {string} str + * @returns {string} + */ +function stripCodes(str) { + for (const format in formattingInfo) { + // @ts-ignore + str = str.replaceAll(new RegExp(format, 'ig'), ''); + } + return str; +} + +const repo = path.join(__dirname, 'neu-item-repo-dangerous'); +const itemPath = path.join(repo, 'items'); +const items = await fs.readdir(itemPath); + +const randomItem = items[Math.floor(Math.random() * items.length)]; +/** @type {RawNeuItem} */ +const item = (await import(path.join(itemPath, randomItem), { assert: { type: 'json' } })).default; + +console.log(randomItem); +fs.writeFile('./test.png', tooltip(item)); diff --git a/misc/test.png b/misc/test.png new file mode 100644 index 0000000..942a85e Binary files /dev/null and b/misc/test.png differ diff --git a/misc/tooltips.nnb b/misc/tooltips.nnb new file mode 100644 index 0000000..6e36999 --- /dev/null +++ b/misc/tooltips.nnb @@ -0,0 +1,118 @@ +{ + "cells": [ + { + "language": "markdown", + "source": [ + "# Thingy" + ], + "outputs": [] + }, + { + "language": "typescript", + "source": [ + "function drawGradientRect(\n\tzLevel: number,\n\tleft: number,\n\ttop: number,\n\tright: number,\n\tbottom: number,\n\tstartColor: number,\n\tendColor: number\n) {\n\tconst startAlpha = ((startColor >> 24) & 255) / 255.0;\n\tconst startRed = ((startColor >> 16) & 255) / 255.0;\n\tconst startGreen = ((startColor >> 8) & 255) / 255.0;\n\tconst startBlue = (startColor & 255) / 255.0;\n\tconst endAlpha = ((endColor >> 24) & 255) / 255.0;\n\tconst endRed = ((endColor >> 16) & 255) / 255.0;\n\tconst endGreen = ((endColor >> 8) & 255) / 255.0;\n\tconst endBlue = (endColor & 255) / 255.0;\n\n\tconsole.dir({ startAlpha, startRed, startGreen, startBlue, endAlpha, endRed, endGreen, endBlue });\n\tconsole.dir({\n\t\tstartAlpha: color(startAlpha),\n\t\tstartRed: color(startRed),\n\t\tstartGreen: color(startGreen),\n\t\tstartBlue: color(startBlue),\n\t\tendAlpha: color(endAlpha),\n\t\tendRed: color(endRed),\n\t\tendGreen: color(endGreen),\n\t\tendBlue: color(endBlue)\n\t});\n}\n\nfunction color(num: number) {\n\treturn Math.floor(num * 255);\n}\n\nconst zLevel = 300;\nconst backgroundColor = 0xF0100010;\ndrawGradientRect(\n zLevel,\n 0,\n 0,\n 0,\n 0,\n backgroundColor,\n backgroundColor\n);" + ], + "outputs": [ + { + "items": [ + { + "mime": "application/vnd.code.notebook.stdout", + "value": [ + "{", + " startAlpha: 0.9411764705882353,", + " startRed: 0.06274509803921569,", + " startGreen: 0,", + " startBlue: 0.06274509803921569,", + " endAlpha: 0.9411764705882353,", + " endRed: 0.06274509803921569,", + " endGreen: 0,", + " endBlue: 0.06274509803921569", + "}", + "{", + " startAlpha: 240,", + " startRed: 16,", + " startGreen: 0,", + " startBlue: 16,", + " endAlpha: 240,", + " endRed: 16,", + " endGreen: 0,", + " endBlue: 16", + "}", + "" + ] + } + ] + } + ] + }, + { + "language": "markdown", + "source": [ + "# Thingy 2" + ], + "outputs": [] + }, + { + "language": "typescript", + "source": [ + "enum FormattingCodes {\n\tBlack = '§0',\n\tDarkBlue = '§1',\n\tDarkGreen = '§2',\n\tDarkAqua = '§3',\n\tDarkRed = '§4',\n\tDarkPurple = '§5',\n\tGold = '§6',\n\tGray = '§7',\n\tDarkGray = '§8',\n\tBlue = '§9',\n\tGreen = '§a',\n\tAqua = '§b',\n\tRed = '§c',\n\tLightPurple = '§d',\n\tYellow = '§e',\n\tWhite = '§f',\n\n\tObfuscated = '§k',\n\tBold = '§l',\n\tStrikethrough = '§m',\n\tUnderline = '§n',\n\tItalic = '§o',\n\tReset = '§r'\n}\n\nconst formattingInfo = {\n\t[FormattingCodes.Black]: {\n\t\tforeground: 'rgb(0, 0, 0)',\n\t\tforegroundDarker: 'rgb(0, 0, 0)',\n\t\tbackground: 'rgb(0, 0, 0)',\n\t\tbackgroundDarker: 'rgb(0, 0, 0)',\n\t\tansi: '\\u001b[0;30m'\n\t},\n\t[FormattingCodes.DarkBlue]: {\n\t\tforeground: 'rgb(0, 0, 170)',\n\t\tforegroundDarker: 'rgb(0, 0, 118)',\n\t\tbackground: 'rgb(0, 0, 42)',\n\t\tbackgroundDarker: 'rgb(0, 0, 29)',\n\t\tansi: '\\u001b[0;34m'\n\t},\n\t[FormattingCodes.DarkGreen]: {\n\t\tforeground: 'rgb(0, 170, 0)',\n\t\tforegroundDarker: 'rgb(0, 118, 0)',\n\t\tbackground: 'rgb(0, 42, 0)',\n\t\tbackgroundDarker: 'rgb(0, 29, 0)',\n\t\tansi: '\\u001b[0;32m'\n\t},\n\t[FormattingCodes.DarkAqua]: {\n\t\tforeground: 'rgb(0, 170, 170)',\n\t\tforegroundDarker: 'rgb(0, 118, 118)',\n\t\tbackground: 'rgb(0, 42, 42)',\n\t\tbackgroundDarker: 'rgb(0, 29, 29)',\n\t\tansi: '\\u001b[0;36m'\n\t},\n\t[FormattingCodes.DarkRed]: {\n\t\tforeground: 'rgb(170, 0, 0)',\n\t\tforegroundDarker: 'rgb(118, 0, 0)',\n\t\tbackground: 'rgb(42, 0, 0)',\n\t\tbackgroundDarker: 'rgb(29, 0, 0)',\n\t\tansi: '\\u001b[0;31m'\n\t},\n\t[FormattingCodes.DarkPurple]: {\n\t\tforeground: 'rgb(170, 0, 170)',\n\t\tforegroundDarker: 'rgb(118, 0, 118)',\n\t\tbackground: 'rgb(42, 0, 42)',\n\t\tbackgroundDarker: 'rgb(29, 0, 29)',\n\t\tansi: '\\u001b[0;35m'\n\t},\n\t[FormattingCodes.Gold]: {\n\t\tforeground: 'rgb(255, 170, 0)',\n\t\tforegroundDarker: 'rgb(178, 118, 0)',\n\t\tbackground: 'rgb(42, 42, 0)',\n\t\tbackgroundDarker: 'rgb(29, 29, 0)',\n\t\tansi: '\\u001b[0;33m'\n\t},\n\t[FormattingCodes.Gray]: {\n\t\tforeground: 'rgb(170, 170, 170)',\n\t\tforegroundDarker: 'rgb(118, 118, 118)',\n\t\tbackground: 'rgb(42, 42, 42)',\n\t\tbackgroundDarker: 'rgb(29, 29, 29)',\n\t\tansi: '\\u001b[0;37m'\n\t},\n\t[FormattingCodes.DarkGray]: {\n\t\tforeground: 'rgb(85, 85, 85)',\n\t\tforegroundDarker: 'rgb(59, 59, 59)',\n\t\tbackground: 'rgb(21, 21, 21)',\n\t\tbackgroundDarker: 'rgb(14, 14, 14)',\n\t\tansi: '\\u001b[0;90m'\n\t},\n\t[FormattingCodes.Blue]: {\n\t\tforeground: 'rgb(85, 85, 255)',\n\t\tforegroundDarker: 'rgb(59, 59, 178)',\n\t\tbackground: 'rgb(21, 21, 63)',\n\t\tbackgroundDarker: 'rgb(14, 14, 44)',\n\t\tansi: '\\u001b[0;94m'\n\t},\n\t[FormattingCodes.Green]: {\n\t\tforeground: 'rgb(85, 255, 85)',\n\t\tforegroundDarker: 'rgb(59, 178, 59)',\n\t\tbackground: 'rgb(21, 63, 21)',\n\t\tbackgroundDarker: 'rgb(14, 44, 14)',\n\t\tansi: '\\u001b[0;92m'\n\t},\n\t[FormattingCodes.Aqua]: {\n\t\tforeground: 'rgb(85, 255, 255)',\n\t\tforegroundDarker: 'rgb(59, 178, 178)',\n\t\tbackground: 'rgb(21, 63, 63)',\n\t\tbackgroundDarker: 'rgb(14, 44, 44)',\n\t\tansi: '\\u001b[0;96m'\n\t},\n\t[FormattingCodes.Red]: {\n\t\tforeground: 'rgb(255, 85, 85)',\n\t\tforegroundDarker: 'rgb(178, 59, 59)',\n\t\tbackground: 'rgb(63, 21, 21)',\n\t\tbackgroundDarker: 'rgb(44, 14, 14)',\n\t\tansi: '\\u001b[0;91m'\n\t},\n\t[FormattingCodes.LightPurple]: {\n\t\tforeground: 'rgb(255, 85, 255)',\n\t\tforegroundDarker: 'rgb(178, 59, 178)',\n\t\tbackground: 'rgb(63, 21, 63)',\n\t\tbackgroundDarker: 'rgb(44, 14, 44)',\n\t\tansi: '\\u001b[0;95m'\n\t},\n\t[FormattingCodes.Yellow]: {\n\t\tforeground: 'rgb(255, 255, 85)',\n\t\tforegroundDarker: 'rgb(178, 178, 59)',\n\t\tbackground: 'rgb(63, 63, 21)',\n\t\tbackgroundDarker: 'rgb(44, 44, 14)',\n\t\tansi: '\\u001b[0;93m'\n\t},\n\t[FormattingCodes.White]: {\n\t\tforeground: 'rgb(255, 255, 255)',\n\t\tforegroundDarker: 'rgb(178, 178, 178)',\n\t\tbackground: 'rgb(63, 63, 63)',\n\t\tbackgroundDarker: 'rgb(44, 44, 44)',\n\t\tansi: '\\u001b[0;97m'\n\t},\n\n\t[FormattingCodes.Obfuscated]: { ansi: '\\u001b[8m' },\n\t[FormattingCodes.Bold]: { ansi: '\\u001b[1m' },\n\t[FormattingCodes.Strikethrough]: { ansi: '\\u001b[9m' },\n\t[FormattingCodes.Underline]: { ansi: '\\u001b[4m' },\n\t[FormattingCodes.Italic]: { ansi: '\\u001b[3m' },\n\t[FormattingCodes.Reset]: { ansi: '\\u001b[0m' }\n} as const;" + ], + "outputs": [] + }, + { + "language": "markdown", + "source": [ + "# Thingy 3" + ], + "outputs": [] + }, + { + "language": "typescript", + "source": [ + "type code = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f';\n\n// stolen from NEU\nfunction getPrimaryColourCode(displayname: string): code {\n\tlet lastColourCode = -99;\n\tlet currentColour = 0;\n\tconst mostCommon = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];\n\tfor (let i = 0; i < displayname.length; i++) {\n\t\tconst c = displayname.charAt(i);\n\t\tif (c === '\\u00A7') {\n\t\t\tlastColourCode = i;\n\t\t} else if (lastColourCode === i - 1) {\n\t\t\tconst colIndex = '0123456789abcdef'.indexOf(c);\n\t\t\tif (colIndex >= 0) {\n\t\t\t\tcurrentColour = colIndex;\n\t\t\t} else {\n\t\t\t\tcurrentColour = 0;\n\t\t\t}\n\t\t} else if ('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf(c) >= 0) {\n\t\t\tif (currentColour > 0) {\n\t\t\t\tmostCommon[currentColour] = mostCommon[currentColour]++;\n\t\t\t}\n\t\t}\n\t}\n\tlet mostCommonCount = 0;\n\tfor (let index = 0; index < mostCommon.length; index++) {\n\t\tif (mostCommon[index] > mostCommonCount) {\n\t\t\tmostCommonCount = mostCommon[index];\n\t\t\tcurrentColour = index;\n\t\t}\n\t}\n\n\treturn '0123456789abcdef'.charAt(currentColour);\n}\n\nfunction getPrimaryColour(displayname: string) {\n\tconst code = getPrimaryColourCode(displayname);\n\treturn formattingInfo[`§${code}`].foregroundDarker;\n}\n\nfunction stripCodes(str: string) {\n\tfor (const format in formattingInfo) {\n\t\tstr = str.replaceAll(format, '');\n\t}\n\treturn str;\n}\n" + ], + "outputs": [] + }, + { + "language": "markdown", + "source": [ + "# Thingy 4" + ], + "outputs": [] + }, + { + "language": "typescript", + "source": [ + "import tinycolor from 'tinycolor2';\nimport canvas from 'canvas';\nimport path from 'path';\n\ntype McItemId = Lowercase;\ntype SbItemId = Uppercase;\ntype MojangJson = string;\ntype SbRecipeItem = `${SbItemId}:${number}` | '';\ntype SbRecipe = {\n\t[Location in `${'A' | 'B' | 'C'}${1 | 2 | 3}`]: SbRecipeItem;\n};\ntype InfoType = 'WIKI_URL' | '';\ntype Slayer = `${'WOLF' | 'BLAZE' | 'EMAN'}_${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`;\ninterface RawNeuItem {\n\titemid: McItemId;\n\tdisplayname: string;\n\tnbttag: MojangJson;\n\tdamage: number;\n\tlore: string[];\n\trecipe?: SbRecipe;\n\tinternalname: SbItemId;\n\tmodver: string;\n\tinfoType: InfoType;\n\tinfo?: string[];\n\tcrafttext: string;\n\tvanilla?: boolean;\n\tuseneucraft?: boolean;\n\tslayer_req?: Slayer;\n\tclickcommand?: string;\n\tx?: number;\n\ty?: number;\n\tz?: number;\n\tisland?: string;\n\trecipes?: { type: string; cost: any[]; result: SbItemId }[];\n\t/** @deprecated */\n\tparent?: SbItemId;\n\tnoseal?: boolean;\n}\n\nfunction tooltip(item: RawNeuItem) {\n\tconst background = '#100010';\n\n\tconst width = 1000;\n\tconst height = 250;\n\tconst scale = 10;\n\n\tcanvas.registerFont(path.join(__dirname, 'assets', 'Faithful.ttf'), { family: 'Compliance Sans' });\n\tcanvas.registerFont(path.join(__dirname, 'assets', 'Roboto-Regular.ttf'), { family: 'Roboto' });\n\n\tconst itemRender = canvas.createCanvas(width, height),\n\t\tctx = itemRender.getContext('2d');\n\n\tctx.globalAlpha = 0.94;\n\tctx.fillStyle = background;\n\n\t// top outside\n\tctx.fillRect(scale, 0, width - 2 * scale, scale);\n\n\t// bottom outside\n\tctx.fillRect(scale, height - scale, width - 2 * scale, scale);\n\n\t// left outside\n\tctx.fillRect(0, scale, scale, height - 2 * scale);\n\n\t// right outside\n\tctx.fillRect(width - scale, scale, scale, height - 2 * scale);\n\n\t// middle\n\tctx.fillRect(2 * scale, 2 * scale, width - 4 * scale, height - 4 * scale);\n\n\tctx.globalAlpha = 0.78;\n\n\tconst borderColorStart = parseInt(new tinycolor(getPrimaryColour(item.displayname)).toHex(), 16);\n\tconst borderColorEnd = ((borderColorStart & 0xfefefe) >> 1) | (borderColorStart & 0xff000000);\n\n\tconst borderColorStartStr = `#${borderColorStart.toString(16)}`;\n\tconst borderColorEndStr = `#${borderColorEnd.toString(16)}`;\n\n\tctx.fillStyle = borderColorStartStr;\n\n\t// top highlight\n\tctx.fillRect(scale, scale, width - 2 * scale, scale);\n\n\t// left highlight\n\tctx.fillRect(scale, 2 * scale, scale, height - 3 * scale);\n\n\t// bottom highlight\n\t{\n\t\tconst x = 2 * scale,\n\t\t\ty = height - 2 * scale,\n\t\t\tw = width - 3 * scale,\n\t\t\th = scale;\n\t\tconst gradient = ctx.createLinearGradient(x, y, x + w, y + h);\n\t\tgradient.addColorStop(0, borderColorStartStr);\n\t\tgradient.addColorStop(1, borderColorEndStr);\n\t\tctx.fillStyle = gradient;\n\n\t\tctx.fillRect(x, y, w, h);\n\t}\n\n\t// right highlight\n\t{\n\t\tconst x = width - 2 * scale,\n\t\t\ty = 2 * scale,\n\t\t\tw = scale,\n\t\t\th = height - 4 * scale;\n\t\tconst gradient = ctx.createLinearGradient(x, y, x + w, y + h);\n\t\tgradient.addColorStop(0, borderColorStartStr);\n\t\tgradient.addColorStop(1, borderColorEndStr);\n\t\tctx.fillStyle = gradient;\n\n\t\tctx.fillRect(x, y, w, h);\n\t}\n\n\tctx.font = `48px Roboto`;\n\tctx.fillText(stripCodes(item.displayname), scale * 4, scale * 7);\n\n\tconst buf = itemRender.toBuffer();\n\treturn buf;\n}\n" + ], + "outputs": [] + }, + { + "language": "typescript", + "source": [ + "import fs from 'fs/promises';\nimport path from 'path';\n\nconst repo = path.join(__dirname, 'neu-item-repo-dangerous');\nconst itemPath = path.join(repo, 'items');\nconst items = await fs.readdir(itemPath);\n\nconst randomItem = items[Math.floor(Math.random() * items.length)];\nconst item = (await import(path.join(itemPath, randomItem), { assert: { type: 'json' } })).default as RawNeuItem;\n\nconsole.log(stripCodes(item.displayname));\ntooltip(item);\n" + ], + "outputs": [ + { + "items": [ + { + "mime": "application/vnd.code.notebook.stdout", + "value": [ + "Enderman Minion IV", + "" + ] + } + ] + }, + { + "items": [ + { + "mime": "image/png", + "value": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAD6CAYAAAAyVW3pAAAABmJLR0QA/wD/AP+gvaeTAAAgAElEQVR4nO3daZAkaX3f8efJoyqzju7qY3q659je7tnZnZ1ddlmWJWSWsBEskgOQhfElwMIYAmNH6JXDjvBLXtt+Y4etCFsOh7yBQbIt2QohWSCMEIhjEQgWWPaaa+fqnpk+q6orq7Iy8/GLmu6uI6urKiu7O6vm+4noiN2eOvKorH5++fyf55GiTwVR2Or3sQAAAAAAoGFLbBX6eZx22BsCAAAAAAB6I6ADAAAAAJAABHQAAAAAABKAgA4AAAAAQAIQ0AEAAAAASAACOgAAAAAACUBABwAAAAAgAQjoAAAAAAAkAAEdAAAAAIAEMAqisNXPA594/sVXD3tjAAAAAAAYN698+5N95W560AEAAAAASAACOgAAAAAACUBABwAAAAAgAQjoAAAAAAAkAAEdAAAAAIAEIKADAAAAAJAABHQAAAAAABKAgA4AAAAAQAIQ0AEAAAAASAAj7hd85duffDzu1wQAAAAAIGmeeP7FV+N8vdgDOgAAAAAADwYV66tR4g4AAAAAQAIQ0AEAAAAASABK3AEAAAAAiKS1xF0NWfFOQAcAAAAAIIJhA3k7StwBAAAAAEgAetABAAAAAIgkiPXV6EEHAAAAACABCOgAAAAAACQAJe4AAAAAAEQS7yxx9KADAAAAAJAABHQAAAAAABKAEncAAAAAACJpLXEPhpzUnYAOAAORIjv5mG6mZ7S6u6Eq2294SvnHvVExGvf9AwAAiM+wgbwdAX2MnD7/WTudOa03/+7WG/+pUnNWYv7YAA8mqaXkwvKvW1b27N515lZXg5XLX3B8rxzvDCHHYNz3DwAAIOkYgw4AfZpeeH+qObwKIUTKmtdmTv/N9HFtU5zGff8AAADiF/T505+R6kG380v6wvI/suN+3Vrlln/rzd9y4n5dANEtP/35XLd/u33pv1SqOzdDv+l0Iy8Xn/jnWSFk6HNvvPrvduruZqTe4Ez+nD7I70fNuO/fKNN0Sz785L/KxvmaYRVWZx79p3bKnm853wddb73oRkYuPvEvW67HwHfUW6/8mx2lKO4CAKAdPegARo6VO9f15qKdP6d3C+fDCgJvoN+PmnHfP/RWKb3ZMeHAQddbL2HXY6V02SecAwAQjoAOYOTYueWuPbqZfPd/G9bO9quhSbXb70fNuO8fetvZ7gzoB11vvYQ91yld5vMEABgjqs+f/hDQAYwcO3tGk5oZ2k0+TJjoZfvut9zy5k/qzb+rFN/wNlb+1D2s9zxK475/6M11bvqB77S0Ig663nqxc+3DI5SoFC+xLAAAAF2M1Bj0MOWtn3tO+epQf+yZnRgYMVKXdm5JqxTfaLn2zfQJTTdzh3bjUalA3L3++7XNO9+sm9as5tW2Are6Oja1uuO+f6NMBXV17+Yf1br9e9qe0yZmnjObf1fe/Gnd2bne9fzV3a2Ov31KBaJSuurnChf32wddrrdejNSMZqQmWq5H17nj+16Jv7kAAHQx8gG9Vrnpl9b/st77kQBGWeBXlaalhJCaFKLRM9ceGDITrb11vldRupGJfUB6vbYW1GtrYxtcx33/RpFSvjjob10weUFvD+jOzltBaf0HA/99dEqXvJaALsKvt14a12NrFg8b4w4AwGhr/VunhrwNTYk7gJGwvfb9enO1jJ1/uOMGY3M5rVu9F2ysfK1rjyOAcJXim35jOZj9cXN2fnHgG/phw012tgcL+QAAJJ1SrT/DIqADGBmV0v7Y1ZQ1p+lGfq93XEpNWNmH9gKBU7rCRFR4wESfkKaZ75WU69xtCdLt11svUmrCzi22BPTAryrXuUVABwDgACNf4n6Ywtad3Vj9M3frzp+7jX9Py4mZ5ww7f95IWTOarmeEUr7wPUdVKzeDSvF1b2frp17U5WQ0PS2zk0/qmYlHjZQ1q+lGTkrNlIFfUW51LagU3/CK6z/wVODGOp7PMCdlZvKCkcmf0830jKYbGSm1tAx8R/leRbnOil8pXfZ3tl7xlOovA6XtBe30o5/LHPSYzTvfdDdXv743GdXu8c1MPGqY6VlN0y2pgrryvZKq7tzwi+s/9GqVm6GNvfz0O4wTZ/+W1fy7t1751zuBX1X5mXeaE9PPGkaqoEmpiWrlRrC5+vXa7jq/mp6WU/MvpDL5Jd1MTcsgqImasxJs33vJrRRf79m4zEw8ps8vfcxu/70KPBUEVRH4NeVW1wK3uhI45at+tXzdj9KQPu7P53GoFN/0Z0798v3/k8LOn9PLmz/2hBAinT2ja3p6L0A45cu+buQGLm8/e+E3MmZ6dqCbl2HrSfdyXOfvqPavm5Q1p2UmHzfs3LJupqakbthSCCkC31FefVs55Wv+zvZrfrdru5sH8Xo4TJXSJT9ln2wK2FLY+WW9vPlyX1/6KfvU/etx/7utUrrC8moAgDEU79QqBPQB7Y5nTVnz2vzyJ2zDbO1RkFITRsqUudRFLVe4aLhz7wnuvvU/q2717kCtkvz0M8b0wvvTYQFDN3LSzuV0O/ewXjjxbnP12u/WapUbQ/dK6EZGFk7+Ympy5lljd5xv679npW5kZco6oeWmnjL9hQ8EG3e+UY8yxjGMmZrae8+0fUqbX/qYpZv5liAh9bTU9LQ007NafvoZs7j+g/r6rT+u9dPo040JOTX//tTEzLMt4zTt3LJuLT9k3770X52asxLML33Cau6J1XRb2Lll3c4t2fdufLla2vjhgQ3U9m3e23bNkLqWE7qRk2Z6RstOPiamTr5X1Gsbwda9794/jsNd4Ef1+Twu9dpa4LnFYHfiqUx+eS+gN5e3q8BT1fJVP1t420h9x43z+TPMgpw+9UI6V3jCCFunXtfyUjfzIp05oxfm3iOc0lVv/fZX3GEmqQs/nvvXmJSGMFJ5mUs9ruUKjxvu3PPB3bf+10gcz8NWKb3pF+beLYTYL9ezc0t9B3Q7v6y3l/mxvBoAAL1R4j4g3chKKQ0xv/RrVnvjOUzKOqGdeuTTdsqa6/tYT82/L3Xi7K9a/fT+6WZOW1j+uGWYE0NNhGWmZ7XT5/+JPTn7nBkWzru994kzH06fOPuraSmHX9nKSBU0IYTQdFueXPqY3S3oNpuYeadZOPneVD+vb1oz2sT0M6GBTWqGnJp/XypbuGg0h/O2R4mZUy+kpTw48w16Lsz0tHbizIfSC+c+ZWu6PdR5PIrP53FrLnNvHuPa/N/Vnet+EIze3JHjev7S9mnt9KOfzeQKT4aG8zB2fsk4ff4zdnbywgBfLq3l3bqRuX88/75lmDnZ6wZY43j+48Qfz+6UUGr/Zxi1nRu+71VbXmaQJQw7H6vuj20HAAAHGdFGyPHRjYzMFh43dsNkPzTdknOLf8eSsvdT8jPPmlMn/3pfgXP/9W05Mfsus/cjwxlmQZ565NP2IPvULD/9jDl96pfSUd9/bzvuv39h7t1mP+Fk19Tce8x+QvHk7F8zlRDSdVb9WmWlo6w8k1/SJ2beZQa+o6o71/2w5fc03ZZW27jKjv0YYNub2blFfWH51y2ppSKH9MP+fCaBU76818jXzZxmpk9oUktJK3N6bwcqpcsjGQTG8fyZ6RPaqUc+aTd6s8PGSHf/kZohTy7+PcvOR1vbXjey94/n1Ngcz27imphm//UC4ZSvtVxHu9dbr+dKzbx/Pe6fS7e6GrC8GgBgPAV9/vRnpMo/k0A3stLK7gY0JZzyNb8R5ipK00yRzpzRMxOPGu2Nu5R1UssWnjDKmz/tWuKnGxk5s/BCaDh3nTt+ceNHnltdCZTvKd3MSTu3qOennzE13Za5qadNTRv8dEqpibnFj1phS1H59XKwU3zVrzmrQeDXlG7Y0sqc1bKTF4z2EDk5+y7TKb3pVYqXQoNRzVkJrrz8+XLz72bP/Eq6udzcMHNS00yRnWws76MCT5U2f+JVSm/4gVcOpJaS+alnjNzU21pvRkhNZgtPGtv3vnNgl6mVPaOvXvvdamX7VU8IISZm32XOnv7g/o0FqUvdyMjrr/77SuA7StPT8syj/6zjxoWZPqE5BwTAne1XvGrlduhVqOtpoZs5LW2f0tKZM3r75ySdOaVPz7/PXL/9J27Y83s5zM9nUjilS75Sgdjdh8zEOb3ubqrmyg9niKWcbrz2HyoH/fvp85+105nTw5eMhDiK89ff/p2KZf+k1MXJxY+mw246BX5NVYqvezXnTiCEEo1hHxcM3ci2PlZqcu7sR9I3Xv9NJ/CrAwU83cjI/YoYJZzyW/eP587943m6y/E8MTLXw2GqlC752cnHWpdbyy/r9dq9A1sZdm5Ja6/EYtJGAAD6Q0AfUMo6qaWsk5rnFoM7136nWnM6glg9bZ/S5pf/od0eevNTbz+wwTcx+wtmWInz5p1v3Z88ralt6ghRKb7ub975Vn1+6eOWlT0bqUGdLTwVWtK9dfcv3M3Vb7jtk8AVxV8K3ci5c4sftVpLGKUozP2NVKV4yYmyHbuvYabnNDM9rXluMVi9+oWOsaBO6YovhBK5qadaQrqVfUjvFdBdZ9XfDedCCLGz9arXEtCFEMW177mB7yghGgFip/iaPzn7Cy2t9+aJyMLcv0nRMyAa5oScXvhAqv2Gw+Tsc+b2ve/Wvfr2wL1Nh/n5TIrAr6la5Za/+5m3c8u64W7tHSu/XgpGdQxx/+fvEyHn7+nEnb/c9NvNlD3f8f3ilK/6d9/6/Wp7lcr67a+6J87+SrpRCr9LCd3MaYW5d5sbK1/vceOq8XK7PcmN75M5rV7bDu6+9T+6Hs+TSx/vOJ65wlOJO569tX9lDNdhXS1d8oRQ6eZhCXZuSS+uvXTgd62dW+o4581DUwAAQHejVcMXIp05o+dnnjMH+UlnooXZXSqoq9uX/1tYY08IIUTNuR2s3/5Kx/rLjV63btlOivzUUx03TMqbP6tvrv4/t1tDK/AdtXr1ix0N3X5NhpTGb6x+w91Y+VpHON/le2W1euW/O66z2tLgsrJn9ZQ1P9RnavbMh9NCKXXn2u90naipuP79jsZhyupddunW1lqOURB0LpFdc+61PMb3djqOaxzj7YUQwqsX1d3rv1crbfy4dX+kLjOTFyLfPDucz2eyNJewZ/Ln9PzU00bYvyVfZ1m3Clx1+/Jvj8X5m5x5zmjfP7d6J1i9+qXQ7ywVuOre9f9dbcz2vTuWuhG4c1PPmL2uvbAybxXU1cqVFw88nhsrXx2J43nUvHpRtX8P27nFjsqfdlZuseW8B35V1XYGm5UfAIDR0dSOU91/+jXyPei5wkUjV7g40H5sr71UH2bW8+L6X9U9d/3AHrqdrZ956syHldTMvRaepltSN7IyrGGasua0jnGnKlAbK1/tWeoc+I7avvc9d3rhhYHGgZvpGa29lNV17vjbd7/Z8z2V8sXa7T9xT537VMtyYnZ+WR9m1uV0ZkEvbfyo3q0xLYQQbvVex/ELK9FvF/i1nleGCno/Jm6bq19389NPm629VMs9e6m6OYzPZ9JUy5c8Ie5PDih1qen7H+Oj6ak7vEPUOH8bPc7fK4d4/qKvn93MTJ/QTGuuYybvjZWv1VRQ7/oGSgVi/dZXamce+1ym+ZrQjay08+f0SvGNgc5vaeNHfR3P2dMfbDue6ZG5Hg6TU7ripay5vaFXUjNlyj6td/sbqht52X7D1Clf85UinwMAxlOcc8AIMQY96MehvPWzni0NpXxRdzdCJhmzQoNkOrPQcS6c8jXfqxf7OuU7W68MXIppZRc73nN77aV6v+vUVsvXfL9eanlwOnNm6O7l7XudPeTNGuuI3ws8d2vvx/cqI9uI9upF5bmtx9FMFSJ33R3G5/P4tYbG6s6toFHd0Nb7rHzhdAT09l7qZOv//K137H8c52+3F7r9Z1BW9mzH94tfLweVYu8Kh3rtbtCYyHFvq4QQSvQeG99ZkTCe10OYsIn2hhd2w+ugSfvs/FJH5UHzxI4AAOBgI9+DfvSUcJ2VvhobKvA6G3xa+GTrKetkZ0DfudZ3T3Td3VSNic36X6YrbKKrQZfBqVZuBdnJC3vbnkpPD9Wg9evloJ8e+Juv/8cDJ7oaNUFQEUJM7P1/PxUB4Q7n83k8DgoYSjjlq35jTe19rrPqDzqR2PFqHTPdOH+rfZ2/wPc7qqWkTM75a4Tp1g10ylc7Vk/oxilf8dOZhZbvqLR9esCbyv0fTxX4IcNZknM8j0tt54avAlc1T/Rn55b0rTt/Hvr4xvjz5kOphFNk/XMAwDiLt+lJQB9Q4Luq29jsYYSteT7oRFdevaxSAwT0xrrAzc8vqUGXwWnvuR62x6nm3ApGoZezH7qRkZnJJwwrc0ZLWXOabuY0TUsJTU/LwK+q3R+vXlaGOdk2c7Ue6TgO/vkc3WPtlK547QH9KMrbw8YRRS1t6hwvfTjfL4OJp/e1YzZ2IUSv2b97PTbse/IgKqgPcDzD9nt0r4+4KOWLSumqn5l4dO9aS9sLmtRSUgVuxwGyskstwxrc6r0gyoSXAAA8qEY+oK/f/mqt1+zdcVLqcN5K08OWIRqsbPugcZ3h79ka5g0zL5ef/nxukNdoN8wa3kIIUa8dPFZ0FBipaW164YVUdvJCx/JNuzTdkrs3M1J26EP61B4UI63ONpIqxTf87XvfadnhsFm3B5mU47gFwSDfL8ku2Q+7Wef7/Re+tN78U/df86Dvl87jETYR5HhTLTd94vroV0qXWwK6kJq0cou6U3yz5Xoz0yc03cy2fOlVy1eP+44TAAAjZeQD+rgIXSfYGyxwD9pY17TQJdeHMuxEQKM+IVNu6m3G3NmPpPfXAI66OyN9GGLRK1j7Xlmt3/7TEb8jMb49to3hEm03kEKGVXQT+G7H85sncUOnw7oXVS1d9oQQLZOQ2rnljoDeGH/euhEsrwYAGH/x9i8ySVxSqCBkgqLB5luTcrD7Lb4/xJLlXQzai99usB7EuPSaVKm/yZeykxf0uYf+trUfzkfF6EyglgyHMxlXcoTt3+D7GASd906kZvR9bWh65w3EYb9fehvn8xqdV99WjSEH+8fGzj3c8Qeqff1zFbiqthN9xRQAAB5E9KAnhO9XO34ntcHGc2t6eqDH+57T0QKt7lwfajmcem1zqFtIg/Sw9fFqffx7PG+naaaYOf1Bq3324sB3VGnjx/VK6bLvuUUV+DsqbNz+qUc+bfezlnun6D2wcY6lPhpRNm6cg1ayx0yHLWuo65m+n985UWJjPe2Dn9U66d4gwxuizlafLIf3eXBKVzwzPbt318RMz2iGOSF3VxqRUhdW9mxLQHfKb7G8GgAAAyKgJ0RYwzNlzWqdS0aFk9IQhjkxWECvb3eE6c0733Cd0pUjbFGNfItYCCFEZvKCYZj5luPvOiv+ypUvVHstAdc4975oHj/ab7DomGRsPA7nfWO1MyGSHbCH5dVLqn1/UtZs3zehzPSs1n4dePWDh8CM1+c/WZzyZX9i9l0tv7Nyy3p588eeEEKk7FN6Y6iWannO0W4lAADHYf9vXxxtEUrcEyJsxvZ05mzfNe4pe0EftLTaKV/rWGc4P/2OB3RdoW49rf2V+lrZh/XdHundn7vX/7DW7/rsmp6XxxcuKOkdTDwl4Mk2/L65zm2/fS31dObhjjWyu2kvlxZCiJpz+xAnkRz3czqcavm635i1vbnMfXHvHIWNP3eKV5ggDgAw9prbOnEgoCdErbLS0fDMTpzX+y1bz009aQzawHTKVwOh/JZerlzhomHlHhps8Hti7R+HRk9c5/HYD9ThF1V7wOh24bX3ngvlq37WcxeisbbzMOueRw8VD0oIGefQFf++9fuZ76W6c7NzmTQzq2UmzvX8fklZc1rKnm97nBK1ys0ePbIPcsg+3H1XyhfVnestx9/KLRq7N1zax6TXa2uBV996kE4AAACxIKAfuv4aTK5zy28sq7b/WKmZcmr+fT2nWjdSU9rE9DsGHq6gAleVNn/S1sMhxcnFf2ClrJNH8Nk47AblUY4rbcvnQkpN631KpNTF9KkPpB/sYNHNg3A89m8ghc0JcOAzYwrSh6VeWwtcZ9VvP49T8+9LHzQbu5SamD71S/dnDN9/nu+VVbWl6gdHrVJqLVnXjaxMWSc0qaVk2l5o+puhhFNieTUAwIOiW3VltLYsAb2nowlOSvmivPVKR4NmcvY5szD3vNmtLNRMTcmF5U9Yg8yO3Gzrzjfd/RnkG/unG7Y8ff4zdmHuefOwlzVKTsA46Pz2/gw0JkpqurkipcjPPHvgcAFNt+Xc4t+1rOxZfWfrZ8cxfX2TcQ/C8Wl8VqOF6vDXinHjYhHfd15p4686PtdmelY7+fCvWbqR7/hukVpKzp79iGVlO6t4Shsv1w93wjFukvXiFC977Z99K7ekW7lFXQkpmyuS2sM8AADozwhPEtdoPKUzp/X8zDuHeiXfK6vK9mvHfrd/6+536xPTzxhC6k0NVymmF15IZycvGqXNlz23eidQvqf0VFazc+f0iem3G1IzZXnrZ56u28LOLw90Tuvuplq7/VV35tQvt6xxK6Qhp+bfn5488Z5UpfSmX6vc8j13K/C9ihAqUJphSamlpG7kpJme0VLpE3Lt1pddzx1uFvf49NvAjqcRXt255uen394SyKfm3582UtNaaf0H9XptI1DKE1JLyVT6hMxMPmbkp582dSMnhRCiuPFDL1t4cu/5mm7J049+LuPXi0Hd3VZ+vRhsr33fa4wBPWgfKXFvNVyATrbkn7/y5stefvoZv71c3cqe1c889rnMTvGS79Xu+UoFwkzPaJmJ84am2x3B3a8Xg+La9/q4iZXs4zEMKXWRm36q600/Mz2nNT7v+7+zMmc1IUTX51S2X/MCv3M1j268+paq19YDMz2zd3Pfzj2sG+5ky2uooM7yagAARDSCAb21LZErXDRyhYtD7Uetcts/3IDeX/vHczeCjTvfrE/PvzfVnilS9oI+Yy+Ejt1UqrEEjpV9SI8yq3dx7aW6mZ7WJmae62jIaXpa5gpPGrnCkz2Pcco6WffczY7fm6kpeebCb2TDn9W6gbNnPmTNnvlQx6M2Vv/M3b77F50LKydEZfs1z58vB7qZ22u4SqmJiZl3mBMz7zCFUEIFvuqsdFCiXlsLquUbvlK+kFK7/1wpUtYJTVgnNPv+I8tbP/c9d2N8E0ibziXgDnfXrdzD+vzSJ+zej+x06vxnuqwfpoRbvRfcfvO3Kt3+/eD/j4+VWxxi/z7ddX20g/ZPqUDcu/EHtVOPfMpuzPC9T2qmzBUeN4R4vO27pf1LLFD3bn65FrZs24NEaoacXmi7kdqm/RLJFi6a2cLFro93Kyu+O0BAF0IIp3zVbw7oVu5hw1KLLa/RWK7z2O95AwBwROJtolDi3lP0ssf2Ccj6KWfduvMtt7jeWRZ6ELd6Nyhv/tTrtQTRQdZv/d/a2q0/rqmgrsL3ufePmZ4a8c9Tt8PX+/wHQV3cu/l/akJ5KvyxUoQNQ1CBq9ZufrnaCPD1Yw4gD3T+EVE/93GMNTp+h7ef9dp6sHrli06/Kxq0bFVQV3ev/171aMaej/L5OzrV8hWv4xhJKZt/55SPcqlOAADGyxgEqsF+2sfP9RpDevQTMSmxdvOPauu3v1LrXXqohFtdDe5c/ZKjlCd8b3fd4WgNzNL6D+q33vzPTmnjx3UVeAPvqZFKWkDvp8EdX2PcKV31V65+qerXi32V+bvVe8HKlS9Wd2e7jnLMh7uBJNqug8HffTSMa+iKcu6P50ZCzVkJVi79dqW89XOv3/erlq95K5dfdCrFSwOEvc7v+UEkZ06MgxzvTaTGcmsH30x0GH8OAEBkI1XiPuyETMdn0G1Worj2Ur28+RMvW7hoZPLndTM9oxlmTkppCN+vCNe55+9s/9wrb77s7U6c1JiobDj12nqwdvMPaxsrX3MzE4/o6cwZPW0vaLqRkZpuS023WnqBVeAq39tRbnU9cJ3bBzTKRvG8Da5avubfeO03K9nCRSMzcd5IWSc13chIqZky8B0V+I6qVW77ldIlv7L9uqfUfpYPgqrQVNdK4lBRhjSMjnHYmV4haBz2sT9efVut3fiD6vbd72iZyfOGnVvUDbMgNT0jhZAi8B3l1YuqunPdd4qXvJpza+D5LMbr8x/m+HdQKU9Ud276dn4ptP1Qr20EnruVkLlIAAA4Ko0/fUEMfwFlQRS2+nngE8+/+Go/j3vl2598fLhNAgAAAAAg+c49+2/7ysmXf/gv+srJCStJBgAAAADgwTRSJe4AAAAAACRPPEPRCOgAAAAAAEQS7xwxlLgDAAAAAJAA9KADAAAAADAUStwBAAAAADhGlLgDAAAAADB26EEHAAAAAGBISgVDvwYBHQAAAACACOII5c0ocQcAAAAAIAHoQQcAAAAAIBbDTRpHQAcAAAAAIBJmcQcAAAAAYOzQgw4AAAAAQCwocQcAAAAA4BhQ4g4AAAAAwNihBx0AAAAAgCGpvc706L3qBHQAAAAAACJQihJ3AAAAAADGDj3oAAAAAADEKoj0LAI6AAAAAACRRAvi3VDiDgAAAABAAtCDDgAAAABArKJNHkdABwAAAAAgEmZxBwAAAABg7NCDDgAAAADAkIKO+eIG710noAMAAAAAEEEQUOIOAAAAAMDYoQcdAAAAAIBDMVgPOwEdAAAAAIBIKHEHAAAAAGDs0IMOAAAAAMCh6Jja/UAEdAAAAAAAIhksgPdCiTsAAAAAAAlADzoAAAAAAENSXeeL638iOQI6AAAAAAARqO6pPBJK3AEAAAAASAACOgAAAAAACUCJOwAAAAAAkVDiDgAAAADA2CGgAwAAAACQAJS4AwAAAAAQSRDrq9GDDgAAAABAAtCDDgAAAADAkOJYEp2ADgAAAABABCqOVN6EEncAAAAAABKAgA4AAAAAQAJQ4g4AAAAAQCSUuAMAAAAAMHYI6AAAAAAAJAAl7gAAAAAAREKJOwAAAAAAY4cedAAAAAAAhhTHkugEdAAAAPcAmoQAAAXTSURBVAAAIlAqiPX1KHEHAAAAACABCOgAAAAAACQAJe4AAAAAAETCLO4AAAAAAIwdAjoAAAAAAAlAiTsAAAAAAJFQ4g4AAAAAwNihBx0AAAAAgCEEMS2HTkAHAAAAACCCIK5kfh8l7gAAAAAAJAABHQAAAACABKDEHQAAAACASJjFHQAAAACAsUNABwAAAAAgAShxBwAAAAAgEkrcAQAAAAAYO/SgAwAAAAAwBBVTRzoBHQAAAACACFRcyfw+StwBAAAAAEgAAjoAAAAAAAlAiTsAAAAAAJEEsb4aPegAAAAAACQAAR0AAAAAgASgxB0AAAAAgEiYxR0AAAAAgLFDDzoAAAAAAEOIazl0AjoAAAAAABGouJL5fZS4AwAAAACQAAR0AAAAAAASgBJ3AAAAAAAiCWJ9NXrQAQAAAABIAAI6AAAAAAAJQIk7AAAAAACRMIs7AAAAAABjhx50AAAAAACGEMQ0VxwBHQAAAACACIKAEncAAAAAAMYOAR0AAAAAgASgxB0AAAAAgMjiK3OnBx0AAAAAgEgYgw4AAAAAwNihxB0AAAAAgMhiWmNN0IMOAAAAAEBE8YVzIehBBwAAAABgKCqmoegEdAAAAAAAItgP5vEkdAI6AAAAAACRMIs7AAAAAABjhx50AAAAAACGNnxvOgEdAAAAAIBIKHEHAAAAAGDs0IMOAAAAAMDQhl8TnYAOAAAAAEAkw4fyZgR0AAAAAACGoGIaik5ABwAAAAAggs5gPlxSJ6ADAAAAABAJs7gDAAAAADB26EEHAAAAACA20XvVCegAAAAAAERCiTsAAAAAAGOHHnQAAAAAAGJDiTsAAAAAAEcs3hJ3AjoAAAAAAEPoXA89GgI6AAAAAAARdA/mQaTXI6ADAAAAABBJtCDeDbO4AwAAAACQAPSgAwAAAAAQu8EHphPQAQAAAACIJN5Z3ClxBwAAAAAgAehBBwAAAAAgdpS4AwAAAABwROItcSegAwAAAAAwhCCm1dYI6AAAAAAARNA7mA/Ww05ABwAAAAAgEmZxBwAAAABg7BDQAQAAAAA4NP0PUKfEHQAAAACASGKaHe4+etABAAAAAEgAAjoAAAAAAIem/4nkKHEHAAAAACCSeGdxJ6ADAAAAADAEFVNOJ6ADAAAAABBBXMF8FwEdAAAAAIBI4k3oTBIHAAAAAEACENABAAAAAEgAStwBAAAAAIgkiPXV6EEHAAAAACABCOgAAAAAACQAJe4AAAAAAEQS7yzuBHQAAAAAACKKcy10StwBAAAAAIggznAuBD3oAAAAAABEFG9CpwcdAAAAAIAEIKADAAAAAJAAlLgDAAAAABAJJe4AAAAAAIwdAjoAAAAAAAlAiTsAAAAAAJEEsb4aAR0AAAAAgIiCGDM6Je4AAAAAAEQQZzgXgh50AAAAAAAiYhZ3AAAAAADGDgEdAAAAAIAEoMQdAAAAAIBIKHEHAAAAAGDsENABAAAAAEiA2Evcn3j+xVc7fxu123//eWqgl4g61/1w2zn4FPtHv52DHcdh3y/K/jXea/DtPMptbLzf0R7L6PsXbemHo91OpY7nmh34WdFOujjKa71xvo/+czbwO6lh3u9otnP/dB/1dg72vP3tTO511Po9lMzt7Ly8k3m9j8J2hv/dSd71Ht+xPMptHOb9Du954duZrGu9e3soWddQ96ZGcrbz4OZQcq6jg9vAydnOdvSgAwAAAACQAAR0HJKod+sAAACGQRsEwOgioAMAAAAAkAAEdAAAAAAAEoCAjh7iXdcPAACgP7RBADx4RiKgR544GQAAAACAETESAR1x4C4HAAA4DrRBAKBfBHQAAAAAABKAgA4AwIhiCBgAAONF9vvAgihsHeaGAAAAAAAwjrbEVqGfx9GDDgAAAABAAhDQAQAAAABIAAI6AAAAAAAJQEAHAAAAACABCOgAAAAAACQAAR0AAAAAgAQgoAMAAAAAkAAEdAAAAAAAEoCADgAAAABAAvx/698dMJgpvRYAAAAASUVORK5CYII=" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 5b25d9d..2f73278 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "build:esbuild": "yarn rimraf dist && yarn esbuild --sourcemap=inline --outdir=dist --platform=node --target=es2020 --format=esm --log-level=warning src/**/*.ts", "build:tsc": "yarn rimraf dist && yarn tsc", "build:tsc:no-emit": "yarn tsc --noEmit", - "build:keep": "yarn tsc ", - "start:raw": "node --enable-source-maps --experimental-json-modules --no-warnings dist/src/bot.js", + "build:keep": "yarn tsc", + "start:raw": "node --enable-source-maps --experimental-json-modules --no-warnings --pending-deprecation dist/src/bot.js", "start:esbuild": "yarn build:esbuild && yarn start:raw", "start": "yarn build:tsc && yarn start:raw", "start:keep": "yarn build:keep && yarn start:raw", @@ -33,7 +33,7 @@ "dev": "yarn build:tsc && yarn start:raw", "test": "yarn lint && yarn tsc --noEmit", "format": "yarn prettier . --write", - "lint": "yarn eslint --ext js,jsx,ts,tsx src", + "lint": "yarn eslint yarn eslint src lib config tests", "format:check": "yarn prettier . --check", "upgrade": "yarn rimraf yarn.lock && yarn cache clean && yarn install && yarn up && yarn up -R && yarn set version latest && git submodule update --recursive --remote", "upgrade:sdk": "yarn dlx @yarnpkg/sdks vscode", @@ -44,19 +44,19 @@ }, "imports": { "#lib": { - "default": "./src/lib/index.js" + "default": "./lib/index.js" }, "#constants": { - "default": "./src/lib/utils/BushConstants.js" + "default": "./lib/utils/BushConstants.js" }, "#args": { - "default": "./src/arguments/index.js" + "default": "./lib/arguments/index.js" }, "#commands": { "default": "./src/commands/index.js" }, "#tags": { - "default": "./src/lib/common/tags.js" + "default": "./lib/common/tags.js" } }, "dependencies": { diff --git a/src/arguments/abbreviatedNumber.ts b/src/arguments/abbreviatedNumber.ts deleted file mode 100644 index a7d8ce5..0000000 --- a/src/arguments/abbreviatedNumber.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { BushArgumentTypeCaster } from '#lib'; -import assert from 'assert/strict'; -import numeral from 'numeral'; -assert(typeof numeral === 'function'); - -export const abbreviatedNumber: BushArgumentTypeCaster = (_, phrase) => { - if (!phrase) return null; - const num = numeral(phrase?.toLowerCase()).value(); - - if (typeof num !== 'number' || isNaN(num)) return null; - - return num; -}; diff --git a/src/arguments/contentWithDuration.ts b/src/arguments/contentWithDuration.ts deleted file mode 100644 index 0efba39..0000000 --- a/src/arguments/contentWithDuration.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { parseDuration, type BushArgumentTypeCaster, type ParsedDuration } from '#lib'; - -export const contentWithDuration: BushArgumentTypeCaster> = async (_, phrase) => { - return parseDuration(phrase); -}; diff --git a/src/arguments/discordEmoji.ts b/src/arguments/discordEmoji.ts deleted file mode 100644 index 92d6502..0000000 --- a/src/arguments/discordEmoji.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { regex, type BushArgumentTypeCaster } from '#lib'; -import type { Snowflake } from 'discord.js'; - -export const discordEmoji: BushArgumentTypeCaster = (_, phrase) => { - if (!phrase) return null; - const validEmoji: RegExpExecArray | null = regex.discordEmoji.exec(phrase); - if (!validEmoji || !validEmoji.groups) return null; - return { name: validEmoji.groups.name, id: validEmoji.groups.id }; -}; - -export interface DiscordEmojiInfo { - name: string; - id: Snowflake; -} diff --git a/src/arguments/duration.ts b/src/arguments/duration.ts deleted file mode 100644 index 09dd3d5..0000000 --- a/src/arguments/duration.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { parseDuration, type BushArgumentTypeCaster } from '#lib'; - -export const duration: BushArgumentTypeCaster = (_, phrase) => { - return parseDuration(phrase).duration; -}; diff --git a/src/arguments/durationSeconds.ts b/src/arguments/durationSeconds.ts deleted file mode 100644 index d8d6749..0000000 --- a/src/arguments/durationSeconds.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { parseDuration, type BushArgumentTypeCaster } from '#lib'; - -export const durationSeconds: BushArgumentTypeCaster = (_, phrase) => { - phrase += 's'; - return parseDuration(phrase).duration; -}; diff --git a/src/arguments/globalUser.ts b/src/arguments/globalUser.ts deleted file mode 100644 index 4324aa9..0000000 --- a/src/arguments/globalUser.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { BushArgumentTypeCaster } from '#lib'; -import type { User } from 'discord.js'; - -// resolve non-cached users -export const globalUser: BushArgumentTypeCaster> = async (message, phrase) => { - return message.client.users.resolve(phrase) ?? (await message.client.users.fetch(`${phrase}`).catch(() => null)); -}; diff --git a/src/arguments/index.ts b/src/arguments/index.ts deleted file mode 100644 index eebf0a2..0000000 --- a/src/arguments/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './abbreviatedNumber.js'; -export * from './contentWithDuration.js'; -export * from './discordEmoji.js'; -export * from './duration.js'; -export * from './durationSeconds.js'; -export * from './globalUser.js'; -export * from './messageLink.js'; -export * from './permission.js'; -export * from './roleWithDuration.js'; -export * from './snowflake.js'; diff --git a/src/arguments/messageLink.ts b/src/arguments/messageLink.ts deleted file mode 100644 index c95e42d..0000000 --- a/src/arguments/messageLink.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BushArgumentTypeCaster, regex } from '#lib'; -import type { Message } from 'discord.js'; - -export const messageLink: BushArgumentTypeCaster> = async (message, phrase) => { - const match = new RegExp(regex.messageLink).exec(phrase); - if (!match || !match.groups) return null; - - const { guild_id, channel_id, message_id } = match.groups; - - if (!guild_id || !channel_id || message_id) return null; - - const guild = message.client.guilds.cache.get(guild_id); - if (!guild) return null; - - const channel = guild.channels.cache.get(channel_id); - if (!channel || (!channel.isTextBased() && !channel.isThread())) return null; - - const msg = await channel.messages.fetch(message_id).catch(() => null); - return msg; -}; diff --git a/src/arguments/permission.ts b/src/arguments/permission.ts deleted file mode 100644 index 98bfe74..0000000 --- a/src/arguments/permission.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { BushArgumentTypeCaster } from '#lib'; -import { PermissionFlagsBits, type PermissionsString } from 'discord.js'; - -export const permission: BushArgumentTypeCaster = (_, phrase) => { - if (!phrase) return null; - phrase = phrase.toUpperCase().replace(/ /g, '_'); - if (!(phrase in PermissionFlagsBits)) { - return null; - } else { - return phrase as PermissionsString; - } -}; diff --git a/src/arguments/roleWithDuration.ts b/src/arguments/roleWithDuration.ts deleted file mode 100644 index b97f205..0000000 --- a/src/arguments/roleWithDuration.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Arg, BushArgumentTypeCaster, parseDuration } from '#lib'; -import type { Role } from 'discord.js'; - -export const roleWithDuration: BushArgumentTypeCaster> = async (message, phrase) => { - // eslint-disable-next-line prefer-const - let { duration, content } = parseDuration(phrase); - if (content === null || content === undefined) return null; - content = content.trim(); - const role = await Arg.cast('role', message, content); - if (!role) return null; - return { duration, role }; -}; - -export interface RoleWithDuration { - duration: number | null; - role: Role | null; -} diff --git a/src/arguments/snowflake.ts b/src/arguments/snowflake.ts deleted file mode 100644 index b98a20f..0000000 --- a/src/arguments/snowflake.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { BushArgumentTypeCaster, regex } from '#lib'; -import type { Snowflake } from 'discord.js'; - -export const snowflake: BushArgumentTypeCaster = (_, phrase) => { - if (!phrase) return null; - if (regex.snowflake.test(phrase)) return phrase; - return null; -}; diff --git a/src/arguments/tinyColor.ts b/src/arguments/tinyColor.ts deleted file mode 100644 index 148c078..0000000 --- a/src/arguments/tinyColor.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { BushArgumentTypeCaster } from '#lib'; -import assert from 'assert/strict'; -import tinycolorModule from 'tinycolor2'; -assert(tinycolorModule); - -export const tinyColor: BushArgumentTypeCaster = (_message, phrase) => { - // if the phase is a number it converts it to hex incase it could be representing a color in decimal - const newPhase = isNaN(phrase as any) ? phrase : `#${Number(phrase).toString(16)}`; - return tinycolorModule(newPhase).isValid() ? newPhase : null; -}; diff --git a/src/bot.ts b/src/bot.ts index 038fbbb..10818e9 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,4 +1,4 @@ -import { init } from './lib/utils/BushLogger.js'; +import { init } from '../lib/utils/BushLogger.js'; // creates proxies on console.log and console.warn // also starts a REPL session init(); @@ -6,8 +6,8 @@ init(); import { dirname } from 'path'; import { fileURLToPath } from 'url'; import { default as config } from '../config/options.js'; -import { Sentry } from './lib/common/Sentry.js'; -import { BushClient } from './lib/index.js'; +import { Sentry } from '../lib/common/Sentry.js'; +import { BushClient } from '../lib/extensions/discord-akairo/BushClient.js'; const isDry = process.argv.includes('dry'); if (!isDry && config.credentials.sentryDsn !== null) new Sentry(dirname(fileURLToPath(import.meta.url)) || process.cwd(), config); diff --git a/src/commands/admin/channelPermissions.ts b/src/commands/admin/channelPermissions.ts index 0b09e54..21abd04 100644 --- a/src/commands/admin/channelPermissions.ts +++ b/src/commands/admin/channelPermissions.ts @@ -1,6 +1,5 @@ import { Arg, - BushCommand, ButtonPaginator, clientSendAndPermCheck, emojis, @@ -11,6 +10,7 @@ import { } from '#lib'; import assert from 'assert/strict'; import { ApplicationCommandOptionType, EmbedBuilder, PermissionFlagsBits } from 'discord.js'; +import { BushCommand } from '../../../lib/extensions/discord-akairo/BushCommand.js'; export default class ChannelPermissionsCommand extends BushCommand { public constructor() { diff --git a/src/commands/dev/test.ts b/src/commands/dev/test.ts index ac0ad83..0606497 100644 --- a/src/commands/dev/test.ts +++ b/src/commands/dev/test.ts @@ -18,9 +18,9 @@ import { type ApplicationCommand, type Collection } from 'discord.js'; -import badLinksSecretArray from '../../lib/badlinks-secret.js'; -import badLinksArray from '../../lib/badlinks.js'; -import badWords from '../../lib/badwords.js'; +import badLinksSecretArray from '../../../lib/badlinks-secret.js'; +import badLinksArray from '../../../lib/badlinks.js'; +import badWords from '../../../lib/badwords.js'; export default class TestCommand extends BushCommand { public constructor() { diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts index 348c74f..62f177e 100644 --- a/src/commands/info/help.ts +++ b/src/commands/info/help.ts @@ -9,6 +9,7 @@ import { type OptArgType, type SlashMessage } from '#lib'; +import { stripIndent } from '#tags'; import assert from 'assert/strict'; import { ActionRowBuilder, @@ -21,7 +22,6 @@ import { } from 'discord.js'; import Fuse from 'fuse.js'; import packageDotJSON from '../../../package.json' assert { type: 'json' }; -import { stripIndent } from '../../lib/common/tags.js'; assert(Fuse); assert(packageDotJSON); diff --git a/src/commands/moderation/massEvidence.ts b/src/commands/moderation/massEvidence.ts index 62f4825..cecf273 100644 --- a/src/commands/moderation/massEvidence.ts +++ b/src/commands/moderation/massEvidence.ts @@ -13,7 +13,7 @@ import { } from '#lib'; import assert from 'assert/strict'; import { ApplicationCommandOptionType, PermissionFlagsBits } from 'discord.js'; -import { EvidenceCommand } from '../index.js'; +import EvidenceCommand from './evidence.js'; export default class MassEvidenceCommand extends BushCommand { public constructor() { diff --git a/src/commands/moderation/myLogs.ts b/src/commands/moderation/myLogs.ts index ab67a18..c1cc448 100644 --- a/src/commands/moderation/myLogs.ts +++ b/src/commands/moderation/myLogs.ts @@ -12,7 +12,7 @@ import { } from '#lib'; import { ApplicationCommandOptionType } from 'discord.js'; -import { input, sanitizeInputForDiscord } from '../../lib/common/util/Format.js'; +import { input, sanitizeInputForDiscord } from '../../../lib/utils/Format.js'; import ModlogCommand from './modlog.js'; export default class MyLogsCommand extends BushCommand { public constructor() { diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index 648a178..620f499 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -14,7 +14,7 @@ import { } from '#lib'; import assert from 'assert/strict'; import { ApplicationCommandOptionType, PermissionFlagsBits, type GuildMember } from 'discord.js'; -import { BushCommand } from '../../lib/extensions/discord-akairo/BushCommand.js'; +import { BushCommand } from '../../../lib/extensions/discord-akairo/BushCommand.js'; export default class UnmuteCommand extends BushCommand { public constructor() { diff --git a/src/commands/moulberry-bush/neuRepo.ts b/src/commands/moulberry-bush/neuRepo.ts index d07ba53..fcb6f23 100644 --- a/src/commands/moulberry-bush/neuRepo.ts +++ b/src/commands/moulberry-bush/neuRepo.ts @@ -10,7 +10,7 @@ import { import { dirname, join } from 'path'; import tinycolor from 'tinycolor2'; import { fileURLToPath } from 'url'; -import { formattingInfo, RawNeuItem } from '../../lib/common/util/Minecraft.js'; +import { formattingInfo, RawNeuItem } from '../../../lib/utils/Minecraft.js'; export default class NeuRepoCommand extends BushCommand { public static items: { name: string; id: string }[] = []; diff --git a/src/commands/moulberry-bush/rule.ts b/src/commands/moulberry-bush/rule.ts index 25a3ef0..ab5500d 100644 --- a/src/commands/moulberry-bush/rule.ts +++ b/src/commands/moulberry-bush/rule.ts @@ -8,8 +8,8 @@ import { type OptArgType, type SlashMessage } from '#lib'; +import { stripIndent } from '#tags'; import { ApplicationCommandOptionType, EmbedBuilder, PermissionFlagsBits } from 'discord.js'; -import { stripIndent } from '../../lib/common/tags.js'; const rules = [ { diff --git a/src/commands/utilities/calculator.ts b/src/commands/utilities/calculator.ts index c9dbbf2..dc5593b 100644 --- a/src/commands/utilities/calculator.ts +++ b/src/commands/utilities/calculator.ts @@ -52,7 +52,7 @@ export default class CalculatorCommand extends BushCommand { name: '📤 Output', value: await this.client.utils.inspectCleanRedactCodeblock(calculated.toString(), 'mma') }); - } catch (error) { + } catch (error: any) { decodedEmbed .setTitle(`${emojis.errorFull} Unable to Calculate Expression`) .setColor(colors.error) diff --git a/src/commands/utilities/highlight-block.ts b/src/commands/utilities/highlight-block.ts index a450d71..58e7766 100644 --- a/src/commands/utilities/highlight-block.ts +++ b/src/commands/utilities/highlight-block.ts @@ -2,7 +2,7 @@ import { AllowedMentions, BushCommand, emojis, type ArgType, type CommandMessage import assert from 'assert/strict'; import { Argument, ArgumentGeneratorReturn } from 'discord-akairo'; import { BaseChannel, GuildMember, User } from 'discord.js'; -import { HighlightBlockResult } from '../../lib/common/HighlightManager.js'; +import { HighlightBlockResult } from '../../../lib/common/HighlightManager.js'; import { highlightSubcommands } from './highlight-!.js'; export default class HighlightBlockCommand extends BushCommand { diff --git a/src/commands/utilities/highlight-unblock.ts b/src/commands/utilities/highlight-unblock.ts index 702fa65..2238831 100644 --- a/src/commands/utilities/highlight-unblock.ts +++ b/src/commands/utilities/highlight-unblock.ts @@ -2,7 +2,7 @@ import { AllowedMentions, BushCommand, emojis, type ArgType, type CommandMessage import assert from 'assert'; import { Argument, ArgumentGeneratorReturn } from 'discord-akairo'; import { BaseChannel, GuildMember, User } from 'discord.js'; -import { HighlightUnblockResult } from '../../lib/common/HighlightManager.js'; +import { HighlightUnblockResult } from '../../../lib/common/HighlightManager.js'; import { highlightSubcommands } from './highlight-!.js'; export default class HighlightUnblockCommand extends BushCommand { diff --git a/src/commands/utilities/uuid.ts b/src/commands/utilities/uuid.ts index 3f99e66..04d4013 100644 --- a/src/commands/utilities/uuid.ts +++ b/src/commands/utilities/uuid.ts @@ -23,7 +23,7 @@ export default class UuidCommand extends BushCommand { { id: 'ign', description: 'The ign to find the ign of.', - customType: /\w{1,16}/im, + customType: /^\w{1,16}$/im, readableType: 'string[1,16]', prompt: 'What ign would you like to find the uuid of?', retry: '{error} Choose a valid ign.', diff --git a/src/commands/utilities/wolframAlpha.ts b/src/commands/utilities/wolframAlpha.ts index b35e14f..5ba55f7 100644 --- a/src/commands/utilities/wolframAlpha.ts +++ b/src/commands/utilities/wolframAlpha.ts @@ -87,6 +87,8 @@ export default class WolframAlphaCommand extends BushCommand { }); } } catch (error) { + assert(error instanceof Error); + decodedEmbed .setTitle(`${emojis.errorFull} Unable to Query Expression`) .setColor(colors.error) diff --git a/src/context-menu-commands/message/viewRaw.ts b/src/context-menu-commands/message/viewRaw.ts index 6cfe552..f216a59 100644 --- a/src/context-menu-commands/message/viewRaw.ts +++ b/src/context-menu-commands/message/viewRaw.ts @@ -1,6 +1,6 @@ -import { ViewRawCommand } from '#commands'; import { ContextMenuCommand } from 'discord-akairo'; import { ApplicationCommandType, type ContextMenuCommandInteraction, type Message } from 'discord.js'; +import ViewRawCommand from '../../commands/utilities/viewRaw.js'; export default class ViewRawContextMenuCommand extends ContextMenuCommand { public constructor() { diff --git a/src/context-menu-commands/user/modlog.ts b/src/context-menu-commands/user/modlog.ts index c78396e..91b1b62 100644 --- a/src/context-menu-commands/user/modlog.ts +++ b/src/context-menu-commands/user/modlog.ts @@ -1,7 +1,7 @@ -import { ModlogCommand } from '#commands'; import { emojis, SlashMessage } from '#lib'; import { CommandUtil, ContextMenuCommand } from 'discord-akairo'; import { ApplicationCommandType, type ContextMenuCommandInteraction } from 'discord.js'; +import ModlogCommand from '../../commands/moderation/modlog.js'; export default class ModlogContextMenuCommand extends ContextMenuCommand { public constructor() { diff --git a/src/context-menu-commands/user/userInfo.ts b/src/context-menu-commands/user/userInfo.ts index 6d7f3b6..0d19cce 100644 --- a/src/context-menu-commands/user/userInfo.ts +++ b/src/context-menu-commands/user/userInfo.ts @@ -1,7 +1,7 @@ -import { UserInfoCommand } from '#commands'; import { format } from '#lib'; import { ContextMenuCommand } from 'discord-akairo'; import { ApplicationCommandType, type ContextMenuCommandInteraction, type Guild } from 'discord.js'; +import UserInfoCommand from '../../commands/info/userInfo.js'; export default class UserInfoContextMenuCommand extends ContextMenuCommand { public constructor() { diff --git a/src/lib/badlinks.ts b/src/lib/badlinks.ts deleted file mode 100644 index 3b4cf3b..0000000 --- a/src/lib/badlinks.ts +++ /dev/null @@ -1,6930 +0,0 @@ -/* Links in this file are treated as severity 3 offences. - -made in part possible by https://github.com/nacrt/SkyblockClient-REPO/blob/main/files/scamlinks.json */ -export default [ - "//iscord.gift", - "100cs.ru", - "100eshopdeals.com", - "101nitro.com", - "12mon.space", - "1nitro.club", - "2021cs.net.ru", - "2021ga.xyz", - "2021liss.ru", - "2021pn.ru", - "2021y.ru", - "2022p.ru", - "2022yg.com", - "2023g.com", - "23c7481e.hbrex.cn", - "2discord.ru", - "2faceteam.ml", - "3ds-security.xyz", - "3items4rocket.com", - "4drop.ru.com", - "academynaviagg.xyz", - "accountauthorization.xyz", - "acercup.com", - "ach2x.net.ru", - "achnavi.net.ru", - "acid-tournament.ru", - "affix-cup.click", - "affix-cup.link", - "affix-cup.ru", - "affix-sport.ru", - "affixesports.ru", - "affixsport.ru", - "afkskroll.ru", - "ahijeoir.ru", - "airdrop-discord.com", - "airdrop-discord.online", - "airdrop-discord.ru", - "airdrop-nitro.com", - "airdrops.tips", - "akellasport.me", - "aladdinhub.fun", - "alexandrkost.ru", - "alexs1.ru", - "alive-lives.ru", - "allskinz.xyz", - "alm-gaming.com", - "alone18.ru", - "alonemoly.ru", - "amaterasu.pp.ua", - "ano-skinspin.xyz", - "anomalygiveaways.pro", - "anomalyknifes.xyz", - "anomalyskin.xyz", - "anomalyskinz.xyz", - "anoskinzz.xyz", - "antibot.cc", - "aoeah.promo-codes.world", - "aoeah.shop", - "api.code2gether.cf", - "api.innovations-urfu.site", - "app-discord.com", - "app-discord.ru", - "app-nitro.com", - "application-discord.com", - "appnitro-discord.com", - "appnitro-discord.ru.com", - "appnitrodiscord.ru.com", - "apps-discord.org", - "apps-nitro.com", - "arik.pp.ua", - "asprod911.com", - "asstralissport.org.ru", - "astr-teem.net.ru", - "astr-teem.org.ru", - "astralis-gg.com", - "astralis.monster", - "astralis2.net.ru", - "astralis2.org.ru", - "astralisgift.fun", - "astrallis.net.ru", - "astrallis.org.ru", - "astralliscase.org.ru", - "astralteam.org.ru", - "astresports.xyz", - "atomicstore.ru", - "attaxtrade.com", - "aucryptohubs.com", - "authnet.cf", - "autumnbot.cloud", - "avitofast.ru", - "awirabigmoneyroll.xyz", - "awirabigmoneyrolls.xyz", - "azimovcase.tk", - "badge-team.ml", - "ball-chaser.xyz", - "bandycazez.xyz", - "bangbro.ru", - "battiefy.com", - "beast-cup.ru", - "beast-dr0p.ru", - "beast-winer.ru", - "belekevskeigames.xyz", - "berrygamble.com", - "best-cup.com", - "best-cup.ru", - "bestgeeknavi.ru", - "bestshopusaoffers.com", - "bestskins.org.ru", - "beststeam.gq", - "bestwatchstyle.com", - "beta.discorder.app", - "betadiscord.com", - "bets-cup.ru", - "big.org.ru", - "big.pp.ru", - "bigcsgo.pro", - "bigesports.ru", - "bigmoneyrollawira.xyz", - "bigs.monster", - "bigsports.xyz", - "bistripudel.xyz", - "bit-skins.ru", - "bitcoingenerator.cash", - "bitknife.xyz", - "bitskeansell.ru", - "bitskines.ru", - "blockmincnain.com", - "blocknimchain.com", - "blocksilcnain.com", - "blox.land", - "bloxpromo.com", - "blustcoin.com", - "board-nitro.com", - "bondikflas.xyz", - "bonusxcase.xyz", - "books-pash.org.ru", - "boost-discord.com", - "boost-nitro.com", - "boosted-nitro.com", - "boostnitro.com", - "boostnltro.com", - "bountyweek.com", - "box-surprisebynavi.net.ru", - "boxgolg.club", - "boxnode.ru", - "br0ken-fng.xyz", - "bracesports.ru", - "bro-skiils.net.ru", - "brokenfang-csgo.com", - "brokenfangpassfree.pp.ru", - "brokenfant.org.ru", - "brokentournament.xyz", - "bruteclub.ru", - "buff-market.ru", - "buffgames.ru", - "but-three.xyz", - "buxquick.com", - "buzz-cup.ru", - "bycdu.cam", - "bycsdu.cam", - "bysellers.xyz", - "c-you-mamont.ru", - "c2bit.online", - "c2bit.su", - "case-free.com", - "case-gift.com", - "case-give.com", - "case-magic.space", - "casecs.ru", - "casefire.fun", - "casekey.ru.com", - "casesdrop.ru", - "casesdrop.xyz", - "cash.org.ru", - "cash.pp.ru", - "cashcsgo.ru", - "cashout.monster", - "cashy.monster", - "cassesoma.ru", - "cave-nitro.com", - "cawanmei.ru", - "cawanmei99.ru", - "ccomstimoon.org.ru", - "cgsell.ru", - "cgskinky.xyz", - "chainexplo.com", - "challengeme.in", - "challengeme.vip", - "challengme.ru", - "chance-stem.ru", - "chinchopa.pp.ua", - "circus-shop.ru", - "cis-fastcup.ru", - "cis-rankig.ru", - "cityofmydream.pp.ua", - "claim.robuxat.com", - "claimgifts.shop", - "clan-big.ru", - "classic-nitro.com", - "claud9.xyz", - "clck.ru", - "click-mell.pp.ru", - "cliscord-gift.ru.com", - "cllscordapp.fun", - "cloud9.ru.com", - "cloud9team.space", - "cloudeskins.com", - "cloudfox.one", - "cloudteam9.com", - "clove-nitro.com", - "cmepure.com", - "cmskillcup.com", - "cod3r0bux.pw", - "cointradebtc.com", - "comboline.xyz", - "comdiscord.com", - "come-nitro.com", - "communitytradeoffer.com.ru", - "communitytradeoffer.com", - "communltydrop.pp.ua", - "communltyguard.pp.ua", - "comsteamcommunity.com", - "contact-infoservice.com", - "contralav.ru", - "contralav.xyz", - "coolcools.xyz", - "cooldrop.monster", - "copyrightbusinessgroup.com", - "copyrightbussinessgroup.com", - "copyrighthelpbusiness.org", - "cose-lore.ru", - "counter-stricke.ru", - "counter-strlke.site", - "counterbase.ru.com", - "counterpaid.xyz", - "counterspin.top", - "counterstrik.xyz", - "counterstrikegift.xyz", - "cpanel.copyrighthelpbusiness.org", - "cpbldi.com", - "cpp-discord.com", - "crazy-soom.org.ru", - "crazypage.me", - "creack.tk", - "creditscpfree.website", - "crosflah.online", - "crustalcup.ga", - "cs-activit.xyz", - "cs-astria.xyz", - "cs-beast.xyz", - "cs-betway.xyz", - "cs-boom.org.ru", - "cs-cool.net.ru", - "cs-dark.org.ru", - "cs-dump.org.ru", - "cs-esports.link", - "cs-exeword.xyz", - "cs-fail.ru.com", - "cs-fall.ru.com", - "cs-gameis.ru", - "cs-gorun.ru.com", - "cs-grun.ru.com", - "cs-incursed.xyz", - "cs-legend.xyz", - "cs-lucky.xyz", - "cs-moneyy.ru", - "cs-navigiveaway.ru", - "cs-open.link", - "cs-pill.xyz", - "cs-play.org.ru", - "cs-prizeskins.xyz", - "cs-prizeskinz.xyz", - "cs-riptide.com", - "cs-riptide.ru", - "cs-riptide.xyz", - "cs-simpleroll.xyz", - "cs-skins.link", - "cs-skinz.xyz", - "cs-smoke.xyz", - "cs-spinz.xyz", - "cs-toom.pp.ru", - "cs-tournament.link", - "cs-victory.xyz", - "cs11go.space", - "cs4real.pp.ua", - "cs500go.com", - "csallskin.xyz", - "csbuyskins.in", - "cschanse.ru", - "cschecker.ru", - "cscoat.eu", - "cscodes.ru", - "csfair.pp.ua", - "csfix.me", - "csfreedom.me", - "csfreesklns.ru.com", - "csgameik.ru", - "csgdrop.ru", - "csgfocusa.ru", - "csggolg.ru", - "csgif.org.ru", - "csgift.fun", - "csgo-analyst.com", - "csgo-battle.ru", - "csgo-cash.eu", - "csgo-cup.ru", - "csgo-cyber.link", - "csgo-dym.ru", - "csgo-fute.net.ru", - "csgo-game-steam.ru", - "csgo-games.xyz", - "csgo-gamesteam.ru", - "csgo-gifts.com", - "csgo-lute.net.ru", - "csgo-market.ru.com", - "csgo-pell.org.ru", - "csgo-riptide.ru", - "csgo-run.info", - "csgo-run.site", - "csgo-sports.com", - "csgo-st.ru", - "csgo-steam-game.ru", - "csgo-steam-good.ru", - "csgo-steamanalyst.net", - "csgo-steamgame.ru", - "csgo-steamplay.ru", - "csgo-store-steam.ru", - "csgo-storesteam.ru", - "csgo-swapskin.com", - "csgo-trade.net", - "csgo-up.com", - "csgo-z.com", - "csgo.ghservers.cl", - "csgo2021.ru", - "csgo4cases.fun", - "csgobb.xyz", - "csgobccp.ru", - "csgobeats.com", - "csgobelieve.ru", - "csgocase.monster", - "csgocase.one", - "csgocases.monster", - "csgocashs.com", - "csgocheck.ru.com", - "csgocheck.ru", - "csgochinasteam.ru", - "csgocj-steam.work", - "csgocnfocuss.ru", - "csgocompetive.com", - "csgocup.ru", - "csgocupp.ru.com", - "csgocybersport.ru.com", - "csgodetails.info", - "csgodirect.xyz", - "csgodreamer.com", - "csgodrops.monster", - "csgodrs.com", - "csgoeasywin.ru.com", - "csgoelite.xyz", - "csgoencup.com", - "csgoevent.xyz", - "csgofast.xyz", - "csgoflash.net.ru", - "csgofocusc.xyz", - "csgogame-steam.ru", - "csgoganeak.ru", - "csgoganeik.ru", - "csgogf01.xyz", - "csgogf02.xyz", - "csgogf03.xyz", - "csgogf04.xyz", - "csgogf05.xyz", - "csgogf06.xyz", - "csgogf07.xyz", - "csgogf12.xyz", - "csgogf13.xyz", - "csgogf14.xyz", - "csgogf15.xyz", - "csgogift25.xyz", - "csgogift26.xyz", - "csgogift34.xyz", - "csgogift43.xyz", - "csgogift44.xyz", - "csgogift45.xyz", - "csgogift47.xyz", - "csgogift49.xyz", - "csgogift50.xyz", - "csgogift51.xyz", - "csgogift55.xyz", - "csgogift56.xyz", - "csgogift57.xyz", - "csgogift58.xyz", - "csgogift59.xyz", - "csgogift60.xyz", - "csgogift62.xyz", - "csgogift77.xyz", - "csgogpusk.ru", - "csgoindex.ru.com", - "csgoindex.ru", - "csgoitemdetails.com", - "csgoitemsprices.com", - "csgojs.xyz", - "csgojump.ru", - "csgoko.tk", - "csgold.monster", - "csgomarble.xyz", - "csgomarketplace.net", - "csgomarkets.net", - "csgonavi.com", - "csgoorun.ru", - "csgoprocupgo.com", - "csgorcup.com", - "csgoroll.ru", - "csgorose.com", - "csgoroulette.monster", - "csgoroyalskins1.com", - "csgorun-rubonus.ru", - "csgorun.info", - "csgorun.pro-login.ru", - "csgorun.pro-loginn.com", - "csgosell.xyz", - "csgoskill.ru", - "csgoskinprices.com", - "csgoskinsinfo.com", - "csgoskinsroll.com", - "csgosprod.com", - "csgossteam.ru", - "csgossteam.xyz", - "csgostats.fun", - "csgosteam-game.ru", - "csgosteam-play.ru", - "csgosteamanalysis.com", - "csgosteamanalyst.ru", - "csgosteamcom.ru", - "csgosteamgo.ru", - "csgoteammate.gq", - "csgothunby.com", - "csgotournaments.cf", - "csgotrades.net", - "csgotreder.com", - "csgovip.ru", - "csgowans.ru", - "csgowaycup.ru.com", - "csgowincase.xyz", - "csgoworkshops.com", - "csgoxgiveaway.ru", - "csgozone.net.in", - "csgunskins.xyz", - "cslpkmf.ru", - "csm-oney.ru", - "csmarkete.info", - "csmone-y.ru", - "csmoneyskinz.xyz", - "csmvcecup.com", - "csogamech.xyz", - "csogamecm.xyz", - "csogamee.xyz", - "csogamef.xyz", - "csogamegg.ru", - "csogameke.xyz", - "csoggskif.ru", - "csoggskif.xyz", - "csogzhnc.xyz", - "csprices.in", - "csrandom.monster", - "css500gggo.ru", - "csskill.com", - "csskillpro.xyz", - "csskins.space", - "csskinz.xyz", - "csteamskin.ru", - "cstournament.ru", - "cswanmei.ru", - "cswanmei4.ru", - "cswinterpresent.xyz", - "csxrnoney.com", - "cteamcamnynity67823535672.xyz", - "cteamcommunity.xyz", - "cubesmc.ru", - "cupcs.ru", - "cupcsgo.ru", - "cupgoo.xyz", - "cupsul.ru", - "cupwin.xyz", - "cyber-csgo.link", - "cyber-csgo.space", - "cyber-lan.com", - "cyber-roll.club", - "cyber-roll.monster", - "cyber-shok.online", - "cyber-shok.ru", - "cyber-win.ru", - "cyber-x.xyz", - "cybercsgo.link", - "cyberdex.ru", - "cyberegocscom.ru", - "cyberesports-tournaments.ru", - "cybergamearena.ru", - "cyberiaevents.ru", - "cyberlev.ru", - "cybermode.ru", - "cyberscsgo.ru", - "cyberspark.org.ru", - "d-nitro.tk", - "d.iscord.xyz", - "d.myticks.xyz", - "d1scord.xyz", - "d1scrod.site", - "d2csbox.pp.ua", - "d2cups.com", - "d2faceit.com", - "d3l3.tk", - "dac-game.xyz", - "daddsda.xyz", - "dailymegadeal.xyz", - "dawbab.xyz", - "daxrop.xyz", - "dciscord.com", - "ddiscord.com", - "deadisidddde.xyz", - "deamonbets.ru", - "def-dclss.pp.ua", - "demonbets.ru", - "denforapasi.cf", - "der-csgo.ru", - "derimonz.xyz", - "derwoood.xyz", - "desmond.ru.com", - "determined-haslett.45-138-72-103.plesk.page", - "dfiscord.com", - "diablobets.com", - "diacordapp.com", - "diascord.com", - "diccrd.com", - "dicksod.co", - "dicoapp.me", - "dicoapp.pro", - "dicord.gg", - "dicord.gift", - "dicord.site", - "dicord.space", - "dicordapp.com", - "dicordgift.ru.com", - "dicordglfts.ga", - "dicordglfts.gq", - "dicovrd.com", - "dicrod.com", - "dicscordapp.com", - "dicsocrd.com", - "dicsord-airdrop.com", - "dicsord-airdrop.ru", - "dicsord-app.com", - "dicsord-events.com", - "dicsord-gift.com", - "dicsord-gifte.ru.com", - "dicsord-gifted.ru", - "dicsord-gifts.ru", - "dicsord-give.com", - "dicsord-give.ru", - "dicsord-gives.com", - "dicsord-hypesquads.com", - "dicsord-nitro.com", - "dicsord-nitro.ru", - "dicsord-steam.com", - "dicsord-ticket.com", - "dicsord.gg", - "dicsord.gifts", - "dicsord.net", - "dicsord.pl", - "dicsord.pw", - "dicsord.ru", - "dicsord.space", - "dicsord.website", - "dicsordapp.co", - "dicsordgift.club", - "dicsordgift.com", - "dicsordgive.ru.com", - "dicsordnitro.info", - "dicsordnitro.store", - "dicsordr.xyz", - "dicsords-gift.ru", - "dicsords.ru", - "dicsrod.com", - "didiscord.com", - "didscord.com", - "diiiscrod.club", - "diisccord.club", - "diiscord-app.com", - "diiscord-gift.com", - "diiscord-nittro.ru", - "diiscord.com", - "dIiscord.com", - "diiscord.gift", - "diiscord.me", - "diiscordapp.com", - "diisscord.club", - "diisscord.online", - "dijscord.com", - "dilscord.com", - "dioscord.com", - "diqscordapp.com", - "dircode.ru", - "direct-link.net", - "dirolzz.xyz", - "dirscod.com", - "dirscod.gift", - "dirscord-gift.ru", - "dirscordapp.com", - "dis.cord.gifts", - "disbordapp.com", - "disbords.com", - "disbored.com", - "disc-ord.com", - "disc.cool", - "disc.gifts", - "disc0rd-app.ru.com", - "disc0rd-nitro.site", - "disc0rd.org", - "disc0rd.site", - "disc0rd.xyz", - "discapp.info", - "discard.gg", - "discard.gift", - "discard.xyz", - "discardapp.fun", - "disccor.com", - "disccord-apps.com", - "disccord-appss.ru", - "disccord-club.com", - "disccord-gift.com", - "disccord.gg", - "disccord.ru.com", - "disccord.ru", - "disccord.shop", - "disccord.tk", - "disccords.com", - "disccrd.gifts", - "disccrdapp.com", - "disceord.gift", - "discerd.gift", - "discford.com", - "discgrdapp.com", - "dischrd.com", - "discird.gg", - "discird.me", - "discjrd.com", - "disckord.com", - "disckordapp.com", - "disclord.com", - "disclrd.com", - "discnrd.gift", - "discnrdapp.com", - "disco.to", - "disco3d.app", - "disco9rdapp.com", - "discoapps.club", - "discoard.com", - "discocd.com", - "discocdapp.com", - "discocl.xyz", - "discoclapp.xyz", - "discocord.com", - "discocrd-gift.com", - "discocrd-gifts.com", - "discocrd-nitro.com", - "discocrd.gift", - "discocrd.gifts", - "discocrdapp.com", - "discod-hitro.xyz", - "discod-nitro.ru", - "discod.art", - "discod.fun", - "discod.gift", - "discod.gifts", - "discod.info", - "discod.tech", - "discodapp.gift", - "discodapp.net", - "discode.gift", - "discodnitro.info", - "discodnitro.ru", - "discodrd.com", - "discoed.gg", - "discoed.me", - "discoerd.com", - "discoerdapp.com", - "discofd.com", - "discokrd.com", - "discold.online", - "discold.ru", - "discolrd.com", - "discond-nitro.ru", - "discond-njtro.tech", - "discond.gift", - "discond.ru.com", - "discondapp.fun", - "disconrd.com", - "discontro.ru", - "discoogs.com", - "discoord-apps.com", - "discoord-nitro.com", - "discoord.space", - "discor-dnitro.fun", - "discor.de", - "discor.gg", - "discor.link", - "discor.me", - "discorad.com", - "discorapp.gq", - "discorapp.pw", - "discorb-nitro.ru.com", - "discorb.blog", - "discorb.co", - "discorb.com", - "discorb.gift", - "discorb.gifts", - "discorb.ru.com", - "discorc-nitro.site", - "discorcd-apps.com", - "discorcd-gift.com", - "discorcd-nitro.com", - "discorcd.click", - "discorcd.com", - "discorcd.gift", - "discorcd.gifts", - "discorcd.site", - "discorcdapp.com", - "discorci.com", - "discorcl-air.xyz", - "discorcl-app.com", - "discorcl-app.ru", - "discorcl-app.xyz", - "discorcl-boost.ru", - "discorcl-gift.org.ru", - "discorcl-gift.ru.com", - "discorcl-gift.ru", - "discorcl-gift.xyz", - "discorcl-give.site", - "discorcl-nitro.com", - "discorcl-nitro.ru.com", - "discorcl-nitro.site", - "discorcl.app", - "discorcl.art", - "discorcl.click", - "discorcl.club", - "discorcl.fun", - "discorcl.ga", - "discorcl.gift", - "discorcl.gifts", - "discorcl.info", - "discorcl.link", - "discorcl.online", - "discorcl.ru.com", - "discorcl.ru", - "discorcl.shop", - "discorcl.site", - "discorcl.store", - "discorclapp.com", - "discorclapp.fun", - "discorclgift.com", - "discorclgift.xyz", - "discorcll.com", - "discorcll.online", - "discorclnitro.ru", - "discorclsteam.com", - "discorcrd.gift", - "discorcz-booster.ru", - "discord-a.com", - "discord-accept.com", - "discord-accounts.com", - "discord-accounts.ru", - "discord-air.fun", - "discord-air.pw", - "discord-air.xyz", - "discord-airclrop.pw", - "discord-airdop.link", - "discord-airdrop.com", - "discord-airdrop.fun", - "discord-airdrop.info", - "discord-airdrop.me", - "discord-airdrop.pw", - "discord-airdrop.site", - "discord-airdrop.xyz", - "discord-airnitro.xyz", - "discord-alidrop.me", - "discord-alrdrop.com", - "discord-app.cc", - "discord-app.click", - "discord-app.club", - "discord-app.co.uk", - "discord-app.co", - "discord-app.gift", - "discord-app.gifts", - "discord-app.info", - "discord-app.io", - "discord-app.live", - "discord-app.me", - "discord-app.net", - "discord-app.ru.com", - "discord-app.shop", - "discord-app.store", - "discord-app.su", - "discord-app.top", - "discord-app.uk", - "discord-app.us", - "discord-app.xyz", - "discord-application.com", - "discord-applications.com", - "discord-apply.com", - "discord-appnitro.com", - "discord-apps.ru", - "discord-apps.site", - "discord-apps.space", - "discord-apps.xyz", - "discord-best-nitro.xyz", - "discord-bonus.ru", - "discord-boost.com", - "discord-boost.ru.com", - "discord-boost.ru", - "discord-boost.xyz", - "discord-bot.com", - "discord-bot.ru", - "discord-bugs.com", - "discord-claim.com", - "discord-claim.ru.com", - "discord-claim.ru", - "discord-clap.com", - "discord-click.shop", - "discord-club.ru", - "discord-com-free.online", - "discord-com-free.ru", - "discord-control.com", - "discord-controls.com", - "discord-cpp.com", - "discord-develop.com", - "discord-developer.com", - "discord-devs.com", - "discord-do.com", - "discord-dr0p.ru", - "discord-drop.gift", - "discord-drop.info", - "discord-drop.xyz", - "discord-drops.ru", - "discord-egift.com", - "discord-event.com", - "discord-event.info", - "discord-events.com", - "discord-exploits.tk", - "discord-faq.com", - "discord-free-nitro.ru", - "discord-free.com", - "discord-free.site", - "discord-freenitro.online", - "discord-freenitro.pw", - "discord-fun.com", - "discord-game.com", - "discord-games.cf", - "discord-generator.tk", - "discord-get.click", - "discord-get.ru", - "discord-gg.com", - "discord-gg.ru.com", - "discord-gif.xyz", - "discord-gifft.com", - "discord-gift-free-nitro.tk", - "discord-gift-nitro.site", - "discord-gift.app", - "discord-gift.info", - "discord-gift.net.ru", - "discord-gift.online", - "discord-gift.ru.com", - "discord-gift.ru", - "discord-gift.shop", - "discord-gift.site", - "discord-gift.top", - "discord-gift.us", - "discord-gifte.com", - "discord-gifte.ru", - "discord-gifte.xyz", - "discord-gifted.ru.com", - "discord-giftef.xyz", - "discord-gifteh.xyz", - "discord-giftes.com", - "discord-gifts.com.ru", - "discord-gifts.com", - "discord-gifts.me", - "discord-gifts.org", - "discord-gifts.ru.com", - "discord-gifts.shop", - "discord-gifts.site", - "discord-givaewey.ru", - "discord-give.com", - "discord-give.net", - "discord-give.org", - "discord-give.pw", - "discord-give.ru.com", - "discord-give.ru", - "discord-give.xyz", - "discord-giveaway.com", - "discord-giveaways.ru", - "discord-glft.com", - "discord-glft.ru.com", - "discord-glft.xyz", - "discord-halloween-nitro.com", - "discord-halloween.com", - "discord-halloween.link", - "discord-halloween.me", - "discord-halloween.ru.com", - "discord-halloween.ru", - "discord-hallowen.ru.com", - "discord-help.com", - "discord-helpers.com", - "discord-hse.com", - "discord-hype.com", - "discord-hypeevent.com", - "discord-hypes.com", - "discord-hypesquad.com", - "discord-hypesquad.info", - "discord-hypesquade.com", - "discord-hypesquaders.com", - "discord-hypesquads.com", - "discord-hypevent.com", - "discord-i.com", - "discord-info.com", - "discord-infoapp.xyz", - "discord-information.com", - "discord-information.ru", - "discord-informations.com", - "discord-informations.ru", - "discord-install.com", - "discord-invite-link.com", - "discord-job.com", - "discord-jobs.com", - "discord-list.cf", - "discord-load.ru", - "discord-login.cf", - "discord-mega.xyz", - "discord-mod.com", - "discord-moderation.com", - "discord-moderator.com", - "discord-moderator.us", - "discord-mods.com", - "discord-net-labs.com", - "discord-netro.ru", - "discord-news.com", - "discord-niittro.ru", - "discord-nilro.ru", - "discord-niltro.com", - "discord-niltro.ru.com", - "discord-nitr0gift.fun", - "discord-nitre.xyz", - "discord-nitro-boost.xyz", - "discord-nitro-classic.com", - "discord-nitro-free.ml", - "discord-nitro-free.ru", - "discord-nitro-free.xyz", - "discord-nitro.click", - "discord-nitro.cloud", - "discord-nitro.club", - "discord-nitro.co", - "discord-nitro.com", - "discord-nitro.eu", - "discord-nitro.gift", - "discord-nitro.gifts", - "discord-nitro.info", - "discord-nitro.it", - "discord-nitro.link", - "discord-nitro.live", - "discord-nitro.net", - "discord-nitro.online", - "discord-nitro.org", - "discord-nitro.pro", - "discord-nitro.ru.com", - "discord-nitro.services", - "discord-nitro.shop", - "discord-nitro.store", - "discord-nitro.su", - "discord-nitro.tech", - "discord-nitro.tk", - "discord-nitro.website", - "discord-nitroapp.ru", - "discord-nitroapp.xyz", - "discord-nitrodrop.xyz", - "discord-nitroe.xyz", - "discord-nitrogift.com", - "discord-nitrogift.ru", - "discord-nitrogift.xyz", - "discord-nitros.com", - "discord-nitros.ru", - "discord-nitrot.xyz", - "discord-njtro.store", - "discord-nltro.com", - "discord-nltro.fun", - "discord-nltro.info", - "discord-nltro.ru", - "discord-nudes.club", - "discord-nudes.live", - "discord-o.com", - "discord-offer.com", - "discord-partner.com", - "discord-partners.com", - "discord-premium.com", - "discord-present.ru", - "discord-promo.com", - "discord-promo.info", - "discord-promo.ru.com", - "discord-promo.site", - "discord-promo.xyz", - "discord-promotions.com", - "discord-promox.com", - "discord-report.com", - "discord-ro.tk", - "discord-ru.site", - "discord-security.com", - "discord-service.com", - "discord-sex.live", - "discord-shop.fun", - "discord-sms.eu", - "discord-soft.ru", - "discord-spooky.ru", - "discord-staff.com", - "discord-stat.com", - "discord-stats.com", - "discord-stats.org", - "discord-steam.com", - "discord-steam.ru", - "discord-steam.site", - "discord-steams.com", - "discord-stemdrop.me", - "discord-stuff.com", - "discord-sup.com", - "discord-support.com", - "discord-support.org", - "discord-support.tech", - "discord-supports.com", - "discord-team.com", - "discord-tech.com", - "discord-tester.com", - "discord-to.com", - "discord-true.com", - "discord-trustandsafety.com", - "discord-up.ru", - "discord-verif.ga", - "discord-verification.com", - "discord-verifications.com", - "discord-verify-account.ml", - "discord-verify.com", - "discord-verify.ru", - "discord-vetify.com", - "discord-web.co", - "discord-xnitro.com", - "discord.1nitro.club", - "discord.ac", - "discord.app.br", - "discord.app", - "discord.bargains", - "discord.best", - "discord.biz", - "discord.blog", - "discord.cc", - "discord.cloud", - "discord.cm", - "discord.cn.com", - "discord.co.com", - "discord.co.in", - "discord.co.za", - "discord.com.pl", - "discord.com.tw", - "discord.cool", - "discord.creditcard", - "discord.deals", - "discord.download", - "discord.es", - "discord.eu", - "discord.family", - "discord.fit", - "discord.foundation", - "discord.fyi", - "discord.gifte", - "discord.givaeway.com", - "discord.givaewey.com", - "discord.giveawey.com", - "discord.giveaweys.com", - "discord.glfte.com", - "discord.gq", - "discord.homes", - "discord.in", - "discord.istanbul", - "discord.limited", - "discord.ltd", - "discord.luxe", - "discord.marketing", - "discord.moscow", - "discord.my", - "dIscord.net", - "discord.online", - "discord.org.ru", - "discord.porn", - "discord.pp.ru", - "discord.promo", - "discord.pt", - "discord.ru.net", - "discord.shop", - "discord.si", - "discord.team", - "discord.tools", - "discord.tw", - "discord.world", - "discord2fa.com", - "discord404.com", - "discord4nitro.com", - "discordaap.com", - "discordacc2.repl.co", - "discordadp.com", - "discordadpp.com", - "discordaepp.com", - "discordalt4.repl.co", - "discordalt5.repl.co", - "discordalts293.repl.co", - "discordaoo.com", - "discordaop.com", - "discordapp.best", - "discordapp.biz", - "discordapp.click", - "discordapp.cloud", - "discordapp.co.uk", - "discordapp.eu", - "discordapp.gg", - "discordapp.help", - "discordapp.ir", - "discordapp.org", - "discordapp.pages.dev", - "discordapp.pw", - "discordapp.rip", - "discordapp.ru.com", - "discordapp.social", - "discordapp.store", - "discordapp.support", - "discordapp.top", - "discordapp.us", - "discordapp.vercel.app", - "discordapp.vip", - "discordapp.ws", - "discordappi.fun", - "discordapplication.com", - "discordapplication.xyz", - "discordapplications.com", - "discordappo.com", - "discordappp.com", - "discordappp.net", - "discordappporn.chat", - "discordapps.gift", - "discordapps.gifts", - "discordapps.tk", - "discordappss.com", - "discordaspp.com", - "discordbagequiz.cf", - "discordbeta.com", - "discordbetter.app", - "discordboost.net", - "discordbooster.com", - "discordbothost.com", - "discordbotist.com", - "discordbots.app", - "discordbugs.com", - "discordc.gift", - "discordcanary.com", - "discordcdn.sa.com", - "discordcharity.org", - "discordcheats.net", - "discordclgift.net.ru", - "discordcommunlty.com", - "discordcrasher.wtf", - "discordcreators.net", - "discordd.buzz", - "discordd.gg", - "discordd.gift", - "discorddaapp.com", - "discorddev.com", - "discorddevelopment.com", - "discorddevs.com", - "discorddiscord.com", - "discorddrop.com", - "discorde-gift.com", - "discorde-gifte.com", - "discorde-nitro.com", - "discorde.gift", - "discorde.xyz", - "discordevents.com", - "discordf.com", - "discordf.gift", - "discordfree.com", - "discordfrnitro.site", - "discordg.com.ru", - "discordg.link", - "discordgame.com", - "discordgamers.co.uk", - "discordgft.com", - "discordgg.com", - "discordgif.com", - "discordgift.app", - "discordgift.com", - "discordgift.fun", - "discordgift.info", - "discordgift.net.ru", - "discordgift.org", - "discordgift.pw", - "discordgift.ru.com", - "discordgift.ru", - "discordgift.site", - "discordgift.tk", - "discordgift.xyz", - "discordgifte.site", - "discordgifted.xyz", - "discordgiftis.ru", - "discordgifts-pay.ru.com", - "discordgifts-pay.ru", - "discordgifts.co.uk", - "discordgifts.com", - "discordgifts.fun", - "discordgifts.info", - "discordgifts.link", - "discordgifts.me", - "discordgifts.ru.com", - "discordgifts.ru", - "discordgifts.site", - "discordgifts.store", - "discordgiftss.com", - "discordgiftsteam.ru", - "discordgiftz.xyz", - "discordgive.ru.com", - "discordgive.ru", - "discordgiveaway.fun", - "discordgivenitro.com", - "discordgivenitro.ru.com", - "discordglft.com", - "discordglft.ru", - "discordglfts.com", - "discordglfts.xyz", - "discordhalloween.co.uk", - "discordhalloween.com", - "discordhalloween.gift", - "discordhalloween.uk", - "discordi.gift", - "discordiapp.fun", - "discordiatech.co.uk", - "discordicon.com", - "discordimages.com", - "discordinfo.com", - "discordinfo.ru", - "discordinvite.ml", - "discordist.com", - "discordj.gift", - "discordjob.com", - "discordjs.tech", - "discordl-steam.com", - "discordl.com", - "discordl.pw", - "discordl.site", - "discordl.xyz", - "discordlapp.fun", - "discordlgift.com", - "discordlgift.ru.com", - "discordlinks.co.uk", - "discordlist.repl.co", - "discordlive.xyz", - "discordll.gift", - "discordlogin.com", - "discordmac.com", - "discordme.me", - "discordmoderations.com", - "discordn.com", - "discordn.gift", - "discordnitro-gift.com", - "discordnitro-steam.ru", - "discordnitro.altervista.org", - "discordnitro.biz", - "discordnitro.cc", - "discordnitro.click", - "discordnitro.club", - "discordnitro.com", - "dIscordnitro.com", - "discordnitro.fun", - "discordnitro.gift", - "discordnitro.info", - "discordnitro.link", - "discordnitro.ru.com", - "discordnitro.space", - "discordnitro.store", - "discordnitro.su", - "discordnitro9.repl.co", - "discordnitroapp.ru.com", - "discordnitroevent.info", - "discordnitrofree.com", - "discordnitrofree.xyz", - "discordnitrogenerator.com", - "discordnitrogift.com", - "discordnitrogift.ru", - "discordnitrogifts.pl", - "discordnitrolink.tk", - "discordnitropromo.site", - "discordnitros.gifts", - "discordnitros.xyz", - "discordnitrosteam.com", - "discordnltro.com", - "discordobs.com", - "discordp.com", - "discordp.ml", - "discordpap.com", - "discordpp.com", - "discordprize.xyz", - "discordpromo.site", - "discordq.com", - "discordqapp.com", - "discordqpp.com", - "discordqr.com", - "discordre.store", - "discordresearch.com", - "discordrgift.com", - "discordrgift.online", - "discordrgift.ru", - "discords-accounts.ru", - "discords-app.com", - "discords-dev.ga", - "discords-developers.com", - "discords-events.com", - "discords-gift.com", - "discords-gift.ru", - "discords-gifte.ru", - "discords-gifts.club", - "discords-gifts.ru", - "discords-glft.com", - "discords-hypes.com", - "discords-hypesquad.com", - "discords-hypesquads.com", - "discords-moderation.com", - "discords-moderator.com", - "discords-nitro.com", - "discords-nitro.site", - "discords-nitro.xyz", - "discords-nitroapp.xyz", - "discords-nitros.fun", - "discords-nitros.shop", - "discords-premium.com", - "discords-premium.site", - "discords-steam.com", - "discords-support.com", - "discords-teams.com", - "discords.biz", - "discords.co.uk", - "discords.company", - "discords.gifts", - "discords.net", - "discords.ru.com", - "discords.ru", - "discords.us", - "discordsapi.com", - "discordsapp.fun", - "discordsapp.xyz", - "discordsapplication.info", - "discordsatus.com", - "discordsearch.co", - "discordservice.com", - "discordsex.live", - "discordsgift.com", - "discordsgift.info", - "discordshort.ga", - "discordsite.repl.co", - "discordsnitro.com", - "discordsnitro.store", - "discordsnitros.one", - "discordspp.com", - "discordss.ru", - "discordstaff.xyz", - "discordstat.com", - "discordsteam.com", - "discordsteam.ru", - "discordsteams.com", - "discordsub.com", - "discordsupport.gg", - "discordt.gift", - "discordtest.xyz", - "discordtesters.com", - "discordtext.com", - "discordtoken.com", - "discordtokens.shop", - "discordtokens2.repl.co", - "discordtos.com", - "discordtotal.com", - "discordtotal.net", - "discordtts.com", - "discordtw.com", - "discordu.gift", - "discordup.ru", - "discordx.link", - "discordx.ml", - "discordxgift.xyz", - "discordxnitro.xyz", - "discordxsteam.com", - "discoredapp.com", - "discorfd.com", - "discorg.gg", - "discorgift.online", - "discorgift.xyz", - "discorid.gift", - "discoril.com", - "discorl.com", - "discorld-gift.site", - "discorld.com", - "discorld.site", - "discorlgifts.store", - "discorll.com", - "discornd.com", - "discorrd.com", - "discorrd.gift", - "discorrd.link", - "discorrd.ru", - "discorrd.site", - "discorrdapp.com", - "discorrl.com", - "discorsd.com", - "discorsd.gifts", - "discort-nitro.com", - "discort.com", - "discort.site", - "discortnitosteam.online", - "discortnitostem.online", - "discosd.com", - "discosrd.com", - "discotdapp.com", - "discourd.com", - "discourd.info", - "discourd.site", - "discourdapp.com", - "discovd.com", - "discpordapp.com", - "discprd.com", - "discqorcl.com", - "discrd.co", - "discrd.gg", - "discrdapp.cf", - "discrdapp.com", - "discrds.gift", - "discrdspp.com", - "discrocl.xyz", - "discrod-app.com", - "discrod-app.ru", - "discrod-app.site", - "discrod-apps.ru", - "discrod-gift.com", - "discrod-gifte.com", - "discrod-gifts.club", - "discrod-glfts.com", - "discrod-nitro.fun", - "discrod-nitro.info", - "discrod-up.ru", - "discrod.gg", - "discrod.gift", - "discrod.gifts", - "discrod.pw", - "discrod.ru", - "discrodapp.ru", - "discrodapp.site", - "discrodapp.xyz", - "discrode-app.club", - "discrode-app.com", - "discrode-gift.club", - "discrode-gift.com", - "discrode-gifte.club", - "discrode.gift", - "discrodnitro.org", - "discrodnitro.ru", - "discrods.gift", - "discrods.site", - "discrodsteam.online", - "discrodsteam.ru", - "discrodup.ru", - "discrord.com", - "discrordapp.com", - "discsord.com", - "discsrdapp.com", - "discurcd.com", - "discurd.js.org", - "discvordapp.com", - "discxordapp.com", - "disdrop.com.br", - "disinfo.org.ru", - "disiscord.com", - "diskord.gg", - "diskord.org.ru", - "diskord.ru.com", - "dislcord.com", - "disocordapp.com", - "disocr.com", - "disocrd-gift.com", - "disocrd-gift.ru", - "disocrd.co", - "disocrd.codes", - "disocrd.gg", - "disocrd.gifts", - "disocrd.me", - "disocrd.org", - "disocrd.ru", - "disocrd.tk", - "disocrdapp.com", - "disocrde.gift", - "disocrds.gift", - "disorc.com", - "disord.co", - "disord.codes", - "disord.fun", - "disord.gift", - "disord.gifts", - "disordapp.gift", - "disordapp.gifts", - "disorde.gift", - "disordgift.codes", - "disordgifts.com", - "disordglft.com", - "disordnitros.gifts", - "disordnitros.xyz", - "disordnltro.xyz", - "disordnltros.com", - "disordnltros.com", - "disordnltros.gifts", - "disords.gift", - "disordsnitro.gifts", - "disordsnitros.gifts", - "disrcod.com", - "disrcod.gift", - "disrcod.gifts", - "disrcord.com", - "disscord.com", - "disscord.gift", - "disscord.online", - "disscord.ru", - "disscords.club", - "dissord.com", - "dissord.gift", - "dissord.ru", - "diswcord.com", - "disxcord.com", - "disxord.com", - "diszcord.com", - "diszcordapp.com", - "diucord.js.org", - "diuscordapp.com", - "divinegardens.xyx", - "diwcord.com", - "dixcord.com", - "dixscord.com", - "dizcord.app", - "dizcord.com", - "dizcord.gift", - "dizscord.com", - "djiscord.com", - "djscord.com", - "dkscord.com", - "dlcord.gift", - "dlcsorcl.com", - "dlcsorcl.ru", - "dlcsord-airdrop.com", - "dlcsord-gift.com", - "dlicord-glfts.site", - "dlicsord.ru", - "dliscord-gift.com", - "dliscord-gift.ru.com", - "dliscord-gifts.com", - "dliscord-giveaway.ru", - "dliscord-glft.ru.com", - "dliscord-nitro.com", - "dliscord.com", - "dliscord.gift", - "dliscord.us", - "dliscordl.com", - "dliscordnltro.com", - "dliscords.com", - "dliscrd.one", - "dlisocrd.ru", - "dllscord.online", - "dlscard.ru", - "dlsccord-app.club", - "dlsccord-apps.club", - "dlsccrd.com", - "dlscocrd.club", - "dlscocrd.com", - "dlscocrdapp.com", - "dlscorcl-apps.com", - "dlscorcl.gift", - "dlscorcl.info", - "dlscorcl.ru.com", - "dlscorcl.ru", - "dlscorcl.shop", - "dlscorcl.xyz", - "dlscorclapp.fun", - "dlscord-alirdrop.com", - "dlscord-alirdrop.site", - "dlscord-app.com", - "dlscord-app.info", - "dlscord-app.net", - "dlscord-app.ru.com", - "dlscord-app.ru", - "dlscord-app.su", - "dlscord-app.xyz", - "dlscord-apps.com", - "dlscord-boost.fun", - "dlscord-claim.com", - "dlscord-developer.com", - "dlscord-game.com", - "dlscord-gift.com", - "dlscord-gift.one", - "dlscord-gift.ru.com", - "dlscord-gift.xyz", - "dlscord-gifts.com", - "dlscord-gifts.xyz", - "dlscord-glft.pw", - "dlscord-glft.ru.com", - "dlscord-glft.xyz", - "dlscord-glfts.xyz", - "dlscord-halloween.ru", - "dlscord-hypesquad.com", - "dlscord-hypesquads.com", - "dlscord-inventory.fun", - "dlscord-nitro.click", - "dlscord-nitro.fun", - "dlscord-nitro.info", - "dlscord-nitro.link", - "dlscord-nitro.ru.com", - "dlscord-nitro.space", - "dlscord-nitro.store", - "dlscord-nltro.com", - "dlscord-nltro.ru", - "dlscord-nltro.xyz", - "dlscord-promo.xyz", - "dlscord-spooky.ru", - "dlscord-steam.com", - "dlscord-stime-2021.ru", - "dlscord-store.club", - "dlscord-support.com", - "dlscord.app", - "dlscord.art", - "dlscord.blog", - "dlscord.cc", - "dlscord.click", - "dlscord.cloud", - "dlscord.fr", - "dlscord.gg", - "dlscord.gifts", - "dlscord.in", - "dlscord.info", - "dlscord.ink", - "dlscord.live", - "dlscord.net", - "dlscord.online", - "dlscord.org", - "dlscord.press", - "dlscord.pro", - "dlscord.rocks", - "dlscord.ru.com", - "dlscord.shop", - "dlscord.site", - "dlscord.space", - "dlscord.store", - "dlscord.support", - "dlscord.team", - "dlscord.tech", - "dlscord.tips", - "dlscord.wiki", - "dlscord.world", - "dlscordapp.codes", - "dlscordapp.com", - "dlscordapp.fun", - "dlscordapp.info", - "dlscordapp.pw", - "dlscordapp.ru", - "dlscordapp.store", - "dlscordapps.com", - "dlscordboost.com", - "dlscordd.ru", - "dlscordfull.ru", - "dlscordgift.com", - "dlscordgift.shop", - "dlscordgived.xyz", - "dlscordglft.xyz", - "dlscordglfts.xyz", - "dlscordniltro.com", - "dlscordnitro.com", - "dlscordnitro.info", - "dlscordnitro.ru.com", - "dlscordnitro.ru", - "dlscordnitro.store", - "dlscordnitro.us", - "dlscordnitrofree.com", - "dlscordnitros.gifts", - "dlscordnltro.gifts", - "dlscordnltro.online", - "dlscordnltro.ru", - "dlscordrglft.xyz", - "dlscords.gifts", - "dlscords.site", - "dlscordsgift.xyz", - "dlscordsglfts.xyz", - "dlscordsream.pp.ua", - "dlscordsteam.com", - "dlscorldnitro.store", - "dlscorp.com", - "dlscors.gift", - "dlscourd.info", - "dlscrod-app.xyz", - "dlscrod-game.ru", - "dlscrod-gift.com", - "dlscrod.ru.com", - "dlscrodapp.ru", - "dlsordnitro.gifts", - "dlsordnltros.gifts", - "dmarkef.com", - "dmarket-place.pp.ua", - "dmcordsteamnitro.de", - "dnitrogive.com", - "doatgiveaway.top", - "does-small.ru.com", - "dogewarrior-giveaway.info", - "dola.pp.ua", - "domineer.pp.ua", - "dominosllc.com", - "dominospizza-nl.com", - "dominospizzanl.com", - "dopeskins.com", - "doscord.com", - "doscordapp.com", - "dota2fight.net", - "dota2fight.ru", - "dota2giveaway.top", - "dota2giveaways.top", - "dotacommunitu.xyz", - "dotafights.vip", - "dotagift01.xyz", - "dotagift07.xyz", - "dotagift11.xyz", - "dotagift12.xyz", - "dotagift13.xyz", - "dotagift14.xyz", - "dotagift15.xyz", - "dotagiveaway.win", - "douyutv.ru", - "dragon-black.net.ru", - "dragon-up.online", - "dragonary-giveaway.info", - "dreamhacks-fort.site", - "dripa-discord.com", - "driscord.ru.com", - "driscord.ru", - "dro-coad.ru", - "drop-key.ru", - "drop-nitro.com", - "drop-nitro.fun", - "drop-pro.com", - "drop.net.ru", - "drop.org.ru", - "drop.pp.ru", - "dropkeygood.ml", - "drops4all.pp.ru", - "dropskey.com", - "dropskey.ru", - "dropskin.monster", - "drumairabubakar.com", - "ds-nitr.xyz", - "ds-nitro.com", - "ds-nitro.site", - "dscord-generaot.store", - "dscord.gifts", - "dscord.me", - "dscord.nl", - "dscord.xyz", - "dscordapp.com", - "dscordnitro.xyz", - "dscrd.club", - "dsctnitro.site", - "dsicord.gift", - "dsicrod.com", - "dsiscord.com", - "dsnitro.xyz", - "duiscord.com", - "dumdumdum.ru", - "duscord.com", - "duscord.js.org", - "dwaynejon.xyz", - "dwny.org", - "dxiscord.com", - "dzscord.js.org", - "e-giftpremium.com", - "ea-case.com", - "ea-drop.com", - "each-tel.xyz", - "earnskinz.xyz", - "easy-box.site", - "easycases.pw", - "easyopeningpay.online", - "easyopeningpay.ru", - "eazy-game.online", - "eazy-game.ru", - "eazydrop.monster", - "ecnhasports.ru", - "ecyber-tournament.ru", - "ecyber-versus.ru", - "egamerscup.club", - "emeraldbets.ru", - "en-roblox.com", - "ence.net.ru", - "encebrand.xyz", - "encecsport.me", - "encegun.xyz", - "encesports.xyz", - "enceteam.me", - "enceteam.org.ru", - "encewatch.ru", - "epic-request.xyz", - "epicfriendis.xyz", - "epicfriennd.xyz", - "epicgamees.xyz", - "epicgamesnitro.com", - "epicgamess.xyz", - "epicgammes.xyz", - "epicgamnes.xyz", - "epicganmes.xyz", - "epicggames.site", - "epicggames.xyz", - "epicinvite.xyz", - "epicjames.xyz", - "epickgames.xyz", - "epicqames.xyz", - "epicqannes.xyz", - "epicservic.xyz", - "epicservise.xyz", - "epilcgames.xyz", - "epiqgames.xyz", - "eplcgames.xyz", - "eplcups.com", - "eplicgames.xyz", - "eqiccames.xyz", - "eqicgames.xyz", - "esea-mdl.com", - "esl-2020.com", - "esl-drop.com", - "esl-eu.com", - "esl-gamingnetwork.com", - "esl-gamingseries.com", - "esl-lv.com", - "esl-pl.com", - "esl-playglobal.net", - "esl-pro-legue.xyz", - "esl-proleague.net", - "eslcup.xyz", - "eslgamescommunity.com", - "eslgamesworldwide.com", - "eslgaming-play.com", - "eslgaming-world.com", - "eslgamingnetworks.com", - "eslgamingopen.com", - "eslgamingworldwide.net", - "eslhub.xyz", - "eslhubgaming.com", - "eslplaynetworks.com", - "eslplayoneleague.com", - "eslplayworlds.com", - "eslpro.ru", - "eslquickseries.com", - "eslsports.ru", - "eslworldwideplay.com", - "esportgaming.ru", - "esportgift.ru", - "esportpoinl.xyz", - "esportpoint.xyz", - "esports-2go.pp.ua", - "esports-csgo.ru", - "esports-sale.ru", - "esports-trade.net.ru", - "esportscase.online", - "esportscase.ru", - "esportsfast.pp.ua", - "esportsgvay.xyz", - "esportsi.xyz", - "espots-csgo.xyz", - "essenseglow.com", - "etsdrop.monster", - "etssdrop.monster", - "event-discord.com", - "event-games4roll.com", - "events-discord.com", - "evmcups.ru", - "ewqdsa.xyz", - "exaltedbot.xyz", - "exchangeuritems.gq", - "explorerblocks.com", - "extraskinscs.xyz", - "ez-tasty.cyou", - "ezcase.xyz", - "ezclrop.ru", - "ezdiscord.xyz", - "ezdrop.net.ru", - "ezdropss.net.ru", - "ezdrp.ru", - "ezopen.site", - "ezpudge.pp.ua", - "ezwin24.ru", - "ezwithcounter.xyz", - "ezzrun.pp.ua", - "facecup.fun", - "facedrop.one", - "faceit-premium.com", - "faceiteasyleague.ru", - "faceiten.info", - "facepunch-award.com", - "facepunch-gifts.org.ru", - "facepunch-llc.com", - "facepunch-ltd.com", - "facepunch-reward.com", - "facepunch-studio.com", - "facepunch-studio.us", - "facepunch-twitch.com", - "facepunchltd.com", - "facepunchs.com", - "facepunchskins.com", - "facepunchstudio.com", - "facerit.com", - "faceuinuu.com", - "faceuinuz.com", - "faceuinuz.org.ru", - "faceuinuz.ru.com", - "fai-ceite.info", - "faiceit.ru.com", - "fall500.ru", - "fang-operation.ru", - "fannykey.ru", - "farestonpw.ru.com", - "faritkoko.ru", - "farkimagix.xyz", - "fartik.net.ru", - "fasdf.pp.ua", - "fast-cup.site", - "fastcup.ru.com", - "fastcups.xyz", - "fastdrop.win", - "fastgotournaments.xyz", - "fastlucky.ru.com", - "fastlucky.ru", - "fastskins.ru", - "fasttake.space", - "fatown.net", - "fdiscord.com", - "ff.soul-ns.xyz", - "fineleague.fun", - "fineplay.xyz", - "fireopencase.com", - "firtonesroll.ru.com", - "fiscord.com", - "fivetown.net", - "flyes-coin.com", - "fnaatic.org.ru", - "fnatcas.org.ru", - "fnatic-2021.ru", - "fnatic-drop.com", - "fnatic-gg.fun", - "fnatic-go.fun", - "fnatic-ro1ls.ru.com", - "fnatic-s.fun", - "fnatic-team.ru", - "fnatic-time.ru", - "fnatic.pp.ru", - "fnatic.team", - "fnatic1.org.ru", - "fnatic2.org.ru", - "fnaticez.me", - "fnaticforyou.xyz", - "fnaticgit.xyz", - "fnaticteam.org.ru", - "fnnatic.org.ru", - "fnnaticc.org.ru", - "fntc-bd.pp.ua", - "follow-ask.xyz", - "forcedope.xyz", - "forest-host.ru", - "formulaprize.com", - "fornite.best", - "forse-pash.pp.ru", - "forse-wash.pp.ru", - "forsportss.pp.ua", - "fortnight.space", - "fortnite-newswapper.fun", - "fortnite.sswapper.com", - "fortnitebuy.com", - "fortnitecrew.ru.com", - "fortniteswapper.fun", - "fortuneroll.tk", - "fowephwo.ru", - "foxycyber.ru", - "fozzytournaments.fun", - "fplgo.ru", - "fps-booster.pw", - "fr33item.xyz", - "free-discord.ru", - "free-dislcordnitrlos.ru", - "free-niltross.ru", - "free-nitlross.ru", - "free-nitro-sus.pages.dev", - "free-nitro.ru", - "free-nitroi.ru", - "free-nitros.ru", - "free-skins.ru", - "freediscord-nitro.cf", - "freediscordnitro.ru", - "freediscrodnitro.org", - "freediskord-nitro.xyz", - "freedrop0.xyz", - "freefireclaim.club", - "freeinstagramfollowersonline.com", - "freenetflix.io", - "freenitro.ru", - "freenitrogenerator.cf", - "freenitrogenerator.tk", - "freenitroi.ru", - "freenitrol.ru", - "freenitros.com", - "freenitros.ru", - "freenitros.tk", - "freenltro.ru", - "freerobloxgenerator.tk", - "freeskins.online", - "freeskinsfree.pp.ua", - "freespoty.com", - "from-eliasae.ru.com", - "from-puste.xyz", - "from-sparsei.ru.com", - "from-surenseds.xyz", - "ftp.celerone.cf", - "ftp.copyrighthelpbusiness.org", - "ftp.def-dclss.pp.ua", - "ftp.domineer.pp.ua", - "ftp.fasdf.pp.ua", - "ftp.ghostgame.ru", - "ftp.gooditems.pp.ua", - "ftp.greatdrops.pp.ua", - "ftp.legasytour.it", - "ftp.navieslproleagueseason13.pp.ua", - "ftp.ogevtop.ru", - "ftp.scogtopru.pp.ua", - "ftp.steamcommunlty.it", - "ftp.topeasyllucky.pp.ua", - "ftp.versuscsgoplay.pp.ua", - "fulldiscord.com", - "funchest.fun", - "fundro0p.site", - "funjet1.ru.com", - "funnydrop.store", - "furtivhqqc.com", - "furyesports.xyz", - "furyleage.xyz", - "fustcup.ru", - "g-games.store", - "g1veaway-nav1.site", - "g2-cybersport.net", - "g2-cybersport.ru", - "g2-cybersports.net", - "g2-esports.moscow", - "g2-game.ru", - "g2-give.info", - "g2-give.ru", - "g2-pro.shop", - "g2a.ru.com", - "g2cyber-espots.top", - "g2cybergame.fun", - "g2eref.ru", - "g2ezports.xyz", - "g2team-give.top", - "g2team.org", - "g2teams.com", - "g2teamss.ru", - "gaben-seller.pp.ua", - "gamaloft.xyz", - "gambit-cs.com", - "gambit.net.ru", - "gambit.org.ru", - "gambitesports.me", - "gambling1.ru.com", - "gambling1.ru", - "gamdom.ru", - "game-case.ru", - "game-csgo-steam.ru", - "game-csgosteam.ru", - "game-sense.space", - "game-steam-csgo.ru", - "game-steamcsgo.ru", - "game-tournaments.net.ru", - "game-tournaments.ru.com", - "game.schweitzer.io", - "game4roll.com", - "gameb-platform.com", - "gamecsgo-steam.ru", - "gamegowin.xyz", - "gamekere.net.ru", - "gamekor.net.ru", - "gameluck.ru", - "gamemaker.net.ru", - "gamepromo.net.ru", - "gamerich.xyz", - "gameroli.net.ru", - "gamerolls.net.ru", - "games-code.ru.com", - "games-roll.ga", - "games-roll.ml", - "games-roll.ru", - "gamesbuy.net.ru", - "gamesfree.org.ru", - "gamespol.net.ru", - "gamzc-topz.xyz", - "gamzgss-top.org.ru", - "gamzgss-top.xyz", - "garstel.github.io", - "gave-nitro.com", - "gavenitro.com", - "gbauthorization.com", - "gdiscord.com", - "gdr-op.ru.com", - "generator.discordnitrogift.com", - "get-discord.fun", - "get-gamesroll.xyz", - "get-my-nitro.com", - "get-nitro.com", - "get-nitro.fun", - "get-nitro.net", - "get-traded.xyz", - "get.sendmesamples.com", - "getautomendpro.com", - "getcach.monster", - "getfitnos.com", - "getfreediscordnitro.ml", - "getnaturetonics.com", - "getnitro.xyz", - "getnitrogen.org", - "getproviamax.com", - "getriptide.live", - "getskins.monster", - "getstratuswatch.com", - "getv-bucks.site", - "getyouritems.pp.ua", - "gfrtwgfkgc.xyz", - "gg-dr0p.ru", - "ggbolt.ru", - "ggboom.ru", - "ggdrop-gg.xyz", - "ggdrop.org.ru", - "ggdrop.pp.ru", - "ggdrop.space", - "ggdrop1.net.ru", - "ggdrops.net.ru", - "ggdrops.ru.com", - "ggexpert.online", - "ggexpert.ru", - "ggfail.xyz", - "gglootgood.xyz", - "ggnatus.com", - "ggnavincere.xyz", - "ggtour.ru", - "ghostgame.ru", - "gif-discord.com", - "gife-discorde.com", - "gift-discord.online", - "gift-discord.ru", - "gift-discord.shop", - "gift-discord.xyz", - "gift-discords.com", - "gift-g2.online", - "gift-g2.ru", - "gift-nitro.store", - "gift4keys.com", - "giftc-s.ru", - "giftcsogg.ru", - "giftdiscord.info", - "giftdiscord.online", - "giftes-discord.com", - "giftnitro.space", - "giftsdiscord.com", - "giftsdiscord.fun", - "giftsdiscord.online", - "giftsdiscord.ru", - "giftsdiscord.site", - "givaeway.com", - "givaewey.com", - "giveavvay.com", - "giveaway-fpl-navi.net.ru", - "giveaway-fpl.net.ru", - "giveawaynitro.com", - "giveawayskin.com", - "giveaweys.com", - "giveeawayscin.me", - "givenatus.site", - "giveprize.ru", - "giveweay.com", - "givrayawards.xyz", - "glaem.su", - "gleam.su", - "glets-nitro.com", - "glft-discord.com", - "glob21.online", - "globacs.monster", - "global-skins.gq", - "globalcs.monster", - "globalcss.monster", - "globalcsskins.xyz", - "globalmoestro.ru", - "globalskins.tk", - "gnswebservice.com", - "go-cs.ru.com", - "go-cups.ru", - "go.rancah.com", - "go.thefreedailyraffle.com", - "go2-rush.pp.ua", - "go4you.ru", - "gocs8.ru.com", - "gocs8q.ru", - "gocs8v.ru.com", - "gocsx.ru", - "gocsx8.ru", - "gocups.ru", - "godssale.ru", - "goldendota.com", - "goman.ru.com", - "good-csgo-steam.ru", - "gooditems.pp.ua", - "goodskins.gq", - "gool-lex.org.ru", - "gosteamanalyst.com", - "great-drop.xyz", - "greatdrops.pp.ua", - "greatgreat.xyz", - "greenwisedebtrelief.com", - "gtakey.ru", - "gtwoesport-battle.ru", - "guardian-angel.xyz", - "guns-slot.tk", - "halitaoz.cam", - "hallowen-nitro.com", - "haste.monster", - "hdiscord.com", - "hdiscordapp.com", - "hellcase.net.ru", - "hellgiveaway.trade", - "hellstorecoin.site", - "hellstores.xyz", - "help-center-portal.tk", - "help.usabenefitsguide.com", - "help.usalegalguide.com", - "help.verified-badgeform.tk", - "heroic-esports.ru", - "hjoiaeoj.ru", - "hltvcsgo.com", - "hltvgames.net", - "holyawards.xyz", - "hope-nitro.com", - "horizon-up.org.ru", - "horizonup.ru", - "hornetesports.xyz", - "host322.ru", - "howl.monster", - "howls.monster", - "httpdlscordnitro.ru.com", - "humanlifeof.xyz", - "humnchck.co", - "hunts.monster", - "huracancsgo.tk", - "huyatv.ru", - "hydra2018.ru", - "hype-chat.ru", - "hyper-tournament.xyz", - "hypercups.ru", - "hypertracked.com", - "hyperz.monster", - "id-374749.ru", - "idchecker.xyz", - "idealexplore.com", - "idiscord.pro", - "iemcup.com", - "imvu37.blogspot.com", - "in-gives.ru.com", - "indereyn.ru.com", - "information-discord.com", - "inteledirect.com", - "intimki.com", - "into-nitro.com", - "inventtop.com", - "isp3.queryhost.ovh", - "itemcloud.one", - "iwinner.ru.com", - "jet-crash.xyz", - "jetcase.fun", - "jetcase.ru.com", - "jetscup.ru", - "jjdiscord.com", - "joewfpwg.ru", - "jokedrop.ru", - "jope-nitro.com", - "joyskins.xyz", - "juct-case.ru", - "just-roll.ru", - "justcase.net.ru", - "justcause.fun", - "justdior.com", - "justwins.ru", - "kahiotifa.ru", - "kambol-go.ru", - "kaspi-capital.com", - "katowice.ru", - "katowlce.ru", - "kaysdrop.ru", - "key-dr0b.com", - "key-dr0p.com", - "key-drcp.com", - "key-drop-free.com", - "key-dropo.com", - "keydoppler.one", - "keydorp.me", - "keydrop.guru", - "keydrop.org.ru", - "keydrop.ru.com", - "keydropp.one", - "keydrops.xyz", - "keydrup.ru", - "keys-dropes.com", - "keys-loot.com", - "keysdropes.com", - "kievskiyrosdachy-ua.ru", - "kingofqueens2021.github.io", - "kirakiooi.xyz", - "kkgdrops.monster", - "knife-eazy.pp.ua", - "knifespin.top", - "knifespin.xyz", - "knifespins.xyz", - "knifex.ru.com", - "knifez-roll.xyz", - "knifez-win.xyz", - "knmirjdf.ru", - "konicpirg.com", - "kr1ks0w.ru", - "kredo-capital.com", - "ksgogift.pp.ua", - "ksodkcvm.ru", - "l0d4b860.justinstalledpanel.com", - "l1568586.justinstalledpanel.com", - "l23682ce.justinstalledpanel.com", - "l3a32c23.justinstalledpanel.com", - "l4a13998.justinstalledpanel.com", - "l4bbc943.justinstalledpanel.com", - "l95614b0.justinstalledpanel.com", - "l9f009d3.justinstalledpanel.com", - "la622566.justinstalledpanel.com", - "la76c010.justinstalledpanel.com", - "labfbb02.justinstalledpanel.com", - "lakskuns.xyz", - "lan-pro.fun", - "lan-pro.link", - "lan-pro.ru", - "lan-pro.xyz", - "lb4b95f8.justinstalledpanel.com", - "lb6469d3.justinstalledpanel.com", - "lb9d00fb.justinstalledpanel.com", - "lbd74bef.justinstalledpanel.com", - "lc995e52.justinstalledpanel.com", - "lcb2f337.justinstalledpanel.com", - "ld54d414.justinstalledpanel.com", - "ldb9f474.justinstalledpanel.com", - "ldiscord.gift", - "ldiscordapp.com", - "le491879.justinstalledpanel.com", - "league-csgo.com", - "legasytour.it", - "lehatop-01.ru", - "lemesports.ru", - "lf4d4257.justinstalledpanel.com", - "lf5d73bb.justinstalledpanel.com", - "lfa90cb7.justinstalledpanel.com", - "lfd0d93c.justinstalledpanel.com", - "lifegg.xyz", - "linktrade.pp.ua", - "listycommunity.ru", - "litenavi.xyz", - "lkdiscord.com", - "loginprofile.xyz", - "loginrun.info", - "longxrun.online", - "loot-conveyor.com", - "loot-item.xyz", - "loot-rust.com", - "loot.net.ru", - "loot.pp.ru", - "loot4fun.ru", - "lootmake.com", - "lootship.ga", - "lootshunt.org.ru", - "lootsrow.com", - "lootxmarket.com", - "loungeztrade.com", - "low-cups.ru", - "lozt.pp.ua", - "luancort.com", - "lucky-skins.xyz", - "luckycrush.ga", - "luckydrop.site", - "luckyfast.ru.com", - "luckyfast.ru", - "luckygift.net.ru", - "luckygift.space", - "luckygo.ru.com", - "luckygo.ru", - "luckyiwin.ml", - "luckyiwin.tk", - "luxace.ru.com", - "luxerkils.xyz", - "m-discord.pw", - "m.setampowered.com", - "m90694rb.beget.tech", - "made-nitro.com", - "madessk.pp.ua", - "maggicdrop.xyz", - "magic-delfy.net.ru", - "magicdropgift.ru", - "magicdropnew.xyz", - "magicrollslg.com.ru", - "magicrollslw.com.ru", - "magicroulete.ru", - "magicrun.site", - "magictop.ru.com", - "magifcrolrlc.xyz", - "magifcrolrlh.xyz", - "magifrolbiq.xyz", - "magifrolbit.xyz", - "magik-dr0p.fun", - "magikbrop.xyz", - "magnaviroll.xyz", - "magnavirolls.xyz", - "magnavirollz.xyz", - "mail.celerone.cf", - "mail.csgoroll.ru", - "mail.dicsord-airdrop.ru", - "mail.explorerblocks.com", - "mail.fasdf.pp.ua", - "mail.ghostgame.ru", - "mail.gooditems.pp.ua", - "mail.ogevtop.ru", - "mail.scogtopru.pp.ua", - "mail.streamcomuniity.pp.ua", - "mail.versuscsgoplay.pp.ua", - "majestictips.com", - "major-2021.ru", - "makson-gta.ru", - "malibones.buzz", - "marke-tcgo.ru.com", - "marke-tgo.ru.com", - "market-csgo.ru", - "market-subito.site", - "marketsleam.xyz", - "marketsm.pp.ua", - "markt-csgo.ru.com", - "markt-csru.info", - "marktcsgo.ru.com", - "mars-cup.ru", - "master-up.ru", - "maxskins.xyz", - "mcdaonlds.com", - "mcdelivery-offer.com", - "mcdelivery-sale.com", - "mcdelivery24.com", - "mcdonalds-iloveit.com", - "mcdonalds-saudiarabia.com", - "mcdonaldsau.info", - "mdiscord.com", - "medpatrik.ru", - "megacase.monster", - "mekaverse-minting.com", - "mekaversecollection.com", - "mekaversenft.net", - "microsup.net", - "minea.club", - "moderationacademy-exams.com", - "mol4a.pp.ua", - "money.fastcreditmatch.com", - "money.usacashfinder.com", - "mvcsgo.com", - "mvpcup.ru", - "mvptournament.com", - "my-trade-link.ru", - "my-tradelink.ru", - "myccgo.xyz", - "mychaelknight.com", - "mycsgoo.ru", - "mydrop.monster", - "myfast.ru", - "mygames4roll.com", - "myjustcase.ru", - "myrolls.monster", - "myrollz.com", - "mythic-esports.xyz", - "mythiccups.xyz", - "mythicleagues.xyz", - "mythicups.xyz", - "myticks.xyz", - "mytrade-link.ru.com", - "mytradelink.pp.ua", - "mytradelink.ru.com", - "mytradeoffers.ru.com", - "nacybersportvi.ru", - "nagipen.ru", - "nagiver.ru", - "naturespashowerpurifier.com", - "natus-lootbox.net.ru", - "natus-lootbox.org.ru", - "natus-open.net.ru", - "natus-open.org.ru", - "natus-open.pp.ru", - "natus-rolls.xyz", - "natus-space.ru", - "natus-spot.net.ru", - "natus-spot.pp.ru", - "natus-vincere.ru", - "natus-vincere.space", - "natus-vincere.xyz", - "natus-vincery-majors.ru.com", - "natus-vincerygive.xyz", - "natus-vincerygivess.xyz", - "natus-vincerygivesz.xyz", - "natus-vincerygivex.xyz", - "natus-vincerygivezc.xyz", - "natus-vincerygivezr.ru", - "natus-vincerygivezz.xyz", - "natus-win.net.ru", - "natus-win.org.ru", - "natus-win.pp.ru", - "natusforyou.pp.ua", - "natusspot.pp.ru", - "natustop.net.ru", - "natustop.org.ru", - "natusvincerbestmarket.work", - "natusvinceredrop.ru", - "natuswin.org.ru", - "nav-s1.ru", - "navi-21.ru", - "navi-bp.com", - "navi-cis.net.ru", - "navi-cs.com", - "navi-drop.net", - "navi-drop2020.com", - "navi-es.ru", - "navi-esl.ru.com", - "navi-esports.net", - "navi-eu.ru", - "navi-ez.com", - "navi-freedrop.xyz", - "navi-freeskins.com", - "navi-give.net.ru", - "navi-giveaway-simple.net.ru", - "navi-giveaway.net", - "navi-giveaway.xyz", - "navi-gs.com", - "navi-gt.com", - "navi-gv.com", - "navi-hawai.net.ru", - "navi-io.com", - "navi-keep.net.ru", - "navi-lix.xyz", - "navi-ls.com", - "navi-lzx.ru", - "navi-off.us", - "navi-ol.com", - "navi-q.com", - "navi-rt.com", - "navi-russia.ru", - "navi-share.pp.ru", - "navi-skins.org.ru", - "navi-skins.pp.ru", - "navi-sp.com", - "navi-tm.com", - "navi-tm.ru", - "navi-up.com", - "navi-up.ru", - "navi-winners.org.ru", - "navi-wins-skiins.org.ru", - "navi-x.ru", - "navi-youtube.net.ru", - "navi.pp.ru", - "navi2021.net.ru", - "naviback.ru", - "navibase.net.ru", - "navibase.org.ru", - "navibase.pp.ru", - "navicase-2020.org.ru", - "navicase.org", - "navicsg.ru", - "navidonative.ru", - "naviend.xyz", - "navieslproleagueseason13.pp.ua", - "naviesport.net", - "naviesportsgiveaways.pro", - "navifree.ru", - "navifreeskins.ru", - "navifun.me", - "navigg.org.ru", - "navigg.ru", - "naviggcoronagiveaway.ru", - "navigiveaway.ru", - "navign.me", - "navigs.ru", - "navileague.xyz", - "navination.site", - "navipodarok.ru", - "navipresent.xyz", - "naviqq.org.ru", - "navirolls.org.ru", - "navishare.net.ru", - "navishare.pp.ru", - "naviskins.xyz", - "naviteam.net.ru", - "naviteamway.net.ru", - "navitm.ru", - "navvigg.site", - "navviigg.ru", - "navy-freecases.ru", - "navy-loot.xyz", - "nawegate.com", - "nawi-gw.ru", - "nawibest.ru.com", - "nawigiveavay.xyz", - "netfllix-de.com", - "new-collects.xyz", - "new-drop.net.ru", - "new-offer.trade", - "new-steamcommunlty.xyz", - "new.mychaelknight.com", - "newdiscord.online", - "nice-haesh-info.ru", - "nicegg.ru", - "night-skins.com", - "nightz.monster", - "nise-cell.net.ru", - "nise-gell.org.ru", - "nise-well.org.ru", - "nise-win.xyz", - "nitrlooss-free.ru", - "nitro-airdrop.org", - "nitro-all.xyz", - "nitro-app.com", - "nitro-app.fun", - "nitro-discord.fun", - "nitro-discord.info", - "nitro-discord.me", - "nitro-discord.org", - "nitro-discord.ru.com", - "nitro-discordapp.com", - "nitro-discords.com", - "nitro-drop.com", - "nitro-ds.xyz", - "nitro-for-free.com", - "nitro-from-steam.com", - "nitro-gift.ru.com", - "nitro-gift.ru", - "nitro-gift.site", - "nitro-gift.space", - "nitro-gift.store", - "nitro-gift.xyz", - "nitro-give.site", - "nitro-up.com", - "nitro.gift", - "nitroairdrop.com", - "nitroappstore.com", - "nitrochallange.com", - "nitrodiscord.org", - "nitrodlscordl.xyz", - "nitrodlscordx.xyz", - "nitrofgift.xyz", - "nitrofrees.ru", - "nitrogeneral.ru", - "nitrogift.xyz", - "nitrogive.com", - "nitroos-frieie.ru", - "nitroosfree.ru", - "nitropussy.com", - "nitros-gift.com", - "nitrostore.org", - "nitrotypehack.club", - "nltro.site", - "ns1.dns-soul.wtf", - "ns1.dropc.me", - "ns1.navitry.me", - "ns1.peektournament.me", - "ns2.dropc.me", - "ns2.helpform-center.ml", - "nur-electro-05.ml", - "nv-pick.com", - "nvcontest.xyz", - "nwgwroqr.ru", - "offerdealstop.com", - "official-nitro.com", - "official-nitro.fun", - "ogevtop.ru", - "ogfefieibio.ru", - "okdiscord.com", - "oligarph.club", - "onehave.xyz", - "open-case.work", - "opencase.space", - "operation-broken.xyz", - "operation-pass.ru.com", - "operation-riptide.link", - "operation-riptide.ru.com", - "operation-riptide.xyz", - "operationbroken.xyz", - "operationreptide.com", - "operationriptide.tk", - "opinionshareresearch.com", - "order-40.com", - "order-78.com", - "order-87.com", - "order-96.com", - "orderpropods.com", - "ornenaui.ru", - "out-want.xyz", - "output-nitro.com", - "overdrivsa.xyz", - "ovshau.club", - "ownerbets.com", - "p.t67.me", - "paayar.info", - "pandakey.ru", - "pandaskin.ru.com", - "pandaskins.ru.com", - "pandemidestekpaket.cf", - "passjoz.net.ru", - "path.shareyourfreebies.com", - "path.topsurveystoday.com", - "patrool.net.ru", - "pay-18.info", - "payeaer.xyz", - "payear.xyz", - "payeer.life", - "payeer.live", - "payeer.vip", - "pingagency.ru", - "pizzaeria-papajohns.com", - "playcsgo-steam.ru", - "playerskinz.xyz", - "playeslseries.com", - "please.net.ru", - "pltw.com", - "pluswin.ru", - "pluswsports.ru", - "poloname.net.ru", - "pop.ghostgame.ru", - "pop.ogevtop.ru", - "pose1dwin.ru", - "poste.xyz", - "power-sk1n.net.ru", - "ppayeer.ru.com", - "ppayeer.ru", - "prajyoth-reddy-mothi.github.io", - "prajyoth.me", - "prefix.net.ru", - "premium-discord.com", - "premium-discords.com", - "premium-faceit.com", - "premiums-discord.com", - "price-claim.xyz", - "prime-drop.xyz", - "privatexplore.com", - "privatkeyblok.com", - "prizee-good.com", - "profile-2994292.ru", - "profile-442572242.online", - "profiles-7685291049068.me", - "promo-codes.world", - "promo-discord.com", - "promo-discord.site", - "proz.monster", - "psyonix-trade.online", - "psyonix.website", - "psyonlxcodes.com", - "ptbdiscord.com", - "pubg-asia.xyz", - "pubg-steamcommunityyz.top", - "pubg.network", - "pubg.new-collects.xyz", - "pubgclaims.com", - "pubge21.xyz", - "pubgfree77.com", - "pubgfreedownload.org", - "pubgfreeeus.cf", - "pubggf01.xyz", - "pubggf02.xyz", - "pubggf03.xyz", - "pubggf04.xyz", - "pubggf05.xyz", - "pubggf06.xyz", - "pubggf10.xyz", - "pubggf15.xyz", - "pubggf16.xyz", - "pubggf17.xyz", - "pubggf18.xyz", - "pubggf19.xyz", - "pubggf20.xyz", - "pubggf21.xyz", - "pubggf22.xyz", - "pubggf23.xyz", - "pubggf24.xyz", - "pubggf25.xyz", - "pubggf26.xyz", - "pubggf27.xyz", - "pubggf28.xyz", - "pubggf29.xyz", - "pubggf30.xyz", - "pubggf31.xyz", - "pubggf32.xyz", - "pubggf33.xyz", - "pubggf34.xyz", - "pubggf35.xyz", - "pubggf36.xyz", - "pubggf37.xyz", - "pubggf38.xyz", - "pubggf39.xyz", - "pubggf40.xyz", - "pubggf41.xyz", - "pubggf42.xyz", - "pubggift100.xyz", - "pubggift101.xyz", - "pubggift102.xyz", - "pubggift31.xyz", - "pubggift32.xyz", - "pubggift48.xyz", - "pubggift56.xyz", - "pubggift58.xyz", - "pubggift59.xyz", - "pubggift60.xyz", - "pubggift61.xyz", - "pubggift62.xyz", - "pubggift63.xyz", - "pubggift64.xyz", - "pubggift65.xyz", - "pubggift66.xyz", - "pubggift67.xyz", - "pubggift68.xyz", - "pubggift69.xyz", - "pubggift70.xyz", - "pubggift71.xyz", - "pubggift87.xyz", - "pubggift91.xyz", - "pubggift92.xyz", - "pubggift93.xyz", - "pubggift94.xyz", - "pubggift95.xyz", - "pubggift96.xyz", - "pubggift97.xyz", - "pubggift98.xyz", - "pubggift99.xyz", - "pubgmcheats.com", - "pubgmobile2019ucfreeeee.tk", - "pubgmobile365.com", - "pubgmobile365.giftcodehot.net", - "pubgmobile737373.ml", - "pubgmobileskin2020.com", - "pubgmobilespro.my.id", - "pubgmobileuc2020free.cf", - "pubgofficielbcseller.online", - "pubgtoken.io", - "pubguccmobilefree.cf", - "qbt-giveaway.info", - "qcold.club", - "qcoldteam.life", - "qtteddybear.com", - "quantumtac.co", - "quick-cup.xyz", - "quickrobux.net", - "r-andomfloat.ru", - "rainorshine.ru", - "ran-getto.org.ru", - "rangskins.com", - "rave-clup.ru", - "rave-new.ru", - "rblxcorp.work", - "rbux88.com", - "rbux88go.com", - "rdr2code.ru", - "realskins.xyz", - "realtorg.xyz", - "redirectednet.xyz", - "redizzz.xyz", - "rednance.com", - "redskin.monster", - "reports.noodlesawp.ru", - "reslike.net", - "rewardbuddy.me", - "rewards-rl.com", - "rewardsavenue.net", - "rewardspremium-nitro.gq", - "rien.xyz", - "rip-tide.ru", - "ripetide.ru", - "riptid-operation.ru", - "riptide-cs.com", - "riptide-cs.ru", - "riptide-csgo.ru", - "riptide-free-pass.net.ru", - "riptide-free-pass.org.ru", - "riptide-free-pass.pp.ru", - "riptide-gaming.ru", - "riptide-operation.com", - "riptide-operation.ru.com", - "riptide-operation.ru", - "riptide-operation.xyz", - "riptide-operations.ru", - "riptide-pass.org.ru", - "riptide-take.ru", - "riptide-valve.ru", - "riptidefree.ru", - "riptiden.ru", - "riptideoffer.ru", - "riptideoperation.xyz", - "riptidepass.net.ru", - "riptidepass.ru", - "rl-activate.com", - "rl-award.com", - "rl-bounce.com", - "rl-change.ru", - "rl-chaser.com", - "rl-code.com", - "rl-diamond.com", - "rl-epic.com", - "rl-fandrops.com", - "rl-fanprize.com", - "rl-fast.com", - "rl-fastrading.com", - "rl-garage.info", - "rl-garage.online", - "rl-garage.space", - "rl-give.ru.com", - "rl-insidergift.com", - "rl-performance.com", - "rl-positive.com", - "rl-promocode.com", - "rl-promos.com", - "rl-purple.com", - "rl-retail.fun", - "rl-rewards.ru.com", - "rl-tracking.pro", - "rl-traders.com", - "rlatracker.com", - "rlatracker.pro", - "rldrop-gifts.com", - "rldrop.gifts", - "rlexcihnage.com", - "rlgarages.com", - "rlgifts.org", - "rlgtracker.zone", - "rlq-trading.com", - "rlqtrading.com", - "rlshop.fun", - "rlstracker.com", - "rltracken.ru", - "rltrackings.com", - "rlv-trading.com", - "rlz-trading.com", - "robfan.work", - "roblox-collect.com", - "roblox-login.com", - "roblox-porn.com", - "roblox-robux.de", - "roblox.com.so", - "roblox.free.robux.page", - "roblox.help", - "roblox.link.club", - "robloxbing.com", - "robloxdownload.org", - "robloxgamecode.com", - "robloxgiftcardz.com", - "robloxpasssword.com", - "robloxromania.com", - "robloxs.land", - "robloxsecure.com", - "robloxstore.co.uk", - "robloxux.com", - "robloxwheelspin.com", - "robloxxhacks.co", - "robuux1.club", - "robux-codes.ga", - "robux.claimgifts.shop", - "robux20.club", - "robux247.win", - "robux4sex.tk", - "robuxat.com", - "robuxfiends.com", - "robuxfree.us", - "robuxgen.site", - "robuxhach.com", - "robuxhelp.com", - "robuxhelpers.com", - "robuxhelps.com", - "robuxprofiles.com", - "robuxtools.me", - "robuxx.work", - "robx.pw", - "rocket-dealer.com", - "rocket-item.com", - "rocket-leag.com", - "rocket-league.info", - "rocket-retailer.fun", - "rocket-tournament.fun", - "rocket-trader.fun", - "rocket-traders.store", - "rocket-trades.store", - "rocket-trading.site", - "rocket-trading.space", - "rocket-trading.store", - "rocket-tradings.com", - "rocket2pass.com", - "rocketleague-drops.com", - "rocketleagues.site", - "rocketleaque.info", - "rocketradings.com", - "rockets-garages.com", - "rockets-item.com", - "rockets-items.com", - "rockets-sale.com", - "rockets-sales.com", - "rockets-trade.com", - "roleum.buzz", - "roll-gift.fun", - "roll-skins.ga", - "roll-skins.ru", - "roll-skins.tk", - "roll-statedrop.ru", - "roll4knife.xyz", - "roll4tune.com", - "rollcas.ru.com", - "rollgame.net.ru", - "rollkey.ru.com", - "rollknfez.xyz", - "rollskin-simple.xyz", - "rollskin.ru", - "rollskins.monster", - "rollskins.ru", - "rool-skins.xyz", - "roposp12.design", - "roposp14.design", - "ropost15.xyz", - "roulette-prizes.ru.com", - "roulettebk.ru", - "royalegive.pp.ua", - "run2go.ru", - "runwebsite.ru", - "rushbskins.xyz", - "rushskillz.net.ru", - "rushskins.xyz", - "rust-award.com", - "rust-boom.xyz", - "rust-charge.com", - "rust-chest.com", - "rust-code.com", - "rust-code.ru.com", - "rust-codes.com", - "rust-drop.ru.com", - "rust-get.com", - "rust-gitfs.ru", - "rust-giveaways.xyz", - "rust-kit.com", - "rust-llc.com", - "rust-ltd.com", - "rust-reward.com", - "rust-satchel.com", - "rust-skin.com", - "rust.facepunchs.com", - "rustarea.me", - "rustg1ft.com", - "rustg1fts.online", - "rustg1fts.ru", - "rustgame-servers.com", - "rustprize.com", - "rustygift.site", - "rustyit-ems.xyz", - "s-steame.ru", - "s-teame.ru", - "s1cases.site", - "s1cses.site", - "s1mple-give-away.pp.ua", - "s1mple-spin.xyz", - "s1mplesun.design", - "s92673tu.beget.tech", - "sa-mcdonalds.com", - "safe-funds.site", - "said-home.ru.com", - "sakuralive.ru.com", - "sale-steampowered.com", - "savage-growplus.com", - "scale-navi.pp.ru", - "scl-online.ru", - "sclt.xyz", - "scltourments.xyz", - "scogtopru.pp.ua", - "scteamcommunity.com", - "scwanmei.ru", - "sdiscord.com", - "seamcommunity.com", - "seamconmunity.xyz", - "seancommunity.com", - "seancommunlty.ru", - "secure-instagram.ru", - "secure.yourreadytogoproduct.surf", - "seed-nitro.com", - "services.runescape.rs-tt.xyz", - "services.runescape.rs-ui.xyz", - "setamcommunity.com", - "shadowmarket.xyz", - "shadowpay.pp.ru", - "share.nowblox.com", - "shattereddrop.xyz", - "shib.events", - "shimermsc.ru", - "shopy-nitro.tk", - "shroud-cs.com", - "sieamcommunity.net.ru", - "sieamcommunity.org.ru", - "simple-knifez.xyz", - "simple-win.xyz", - "simplegamepro.ru", - "simplegif.ru", - "simpleroll-cs.xyz", - "simplespinz.xyz", - "simplewinz.xyz", - "siriusturnier.pp.ua", - "sitemap.onedrrive.com", - "skill-toom.pp.ru", - "skin-index.com", - "skin888trade.com", - "skincs-spin.top", - "skincs-spin.xyz", - "skincsggtl.xyz", - "skindeyyes.ru", - "skingstgg.ru", - "skingstgo.ru", - "skini-lords.net.ru", - "skinkeens.xyz", - "skinmarkets.net", - "skinnprojet.ru", - "skinpowcs.ru", - "skinpowst.ru", - "skinroll.ru.com", - "skinroll.ru", - "skins-drop.ru", - "skins-hub.top", - "skins-info.net", - "skins-jungle.xyz", - "skins-navi.pp.ru", - "skins.net.ru", - "skins.org.ru", - "skins.pp.ru", - "skins1wallet.xyz", - "skinsbon.com", - "skinsboost.ru", - "skinscsanalyst.ru", - "skinsdatabse.com", - "skinsgo.monster", - "skinsind.com", - "skinslit.com", - "skinsmedia.com", - "skinsmind.ru", - "skinspace.ru", - "skinsplane.com", - "skinsplanes.com", - "skinsplanets.com", - "skinstradehub.com", - "skinsup.monster", - "skinup.monster", - "skinxinfo.net", - "skinxmarket.site", - "skinz-spin.top", - "skinz-spin.xyz", - "skinzjar.ru", - "skinzprize.xyz", - "skinzspin-cs.xyz", - "skinzspinz.xyz", - "sklinsbaron.net", - "sl1pyymyacc.ru", - "slaaeamcrommunity.com.profiles-7685291049068.me", - "sleam-trade.net.ru", - "sleam-trade.org.ru", - "sleam-trade.pp.ru", - "sleamcominnuty.ru", - "sleamcommiinuty.ru", - "sleamcomminity.ru", - "sleamcomminutiycom.ru.com", - "sleamcommmunily.xyz", - "sleamcommmunitiy.ru", - "sleamcommmunity.com", - "sleamcommmuntiy.ru", - "sleamcommnnity.com", - "sleamcommnunity.net", - "sleamcommuiliy.ru.com", - "sleamcommuinity.xyz", - "sleamcommuintiy.ru.com", - "sleamcommuinty.store", - "sleamcommuity.com", - "sleamcommunety.ru", - "sleamcommuniitey.ru.com", - "sleamcommuniity.me", - "sleamcommuniity.ru.com", - "sleamcommuniity.xyz", - "sleamcommuniiy.ru", - "sleamcommunilly.me", - "sleamcommunilly.ru", - "sleamcommunily.net", - "sleamcommunily.org", - "sleamcommunily.ru.com", - "sleamcommuninty.com", - "sleamcommuninty.ru", - "sleamcommuniry.ru", - "sleamcommunitey.com", - "sleamcommuniti.ru", - "sleamcommuniti.xyz", - "sleamcommunitiy.com", - "sleamcommunitty.xyz", - "sleamcommunittyy.me", - "sleamcommunitu.net.ru", - "sleamcommunitu.ru", - "sleamcommunituy.com", - "sleamcommunity.me", - "sleamcommunity.net", - "sleamcommunity.org.ru", - "sleamcommunity.org", - "sleamcommunity.pp.ru", - "sleamcommunityprofiles76561199056426944.ru", - "sleamcommunityy.me", - "sleamcommunlity.xyz", - "sleamcommunlty.net.ru", - "sleamcommunlty.net", - "sleamcommunlty.ru.com", - "sleamcommunlty.space", - "sleamcommunlty.xyz", - "sleamcommunnitu.com", - "sleamcommunnity.net", - "sleamcommunnity.org", - "sleamcommunnity.ru", - "sleamcommuntiny.ru", - "sleamcommuntity.ru", - "sleamcommuntiy.com", - "sleamcommuntly.ru", - "sleamcommunty.com", - "sleamcommunyti.ru", - "sleamcommunytu.ru", - "sleamcommutiny.com", - "sleamcommuunity.com", - "sleamcommynilu.online", - "sleamcommynitu.ru", - "sleamcommynity.ru", - "sleamcommyunity.com", - "sleamcomnnuniity.ru", - "sleamcomnnuniliy.site", - "sleamcomnnunily.site", - "sleamcomnnunily.website", - "sleamcomnnunitiy.ru", - "sleamcomnnunity.ru", - "sleamcomnnunty.website", - "sleamcomnumity.com", - "sleamcomnunily.ru", - "sleamcomnunity.net.ru", - "sleamcomnunity.xyz", - "sleamcomnunlty.me", - "sleamcomrnunity.com", - "sleamcomuniity.ru", - "sleamcomunitly.co", - "sleamcomunity.me", - "sleamcomunity.net.ru", - "sleamcomunity.ru.com", - "sleamcomunuty.ru", - "sleamconmumity.com", - "sleamconmunity.ru", - "sleamconmunity.xyz", - "sleamconmunlity.com", - "sleamconmunnity.com", - "sleamconnmunitiy.com", - "sleamconnunity.net.ru", - "sleamconnunity.net", - "sleamcoommunilty.com", - "sleamcoommunily.com", - "sleamcoommunity.com", - "sleamcoommunlilty.com", - "sleamcoommunlity.com", - "sleamcoomnnunity.xyz", - "sleamcoomunity.com", - "sleamcoomuuntty.xyz", - "sleamcornmunuity.me", - "sleamcornmunyti.ru", - "sleamcornrnunity.host", - "sleamcornrnunity.ru", - "sleamcummunity.me", - "sleammcommunity.ru", - "sleammcommunnity.ru", - "sleampowered.com", - "sleampowereed.ru", - "sleamscommunity.com", - "sleamtrade-offer.xyz", - "sleancommunlty.xyz", - "sleancomninity.xyz", - "sleanmconmunltiy.ru", - "slearncommunity.store", - "sleemcomnuniti.xyz", - "sleepbuster.xyz", - "slemcamunity.ru", - "slemcommunity.com", - "slemommunity.com", - "sleramconnummitti.org", - "slreamcommumnlty.com", - "slreamcommunntiy.org", - "slreamcomnuitly.xyz", - "slreamcomunity.ru", - "slreamcomunntiy.org", - "slteamcommuinity.com", - "slteamcommunity.com", - "slteamconmuniity.com", - "slum-trade.org.ru", - "smartcommunity.net", - "smeacommunity.com.au", - "smitecommunity.org", - "smtp.ghostgame.ru", - "smtp.ogevtop.ru", - "softhack.ru", - "some-other.ru.com", - "sometheir.xyz", - "sp708431.sitebeat.site", - "spacegivewayzr.xyz", - "spacegivewayzw.xyz", - "special4u.xyz", - "speedtrkzone.com", - "spin-games.com", - "spin4skinzcs.top", - "spin4skinzcs.xyz", - "spinforskin.ml", - "spiritsport.xyz", - "sponsored-simple.xyz", - "sports-liquid.com", - "spt-night.ru", - "sreamcomminity.ru", - "sreamcommuniity.com", - "sreamcommunity.com", - "sreamcommunity.net.ru", - "sreamcommunity.org.ru", - "sreamcommunty.com", - "sreammcommuunntileiy.xyz", - "sreampowered.com", - "sreancomunllty.xyz", - "srtreamcomuninitiy.xyz", - "ssteamcommunitry.com", - "ssteamcommunity.com", - "ssteamcommunity.ru.com", - "ssteampowered.com", - "st-csgo.ru", - "st-eam.ru", - "staamcommunity.com", - "staeaemcornmunite.me", - "staeamcomunnityu.me", - "staeamconmuninty.me", - "staeamconnunitly.online", - "staeamconnunitly.ru", - "staeamcromnuninty.com.profiles-76582109509.me", - "staem-communitu.info", - "staemcammunity.com", - "staemcammunity.me", - "staemcammynlty.ru", - "staemccommunnity.net.ru", - "staemcomcommunlty.ru.com", - "staemcomcommunlty.ru", - "staemcomconmunlty.ru.com", - "staemcommintu.ru", - "staemcomminuty.online", - "staemcomminuty.ru", - "staemcommmunity.com", - "staemcommmunity.online", - "staemcommmunity.ru", - "staemcommnity.ru", - "staemcommnuniti.com", - "staemcommnunity.ru.com", - "staemcommnutiy.ru", - "staemcommueneity.com", - "staemcommuinity.com", - "staemcommuneaity.com", - "staemcommunety.com", - "staemcommuneuity.com", - "staemcommuniity.com", - "staemcommunility.com", - "staemcommunily.com", - "staemcommunily.ru.com", - "staemcommuninity.org.ru", - "staemcommuninty.me", - "staemcommunitey.com", - "staemcommunitiy.com", - "staemcommunitu.com", - "staemcommunitu.ru", - "staemcommunity.click", - "staemcommunity.com.ru", - "staemcommunity.info", - "staemcommunity.org", - "staemcommunity.ru", - "staemcommunityi.com", - "staemcommunityu.ru.com", - "staemcommuniunity.com", - "staemcommunlty.com", - "staemcommunlty.fun", - "staemcommunlty.ru", - "staemcommunlty.us", - "staemcommunninty.com", - "staemcommunnity.club", - "staemcommunnity.com", - "staemcommunnity.ru", - "staemcommunniuty.com", - "staemcommunnlty.ru", - "staemcommuntiy.com", - "staemcommuntiy.ru", - "staemcommuntly.ru", - "staemcommunty.com", - "staemcommunty.ru", - "staemcommuntyi.ru", - "staemcommunulty.ru", - "staemcommunyti.ru.com", - "staemcommynity.xyz", - "staemcomnrnunitiy.ru.com", - "staemcomnuinty.ru", - "staemcomnumity.ru", - "staemcomnunity.fun", - "staemcomnunity.org", - "staemcomnunlty.ru", - "staemcomnunyti.club", - "staemcomnunyti.ru", - "staemcomnunyti.xyz", - "staemcomrnunity.ru.com", - "staemcomrnunity.ru", - "staemcomrnunity.store", - "staemcomrrunity.com", - "staemcomumity.com", - "staemcomunetys.ru.com", - "staemcomunitly.xyz", - "staemcomunity.com", - "staemcomunity.ru", - "staemcomunnity.com", - "staemcomunyti.ru", - "staemconmuilty.com", - "staemconmunilty.com", - "staemconmunity.com", - "staemconmunity.ru.com", - "staemconmunity.ru", - "staemconmunity.xyz", - "staemconmunlty.ru", - "staemcoommnunity.ru", - "staemcoommnuty.ru", - "staemcoommunity.ru", - "staemcoommunlty.ru", - "staemcoommuntiy.ru", - "staemcoommunty.ru", - "staemcoomnunlty.ru", - "staemcoomnunty.ru", - "staemcoomunity.ru", - "staemcoomuntiy.ru", - "staemcoomuunity.ru", - "staemcoomuunity.xyz", - "staemcoomuunty.ru", - "staemcormurnity.com", - "staemcornmunity.com", - "staemcornmunity.online", - "staemcornmunity.ru.com", - "staemcornmunity.ru", - "staemcornmunity.xyz", - "staemcornmuntiy.ru", - "staemcorrmunity.com", - "staemcrommuninty.com.profiles-76577258786.ml", - "staemcrommuninty.com", - "staemcrommunity.com.profiles-768590190751377476483.me", - "staemcrornmmunity.com.profiles-75921098086.me", - "staemcummunity.ru.com", - "staemcummunlty.com", - "staemmcommunity.ru", - "staemncrommunity.store", - "staempawered.xyz", - "staemporewed.xyz", - "staempovered.com", - "staempowered.space", - "staempowered.xyz", - "staermcormmunity.com", - "staermcrommunity.me", - "staermcrommunty.me", - "staermnconnumti.com", - "staerncoinunitiy.me", - "staerncormmunity.com", - "staerncornmunity.co", - "staerncornmunity.com", - "staffcups.ru", - "staffstatsgo.com", - "stamcomunnity.pp.ua", - "stamconnunnity.xyz", - "stammcommunity.com", - "stammcornunity.xyz", - "stampowered.com", - "starmcommunity.net", - "starrygamble.com", - "stat-csgo.ru", - "stats-cs.ru", - "stayempowered.org", - "stceamcomminity.com", - "stcommunity.xyz", - "ste-trade.ru.com", - "ste.amcommunity.com", - "stea-me.ru", - "stea-sgplay.ru", - "steaamcammunitiy.com", - "steaamcamunity.com", - "steaamcommmunity.com", - "steaamcommunity.club", - "steaamcommunnity.co", - "steaamcommunnity.com", - "steaamcommunnity.ru.com", - "steaamcomunity.com", - "steaamcomunity.net", - "steaamcomunity.ru.com", - "steaamconnmunlty.com", - "steaamcorrrmunity.com", - "steacmommunity.com", - "steacommnunity.com", - "steacommunilty.ru.com", - "steacommunity.com", - "steacommunity.net.ru", - "steacommunity.org.ru", - "steacommunity.ru.com", - "steacommunity.site", - "steacommunnity.com", - "steacommunty.ru", - "steacomnmunify.fun", - "steacomnmunity.com", - "steacomnunity.ru.com", - "steaemcamunity.xyz", - "steaemcommunity.pp.ru", - "steaemcommunity.ru.com", - "steaemcomunity.com", - "steaimcoimmunity.com", - "steaimcomminnity.ru", - "steaimcommnunity.com", - "steaimcommumitiy.com", - "steaimcommuniity.com", - "steaimcommunitiy.com", - "steaimcommunytiu.com", - "steaimecommintliy.com", - "steaimecommuninitiy.com", - "steaimecommunytiu.com", - "steaimecommunytu.com", - "steaimeecommunity.com", - "stealcommuniti.ru", - "stealcommunity.com", - "stealcommunlti.com", - "stealmcommulnitycom.xyz", - "stealmcommunity.ru", - "steam-account.ru.com", - "steam-account.ru", - "steam-account.site", - "steam-accounts.com", - "steam-analyst.ru", - "steam-announcements1.xyz", - "steam-auth.com", - "steam-auth.ru", - "steam-cammuneti.com", - "steam-communiity.ru", - "steam-community.net.ru", - "steam-community.org.ru", - "steam-community.ru.com", - "steam-community.xyz", - "steam-community1.xyz", - "steam-communitygifts.xyz", - "steam-communitygifts1.xyz", - "steam-communitysource.xyz", - "steam-communitysource1.xyz", - "steam-communitytrade.xyz", - "steam-comunity.me", - "steam-cs-good.ru", - "steam-cs.ru", - "steam-csgo-game.ru", - "steam-csgo-good.ru", - "steam-csgo-store.ru", - "steam-csgo.ru", - "steam-csgocom.ru", - "steam-csgogame.ru", - "steam-csgoplay.ru", - "steam-discord.com", - "steam-discord.ru", - "steam-discords.com", - "steam-dlscord.com", - "steam-free-nitro.ru", - "steam-g5chanaquyufuli.ru", - "steam-game-csgo.ru", - "steam-gametrade.xyz", - "steam-historyoffer.xyz", - "steam-hometrade.xyz", - "steam-hometrades.xyz", - "steam-hype.com", - "steam-login.ru", - "steam-login1.xyz", - "steam-nitro.com", - "steam-nitro.ru", - "steam-nitro.store", - "steam-nitros.com", - "steam-nitros.ru", - "steam-nltro.com", - "steam-nltro.ru", - "steam-nltros.ru", - "steam-offer.com", - "steam-offersgames.xyz", - "steam-offersofficial.xyz", - "steam-offerstore.xyz", - "steam-officialtrade.xyz", - "steam-play-csgo.ru", - "steam-povered.xyz", - "steam-power.xyz", - "steam-power1.xyz", - "steam-powered-games.com", - "steam-powered.xyz", - "steam-powered1.xyz", - "steam-poweredexchange.xyz", - "steam-poweredoffer.xyz", - "steam-poweredoffers.xyz", - "steam-poweredtrades.xyz", - "steam-profile.com", - "steam-promo-page.ml", - "steam-rep.com", - "steam-ru.ru", - "steam-service.ru", - "steam-servicedeals.xyz", - "steam-servicedeals1.xyz", - "steam-site.ru", - "steam-sourcecommunity.xyz", - "steam-sourcecommunity1.xyz", - "steam-storetrade.xyz", - "steam-storetrade1.xyz", - "steam-support.xyz", - "steam-trade.xyz", - "steam-tradegame.xyz", - "steam-tradehome.xyz", - "steam-tradeoffer.com", - "steam-tradeoffer.xyz", - "steam-trades.icu", - "steam-tradeshome.xyz", - "steam-tradestore.xyz", - "steam-tradestore1.xyz", - "steam.99box.com", - "steam.cards", - "steam.cash", - "steam.cheap", - "steam.codes", - "steam.communty.com", - "steam.communyty.worldhosts.ru", - "steam.comnunity.com", - "steam.luancort.com", - "steam.mmosvc.com", - "steam4you.online", - "steamaccount.xyz", - "steamaccountgenerator.ru.com", - "steamaccounts.net", - "steamaccounts.org", - "steamacommunity.com", - "steamanalysts.com", - "steambrowser.xyz", - "steamc0mmunity.com", - "steamc0munnity.site", - "steamcamiutity.com", - "steamcammiuniltty.com", - "steamcammmunity.ru", - "steamcammnuity.com", - "steamcammuinity.com", - "steamcammuniety.com", - "steamcammunitey.com", - "steamcammuniti.ru", - "steamcammunitu.com", - "steamcammunitu.ru.com", - "steamcammunity-profile.ru", - "steamcammunity.net", - "steamcammunity.top", - "steamcammunlty.ru", - "steamcammuntiy.com", - "steamcammunty.com", - "steamcammunuty.com", - "steamcammunyty.fun", - "steamcammunyty.ru", - "steamcamnunity.com.ru", - "steamcamnunity.ru", - "steamcamunite.com", - "steamcamunitey.com", - "steamcamunitu.com", - "steamcamunitu.xyz", - "steamcamunity-profile.ru", - "steamcamunity.com", - "steamcamunity.ru", - "steamcamunity.top", - "steamcamunity.xyz", - "steamcamunlty.com", - "steamcamunnity.xyz", - "steamcannunlty.com", - "steamcard.me", - "steamccommuniity.com", - "steamccommunity.com", - "steamccommunity.net", - "steamccommunity.ru.com", - "steamccommunityy.ru", - "steamccommunyty.ru", - "steamccommurity.ru", - "steamccommyunity.com", - "steamccomunnity.ru.com", - "steamcconmmuunity.co", - "steamchinacsgo.ru", - "steamcmmunuti.ru", - "steamcmmunyti.ru", - "steamcmunity.com", - "steamco.mmunity.com", - "steamco.ru", - "steamcoarnmmnunity.ru.com", - "steamcodesgen.com", - "steamcokmunity.com", - "steamcomannlty.xyz", - "steamcombain.com", - "steamcomcmunlty.com", - "steamcomcunity.ru", - "steamcominity.ru", - "steamcominuty.ru", - "steamcomity.com", - "steamcomiunity.com", - "steamcomiunity.xyz", - "steamcomiynuytiy.net.ru", - "steamcommenitry.ru", - "steamcommenity.ru", - "steamcommeunity.com", - "steamcommhnity.com", - "steamcomminiity.site", - "steamcomminiti.ru", - "steamcomminity.com", - "steamcomminity.ru.com", - "steamcomminity.ru", - "steamcomminnty.com", - "steamcommintty.com", - "steamcomminty.ru", - "steamcomminulty.ru", - "steamcomminuly.com", - "steamcomminuly.ru", - "steamcomminutiiu.ru", - "steamcomminutiu.ru", - "steamcomminutiy.ru", - "steamcomminutty.ru", - "steamcomminuty-offer.ru.com", - "steamcomminuty.click", - "steamcomminuty.com", - "steamcomminuty.link", - "steamcomminuty.me", - "steamcomminuty.nl", - "steamcomminuty.repl.co", - "steamcomminuty.ru.com", - "steamcomminuty.ru", - "steamcomminuty.xyz", - "steamcomminyti.ru", - "steamcomminytiu.com", - "steamcomminytiu.ru", - "steamcomminytiy.ru", - "steamcomminytu.click", - "steamcomminytu.com", - "steamcomminytu.link", - "steamcomminytu.ru", - "steamcomminyty.ru.com", - "steamcommiuinity.com", - "steamcommiunitiy.pp.ru", - "steamcommiunitty.ru", - "steamcommiunity.pp.ru", - "steamcommiunity.ru", - "steamcommiunniutty.net.ru", - "steamcommiunty.ru", - "steamcommiynitiy.net.ru", - "steamcommllty.com", - "steamcommlnuty.com", - "steamcommlunity.com", - "steamcommmuiniity.ru", - "steamcommmunitty.site", - "steamcommmunity.xyz", - "steamcommmunlity.com", - "steamcommmunnity.com", - "steamcommmunty.com", - "steamcommninty.com", - "steamcommnity.com.ru", - "steamcommnity.com", - "steamcommnity.ru", - "steamcommnity.store", - "steamcommnlty.com", - "steamcommnlty.xyz", - "steamcommnmunity.ru", - "steamcommnnity.net.ru", - "steamcommnnunity.ru", - "steamcommnnunnity.world", - "steamcommntiy.xyz", - "steamcommnuitly.com", - "steamcommnuitty.com", - "steamcommnultiy.ru", - "steamcommnulty.com", - "steamcommnulty.store", - "steamcommnunily.com", - "steamcommnunily.xyz", - "steamcommnuninty.com", - "steamcommnuninty.ru.com", - "steamcommnunitlu.com", - "steamcommnunitu.com", - "steamcommnunity.com", - "steamcommnunity.org.ru", - "steamcommnunity.ru.com", - "steamcommnunlty.com", - "steamcommnunlty.icu", - "steamcommnunlty.ru", - "steamcommnunlty.xyz", - "steamcommnunmity.com", - "steamcommnunniiy.net.ru", - "steamcommnuntiy.com", - "steamcommnunty.ru", - "steamcommnunylti.com", - "steamcommnunyti.com", - "steamcommnunytl.com", - "steamcommnutly.ru.com", - "steamcommnutry.com", - "steamcommnutry.ru", - "steamcommnuty.site", - "steamcommnuuntiy.com", - "steamcommonitey.com", - "steamcommonnnity.ru.com", - "steamcommqnity.com", - "steamcommrnunity.com", - "steamcommrunitly.com", - "steamcommrutiny.ru", - "steamcommtity.com", - "steamcommuanity.ru.com", - "steamcommuenity.com", - "steamcommuhity.ru", - "steamcommuhuity.com", - "steamcommuilty.ru", - "steamcommuinilty.com", - "steamcommuininty.com", - "steamcommuinitiycom.ru", - "steamcommuinity.com", - "steamcommuinity.ru", - "steamcommuinty.com.ru", - "steamcommuinuity.com", - "steamcommuiti.ru", - "steamcommuitliy.com", - "steamcommuitly.ru", - "steamcommuity.com", - "steamcommuity.ru", - "steamcommulity.ru", - "steamcommulltty.com", - "steamcommullty.ru", - "steamcommulnity.com", - "steamcommulnt.ru.com", - "steamcommulnty.ru", - "steamcommulty.ru", - "steamcommumilty.com", - "steamcommumitiy.com", - "steamcommumituy.com", - "steamcommumity.biz", - "steamcommumity.net", - "steamcommumiuty.com", - "steamcommumlity.com", - "steamcommumnity.com", - "steamcommumtiy.com", - "steamcommun1ty.ru", - "steamcommunely.ru", - "steamcommuneteiy.com", - "steamcommunetiy.com", - "steamcommunetiy.ru", - "steamcommunetiyi.com", - "steamcommunetiyy.xyz", - "steamcommunetu.com", - "steamcommunety.com", - "steamcommunety.net.ru", - "steamcommunety.online", - "steamcommunety.org.ru", - "steamcommunety.ru", - "steamcommunety1i.com", - "steamcommunetyei.com", - "steamcommuneuity.ru", - "steamcommunhity.com", - "steamcommuni.com", - "steamcommunicty.com", - "steamcommunicty.ru.com", - "steamcommunidy.com", - "steamcommunieityi.com", - "steamcommunieti.ru", - "steamcommunietiy.com", - "steamcommuniety.com", - "steamcommuniety.ru", - "steamcommunifly.ru.com", - "steamcommunify.com", - "steamcommunify.ru", - "steamcommunihty.com", - "steamcommuniiity.com", - "steamcommuniilty.ru", - "steamcommuniitu.site", - "steamcommuniity.com.ru", - "steamcommuniiy.online", - "steamcommuniiy.ru", - "steamcommunikkty.net.ru", - "steamcommunili.xyz", - "steamcommunility.com", - "steamcommunillty.com", - "steamcommunillty.net.ru", - "steamcommunillty.ru.com", - "steamcommunillty.ru", - "steamcommunilly.com", - "steamcommuniltily.ru.com", - "steamcommuniltiy.online", - "steamcommuniltiy.ru", - "steamcommuniltly.com", - "steamcommunilty.buzz", - "steamcommunilty.it", - "steamcommunilty.ru.com", - "steamcommunilty.us", - "steamcommunilty.xyz", - "steamcommuniltys.com", - "steamcommunilv.com", - "steamcommunily.buzz", - "steamcommunily.org", - "steamcommunily.uno", - "steamcommunimty.ru.com", - "steamcommuninity.ru.com", - "steamcommuninthy.com", - "steamcommuninty.ru.com", - "steamcommuninunty.com", - "steamcommunirtly.ru.com", - "steamcommunirty.com", - "steamcommunirty.ru.com", - "steamcommuniry.com", - "steamcommuniry.net.ru", - "steamcommuniry.ru", - "steamcommunit.org.ru", - "steamcommunit.ru.com", - "steamcommunit.ru", - "steamcommunitcy.ru.com", - "steamcommunite.com", - "steamcommunite.ru", - "steamcommunitey.com", - "steamcommunitey.ru", - "steamcommuniteypowered.com", - "steamcommunitfy.com", - "steamcommunitfy.ru.com", - "steamcommunithy.com", - "steamcommuniti.com.ru", - "steamcommuniti.org.ru", - "steamcommuniti.org", - "steamcommuniti.ru.com", - "steamcommunitie.net", - "steamcommunitie.ru.com", - "steamcommunitie.ru", - "steamcommunitie.site", - "steamcommunities.biz", - "steamcommunitii.xyz", - "steamcommunitily.com", - "steamcommunitity.com", - "steamcommunitiu.ru", - "steamcommunitiv.com", - "steamcommunitiy.ru", - "steamcommunitiycom.ru", - "steamcommunitiyu.com", - "steamcommunitiyy.com", - "steamcommunitj.buzz", - "steamcommunitl.com", - "steamcommunitl.net.ru", - "steamcommunitli.ru", - "steamcommunitlil.ru", - "steamcommunitliy.ru.com", - "steamcommunitlly.com", - "steamcommunitlly.net", - "steamcommunitlly.ru.com", - "steamcommunitlu.com", - "steamcommunitluy.com", - "steamcommunitly.com", - "steamcommunitly.me", - "steamcommunitmy.ru.com", - "steamcommunitry.com", - "steamcommunitry.ru", - "steamcommunitte.com", - "steamcommunitte.ru", - "steamcommunittey.com", - "steamcommunittrade.xyz", - "steamcommunittru.co", - "steamcommunittry.xyz", - "steamcommunitty.com.ru", - "steamcommunitty.esplay.eu", - "steamcommunitty.net", - "steamcommunitty.site", - "steamcommunitty.top", - "steamcommunitu.com-profile-poka.biz", - "steamcommunitu.com-profiles-mellenouz.trade", - "steamcommunitu.icu", - "steamcommunitu.net", - "steamcommunitu.ru.com", - "steamcommunitv.ru", - "steamcommunitvs.com", - "steamcommunitx.ru.com", - "steamcommunity-com.xyz", - "steamcommunity-comtradeoffer.ru", - "steamcommunity-gifts.xyz", - "steamcommunity-gifts1.xyz", - "steamcommunity-nitro.ru", - "steamcommunity-nitrogeneral.ru", - "steamcommunity-profile.net", - "steamcommunity-profiles.ru.com", - "steamcommunity-source.xyz", - "steamcommunity-source1.xyz", - "steamcommunity-trade.xyz", - "steamcommunity-tradeoffer.com", - "steamcommunity-tradeoffer.ru.com", - "steamcommunity-tradeoffer4510426522.ru", - "steamcommunity-tradeoffers.com", - "steamcommunity-user.me", - "steamcommunity-xpubg.xyz", - "steamcommunity.at", - "steamcommunity.best", - "steamcommunity.biz", - "steamcommunity.ca", - "steamcommunity.click", - "steamcommunity.cloud", - "steamcommunity.cn", - "steamcommunity.co.ua", - "steamcommunity.com-id-k4tushatwitchbabydota.ru", - "steamcommunity.com.ru", - "steamcommunity.comlappl251490lrust.ru", - "steamcommunity.de", - "steamcommunity.digital", - "steamcommunity.eu", - "steamcommunity.in", - "steamcommunity.link", - "steamcommunity.live", - "steamcommunity.llc", - "steamcommunity.mobi", - "steamcommunity.moscow", - "steamcommunity.net.in", - "steamcommunity.pl", - "steamcommunity.pp.ru", - "steamcommunity.rest", - "steamcommunity.ru.net", - "steamcommunity.ru", - "steamcommunity.site", - "steamcommunity.steams.ga", - "steamcommunity.support", - "steamcommunity.team", - "steamcommunity.trade", - "steamcommunity.us", - "steamcommunity1.com", - "steamcommunitya.com", - "steamcommunityc.com", - "steamcommunityc.ru", - "steamcommunitycom.ru.com", - "steamcommunitycomoffernewpartner989791155tokenjbhldtj6.trade", - "steamcommunitycomtradeoffer.ru.com", - "steamcommunitygames.com", - "steamcommunitygifts.xyz", - "steamcommunitygifts1.xyz", - "steamcommunityi.com", - "steamcommunityi.ru.com", - "steamcommunityi.ru", - "steamcommunityid.ru", - "steamcommunitylink.xyz", - "steamcommunitym.com", - "steamcommunitym.ru", - "steamcommunitynow.com", - "steamcommunityo.com", - "steamcommunityoff.com", - "steamcommunityoffers.org", - "steamcommunitypubg.com", - "steamcommunityr.com.ru", - "steamcommunityru.tk", - "steamcommunityshop.com", - "steamcommunitysource.xyz", - "steamcommunitysource1.xyz", - "steamcommunitytradeofer.com", - "steamcommunitytradeoffer.com", - "steamcommunitytradeoffer.ru", - "steamcommunitytradeoffter.com", - "steamcommunitytradeofter.com", - "steamcommunitytredeoffer.com", - "steamcommunityu.com", - "steamcommunityu.ru", - "steamcommunityw.com", - "steamcommunityw.net.ru", - "steamcommunityw.org.ru", - "steamcommunitywork.com", - "steamcommunitywork.ml", - "steamcommunityx.com", - "steamcommunityy.online", - "steamcommunityy.ru", - "steamcommunityz.com", - "steamcommunityzbn.top", - "steamcommunityzbo.top", - "steamcommunityzbq.top", - "steamcommunityzbr.top", - "steamcommunityzcd.top", - "steamcommunityzce.top", - "steamcommunityzci.top", - "steamcommunityzda.top", - "steamcommunityzdb.top", - "steamcommunityzdd.top", - "steamcommunityzdl.top", - "steamcommunityzdp.top", - "steamcommunityzdq.top", - "steamcommunityzdr.top", - "steamcommunityzds.top", - "steamcommunityzdt.top", - "steamcommuniuity.com", - "steamcommuniutiiy.com", - "steamcommuniutiy.ru", - "steamcommuniuty.ru", - "steamcommuniy.com", - "steamcommuniyt.com", - "steamcommuniytu.com", - "steamcommuniyty.ru", - "steamcommunjti.com", - "steamcommunjtv.xyz", - "steamcommunjty.net", - "steamcommunjty.ru", - "steamcommunlilty.ru.com", - "steamcommunlite.com", - "steamcommunlitily.ru.com", - "steamcommunlitly.ru", - "steamcommunlitty.ru.com", - "steamcommunlitty.ru", - "steamcommunlity.net", - "steamcommunlity.ru.com", - "steamcommunlity.ru", - "steamcommunlityl.ru", - "steamcommunliu.com", - "steamcommunlky.net.ru", - "steamcommunllity.ru.com", - "steamcommunllty.com", - "steamcommunllty.ru", - "steamcommunlte.ru", - "steamcommunltiy.club", - "steamcommunltiy.com", - "steamcommunltty.com", - "steamcommunltu.com", - "steamcommunltuy.com", - "steamcommunltv.buzz", - "steamcommunlty-proflle.com.ru", - "steamcommunlty.biz", - "steamcommunlty.business", - "steamcommunlty.cloud", - "steamcommunlty.company", - "steamcommunlty.info", - "steamcommunlty.link", - "steamcommunlty.pro", - "steamcommunlty.shop", - "steamcommunlty.site", - "steamcommunlty.store", - "steamcommunlty.top", - "steamcommunltyu.ru", - "steamcommunltyy.com", - "steamcommunly.com", - "steamcommunly.net.ru", - "steamcommunmity.com.ru", - "steamcommunniittly.ru", - "steamcommunniitty.com", - "steamcommunniity.com", - "steamcommunniity.net", - "steamcommunniity.ru", - "steamcommunnilty.com", - "steamcommunnilty.ru", - "steamcommunnitey.com", - "steamcommunnitlly.ru", - "steamcommunnitty.ru", - "steamcommunnity.co", - "steamcommunnity.com.ru", - "steamcommunnity.ml", - "steamcommunnity.net", - "steamcommunnity.ru.com", - "steamcommunnity.ru", - "steamcommunnjty.com", - "steamcommunnlity.ru", - "steamcommunnlty.com.ru", - "steamcommunnty.ru", - "steamcommunnuty.ru", - "steamcommunrinty.ru.com", - "steamcommunrity.com", - "steamcommunrity.ru.com", - "steamcommunrlity.com", - "steamcommunrrity.com", - "steamcommunti.com", - "steamcommuntily.ru.com", - "steamcommuntily.ru", - "steamcommuntity.com", - "steamcommuntity.ru.com", - "steamcommuntiv.com", - "steamcommuntiy.com", - "steamcommuntli.ru", - "steamcommuntliy.ru", - "steamcommuntly.com", - "steamcommuntry.com", - "steamcommunty.buzz", - "steamcommunty.com.ru", - "steamcommunty.com", - "steamcommunty.net", - "steamcommunty.pw", - "steamcommunty.ru.com", - "steamcommuntyy.ru", - "steamcommunuaity.xyz", - "steamcommunuety.ru", - "steamcommunuity.net", - "steamcommunuity.ru", - "steamcommununty-con.ru", - "steamcommununty.ru", - "steamcommunury.ru", - "steamcommunute.com", - "steamcommunuti.co", - "steamcommunuti.ru", - "steamcommunutii.ru", - "steamcommunutiy.com", - "steamcommunutry.com", - "steamcommunutry.ru", - "steamcommunutty.com", - "steamcommunutty.ru", - "steamcommunutuy.com", - "steamcommunuty.buzz", - "steamcommunuty.co", - "steamcommunuty.link", - "steamcommunuty.org.ru", - "steamcommunuty.ru", - "steamcommunutyu.com", - "steamcommunvti.ru", - "steamcommunyity.ru", - "steamcommunylty.ru", - "steamcommunyte.com", - "steamcommunyti.com", - "steamcommunyti.info", - "steamcommunytitradeoffer.com", - "steamcommunytiu.com", - "steamcommunytiu.ru", - "steamcommunytiy.ru", - "steamcommunytiy.tk", - "steamcommunytu.ru", - "steamcommunyty.com", - "steamcommunyty.ru.com", - "steamcommunyty.xyz", - "steamcommunytytradeofferphobos.ru", - "steamcommuriity.com", - "steamcommurity.ru", - "steamcommurjty.com", - "steamcommurlity.com", - "steamcommurlty.com", - "steamcommurnity.com", - "steamcommurnuity.com", - "steamcommutinny.ru.com", - "steamcommutiny.com", - "steamcommutiny.ru.com", - "steamcommutiny.ru", - "steamcommutiny.xyz", - "steamcommutry.ru", - "steamcommuty.com", - "steamcommutyniu.com", - "steamcommutyniy.com", - "steamcommuuity.net.ru", - "steamcommuulty.com", - "steamcommuunitey.com", - "steamcommuunitty.ru.com", - "steamcommuunity.net.ru", - "steamcommuunity.pp.ru", - "steamcommuunity.ru.com", - "steamcommuunity.ru", - "steamcommuunjty.com", - "steamcommuunlity.com", - "steamcommuunlty.com", - "steamcommuwunity.com", - "steamcommuynity.ru.com", - "steamcommyinuty.ru", - "steamcommymity.ru", - "steamcommynite.com", - "steamcommyniti.ru", - "steamcommyniti.xyz", - "steamcommynitiu.com", - "steamcommynitry.ru", - "steamcommynitu.com", - "steamcommynitu.net.ru", - "steamcommynitu.ru.com", - "steamcommynitu.ru", - "steamcommynitu.xyz", - "steamcommynituy.com", - "steamcommynity.icu", - "steamcommynity.ru", - "steamcommynity.space", - "steamcommynityprofile.ru", - "steamcommynltu.com", - "steamcommynlty.com", - "steamcommynlty.ru", - "steamcommynnityy.com", - "steamcommynuti.ru", - "steamcommynutiy.ru", - "steamcommynutu.ru", - "steamcommynuty.ru.com", - "steamcommynyti.ru", - "steamcommynyti.site", - "steamcommytiny.com", - "steamcommytuniu.com", - "steamcommyuinity.net.ru", - "steamcommyunity.com", - "steamcomnenity.ru.com", - "steamcomninuty.ru.com", - "steamcomninytiu.com", - "steamcomniunity.com", - "steamcomnmnuty.ru", - "steamcomnmrunity.online", - "steamcomnmrunity.ru", - "steamcomnmufly.ru.com", - "steamcomnmuituy.com", - "steamcomnmuity.ru", - "steamcomnmunity.com.ru", - "steamcomnmunlty.com", - "steamcomnmuntiy.ru.com", - "steamcomnmutly.ru.com", - "steamcomnmuunity.ru.com", - "steamcomnmynitu.com", - "steamcomnnity.net.ru", - "steamcomnnlty.com", - "steamcomnnuity.com", - "steamcomnnunilty.com", - "steamcomnnunity.co", - "steamcomnnunity.ru.com", - "steamcomnnunity.ru", - "steamcomnnunlty.ru", - "steamcomnnunty.ru", - "steamcomnnuty.ru", - "steamcomnnynlty.com", - "steamcomnuenuity.com", - "steamcomnuhity.com", - "steamcomnuiti.xyz", - "steamcomnulty.com", - "steamcomnumilty.com", - "steamcomnumily.com", - "steamcomnumity.com", - "steamcomnumity.org.ru", - "steamcomnumity.ru.com", - "steamcomnumity.ru", - "steamcomnumity.xyz", - "steamcomnumlity.com", - "steamcomnumlty.com", - "steamcomnumlty.ru", - "steamcomnumnity.com", - "steamcomnumty.ru", - "steamcomnuniity.com.ru", - "steamcomnuniity.pp.ru", - "steamcomnuniity.ru.com", - "steamcomnunilty.com", - "steamcomnunilty.ru.com", - "steamcomnunily.co", - "steamcomnunirty.ru", - "steamcomnuniti.com", - "steamcomnunitiy.com", - "steamcomnunitiy.ru", - "steamcomnunitly.com", - "steamcomnunitly.tk", - "steamcomnunitry.ru", - "steamcomnunitty.com", - "steamcomnunity.com", - "steamcomnunity.net", - "steamcomnunity.org.ru", - "steamcomnunity.ru", - "steamcomnunity.site", - "steamcomnunityprofile.ru.com", - "steamcomnunlity.com", - "steamcomnunlity.ru", - "steamcomnunllty.com", - "steamcomnunllty.net", - "steamcomnunlty.ru.com", - "steamcomnunlty.ru", - "steamcomnunluty.ru", - "steamcomnunmity.com", - "steamcomnunnirty.ru", - "steamcomnunniry.ru", - "steamcomnunnity.com", - "steamcomnunnity.net.ru", - "steamcomnunnity.net", - "steamcomnunnlty.ru", - "steamcomnuntiy.com", - "steamcomnuntty.ru.com", - "steamcomnunutiy.ru", - "steamcomnunuty.com", - "steamcomnunuty.ru", - "steamcomnunytu.ru", - "steamcomnurity.com", - "steamcomnurity.xyz", - "steamcomnutiny.online", - "steamcomnutiny.ru.com", - "steamcomnutiny.ru", - "steamcomnuty.com", - "steamcomnuunlty.com", - "steamcomnynlity.ru", - "steamcomonity.com", - "steamcomrmunity.ru.com", - "steamcomrmunnuity.ru.com", - "steamcomrneuneity.com", - "steamcomrninuty.link", - "steamcomrninuty.ru", - "steamcomrninuty.site", - "steamcomrnity.xyz", - "steamcomrnlnuty.site", - "steamcomrnumity.com", - "steamcomrnunite.com", - "steamcomrnuniti.ru.com", - "steamcomrnunitu.ru.com", - "steamcomrnunitu.ru", - "steamcomrnunity.com.ru", - "steamcomrnunity.online", - "steamcomrnunity.ru.com", - "steamcomrnunity.ru", - "steamcomrnunity.site", - "steamcomrnunity.su", - "steamcomrnunity.xyz", - "steamcomrnunlty.com", - "steamcomrnunlty.ru", - "steamcomrnunuity.ru.com", - "steamcomrnyniti.ru.com", - "steamcomrnyniti.ru", - "steamcomrrnunity.com", - "steamcomrrnunity.net.ru", - "steamcomrrnunity.ru", - "steamcomrunily.com", - "steamcomrunity.com", - "steamcomueniity.ru", - "steamcomumity.com", - "steamcomumunty.com", - "steamcomunety.com", - "steamcomunety.ru", - "steamcomuniety.ru", - "steamcomuniiity.com", - "steamcomuniitly.ru.com", - "steamcomuniity.ru.com", - "steamcomunillty.ru.com", - "steamcomuniltu.xyz", - "steamcomunilty.com", - "steamcomunily.ru.com", - "steamcomuninruty.ru", - "steamcomuniti.com", - "steamcomuniti.ru", - "steamcomuniti.xyz", - "steamcomunitly.pp.ru", - "steamcomunitly.ru", - "steamcomunitty.ru.com", - "steamcomunitu.com", - "steamcomunitu.net.ru", - "steamcomunitu.ru", - "steamcomunituy.com", - "steamcomunity-comid12121212123244465.ru", - "steamcomunity-nitro-free.ru", - "steamcomunity.com.ru", - "steamcomunity.com", - "steamcomunity.me", - "steamcomunity.net.ru", - "steamcomunity.org.ru", - "steamcomunity.ru", - "steamcomunity.us", - "steamcomunityo.com", - "steamcomunitytrades.xyz", - "steamcomunityy.com", - "steamcomunlitly.ru.com", - "steamcomunlty.ru.com", - "steamcomunmity.ru.com", - "steamcomunniity.ru", - "steamcomunninuty.com", - "steamcomunnitly.ru.com", - "steamcomunnitu.xyz", - "steamcomunnity.fun", - "steamcomunnity.ru.com", - "steamcomunnity.site", - "steamcomunnity.xyz", - "steamcomunnlty.com", - "steamcomunnuity.com", - "steamcomunnuty.com", - "steamcomunnyti.ru", - "steamcomuntty.com", - "steamcomunty.org.ru", - "steamcomunuty.com", - "steamcomunuty.ru", - "steamcomunyiti.ru.com", - "steamcomunyti.com", - "steamcomunytiu.com", - "steamcomuuniity.com", - "steamcomuunity.com", - "steamcomuunity.ru.com", - "steamcomyniti.xyz", - "steamcomynitu.ru", - "steamcomynity.ru", - "steamcomynlty.com", - "steamcomynnitu.net.ru", - "steamconimmunity.com", - "steamconminuty.ru", - "steamconmiunity.ru", - "steamconmmuntiy.com", - "steamconmnmnunity.ru", - "steamconmnmunity.ru", - "steamconmnunitiy.ru.com", - "steamconmnunitiy.ru", - "steamconmnunity.co", - "steamconmnunity.com", - "steamconmnunity.ru", - "steamconmnunuty.ru.com", - "steamconmnutiny.ru", - "steamconmuhlty.com", - "steamconmumity.com.ru", - "steamconmumity.com", - "steamconmumity.ru.com", - "steamconmumity.ru", - "steamconmumltu.com.ru", - "steamconmummity.ru", - "steamconmumnity.com", - "steamconmuniti.ru", - "steamconmunitly.com", - "steamconmunitty.com", - "steamconmunity.co", - "steamconmunity.com.ru", - "steamconmunity.pp.ru", - "steamconmunity.xyz", - "steamconmunjty.com", - "steamconmunlly.com", - "steamconmunlty.com.ru", - "steamconmunlty.com", - "steamconmunlty.ru", - "steamconmunnitry.ru", - "steamconmunnlty.ru", - "steamconmunuty.ru", - "steamconmunyty.com", - "steamconmunyty.ru", - "steamconnmuhity.com", - "steamconnmunitu.net.ru", - "steamconnmunity.ru", - "steamconnmunlty.com", - "steamconnmunlty.ru.com", - "steamconnmunlty.ru", - "steamconnnnunity.net.ru", - "steamconnnnunity.org.ru", - "steamconnumity.ru.com", - "steamconnummity.ru", - "steamconnumuty.com", - "steamconnuniitty.tk", - "steamconnunirty.ru", - "steamconnunitiy.com", - "steamconnunity.com.ru", - "steamconnunity.com", - "steamconnunity.de", - "steamconnunity.fun", - "steamconnunity.net", - "steamconnunity.pp.ru", - "steamconnunity.ru.com", - "steamconnunlty.com", - "steamconummity.ru", - "steamconunity.cf", - "steamconunity.ru", - "steamconunity.tk", - "steamconunlty.ru", - "steamconynuyty.net.ru", - "steamconynuyty.org.ru", - "steamcoominuty.site", - "steamcoomminuty.site", - "steamcoommunety.com", - "steamcoommuniity.link", - "steamcoommuniity.ru", - "steamcoommunilty.com", - "steamcoommunity.pp.ru", - "steamcoommunity.ru.com", - "steamcoommunllty.com", - "steamcoommunlty.ru", - "steamcoommunuity.com", - "steamcoommunuty.com", - "steamcoomrnmunity.ml", - "steamcoomunity-nitro.site", - "steamcoomunitye.com", - "steamcoomunjty.com", - "steamcoomunlty.com", - "steamcoomunlty.net", - "steamcoomunlty.ru", - "steamcoomunnity.com", - "steamcoomunnity.ru", - "steamcoomynity.ru", - "steamcoonmuntiy.ru", - "steamcoormmunity.com", - "steamcormmmunity.com", - "steamcormmunity.com", - "steamcormmunity.net.ru", - "steamcormmunity.ru.com", - "steamcormmuntiy.com", - "steamcormmuuity.ru", - "steamcormrunity.com", - "steamcormunity.ru", - "steamcormunity.xyz", - "steamcormurnity.com", - "steamcornminity.ru.com", - "steamcornminty.xyz", - "steamcornminuty.com", - "steamcornmmunity.com", - "steamcornmnitu.ru.com", - "steamcornmnuity.com", - "steamcornmunety.com", - "steamcornmunify.ru.com", - "steamcornmuniity.net.ru", - "steamcornmunily.ru", - "steamcornmunit.ru.com", - "steamcornmunite.com", - "steamcornmunity.fun", - "steamcornmunity.net.ru", - "steamcornmunity.org", - "steamcornmunty.com", - "steamcornmunyti.ru", - "steamcornmynitu.ru", - "steamcornmynity.ru", - "steamcornrnuity.com", - "steamcornrnunity.com.ru", - "steamcornrnunity.fun", - "steamcornrrnunity.com", - "steamcorrmunity.com", - "steamcorrnmunity.ru", - "steamcorrnunity.org", - "steamcoummunitiy.com", - "steamcoummunity.com", - "steamcrommunlty.me", - "steamcromnmunity-com.profiles-7685981598976.me", - "steamcronnmmuniry.me", - "steamcsgo-game.ru", - "steamcsgo-play.ru", - "steamcsgo.ru", - "steamcsgoplay.ru", - "steamcummunity.com.ru", - "steamcummunity.com", - "steamcummunity.ru.com", - "steamcummunity.ru", - "steamcummunityy.pp.ua", - "steamcummunnity.com", - "steamcumumunity.com.ru", - "steamdesksupport.com", - "steamdiscord.com", - "steamdiscord.ru", - "steamdiscordi.com", - "steamdiscordj.com", - "steamdiscords.com", - "steamdiscrod.ru", - "steamdlscord.com", - "steamdlscords.com", - "steamdocs.xyz", - "steamdomain.online", - "steamdomain.ru", - "steamdommunity.com", - "steamecommuinty.com", - "steamecommunitiiy.com", - "steamecommunitiy.com", - "steamecommunituiy.com", - "steamecommunity.net", - "steamecommunity.org", - "steamecommunity.pp.ua", - "steamecommunity.ru.com", - "steamecommuniuty.com", - "steamecommunlty.com.ru", - "steamecommunlty.com", - "steamecommunytu.com", - "steamecomunity.com.ru", - "steamedpowered.com", - "steamepowered.com", - "steamescommunity.com", - "steamgame-csgo.ru", - "steamgame-trade.xyz", - "steamgame.net.ru", - "steamgamepowered.net", - "steamgames.net.ru", - "steamgamesroll.ru", - "steamgametrade.xyz", - "steamgiftcards.cf", - "steamgifts.net.ru", - "steamgiveaway.cc", - "steamgiveawayfree.ru", - "steamgivenitro.com", - "steamglft.ru", - "steamguard.ir", - "steamhelp.net", - "steamhome-trade.xyz", - "steamhome-trades.xyz", - "steamhometrade.xyz", - "steamhometrades.xyz", - "steamicommunnity.com", - "steamid.ru", - "steamitem.xyz", - "steamkey.ru", - "steamkommunity.net.ru", - "steamkommunity.org.ru", - "steamlcommunity.net.ru", - "steamlcommunity.org.ru", - "steamlcommunity.ru.com", - "steamm.store", - "steammatily.online", - "steammatily.ru", - "steammcamunitu.com", - "steammcamunity.com", - "steammcamunity.ru.com", - "steammcomminity.ru", - "steammcomminuty.ru", - "steammcommmunlty.pp.ua", - "steammcommunety.com", - "steammcommuniity.ru", - "steammcommunily.net.ru", - "steammcommunitey.com", - "steammcommunitly.ru", - "steammcommunity-trade.xyz", - "steammcommunity.com", - "steammcommunity.ru.com", - "steammcommunity.ru", - "steammcommunnity.ru", - "steammcommunyti.ru", - "steammcommuunityy.ru.com", - "steammcomtradeoff.com", - "steammcomunit.ru", - "steammcomunity.ru", - "steammcomunlty.ru", - "steammcomunnity.com", - "steammcounity.ru.com", - "steammecommunity.com", - "steammncommunty.ru.com", - "steamncommnunity.ru", - "steamncommnunty.ru", - "steamncommuinity.com", - "steamncommumity.ru", - "steamncommuniity.com", - "steamncommunitiy.com", - "steamncommunitu.co", - "steamncommunity.com", - "steamncommunity.pp.ru", - "steamncommunity.ru", - "steamncommunity.xyz", - "steamncommunytu.ru", - "steamncomnunlty.com.ru", - "steamncomunitity.com", - "steamncomunity.com", - "steamncomunity.xyz", - "steamnconmunity.com", - "steamnconmunity.ru.com", - "steamnconmunity.work", - "steamnconnmunity.com", - "steamnitro.com", - "steamnitrol.com", - "steamnitros.com", - "steamnitros.ru", - "steamnitrro.com", - "steamnltro.com", - "steamnltros.com", - "steamnltros.ru", - "steamnmcomunnity.co", - "steamocmmunity.me", - "steamoemmunity.com", - "steamoffer-store.xyz", - "steamoffered.trade", - "steamoffergames.xyz", - "steamommunity.com", - "steamoowered.com", - "steamowered.com", - "steampawared.club", - "steampawered.store", - "steampcwered.com", - "steampewared.com", - "steampewered.com", - "steampiwered.com", - "steampoeer.com", - "steampoeerd.com", - "steampoewred.com", - "steampoiwered.com", - "steampoowered.com", - "steampowaered.com", - "steampoward.com", - "steampowder.com", - "steampowed.com", - "steampoweded.com", - "steampoweeed.com", - "steampowened.ru.com", - "steampower.co", - "steampower.de", - "steampower.space", - "steampowerco.com", - "steampowerd.com", - "steampowerd.net", - "steampowerde.com", - "steampowerded.com", - "steampowerdwallet.com", - "steampowere.com", - "steampoweread.com", - "steampowerec.com", - "steampowered-offer.xyz", - "steampowered-offers.xyz", - "steampowered-swap.xyz", - "steampowered-swap1.xyz", - "steampowered-trades.xyz", - "steampowered.company", - "steampowered.de", - "steampowered.freeskins.ru.com", - "steampowered.help", - "steampowered.irl.com.pk", - "steampowered.jcharante.com", - "steampowered.org", - "steampowered.tw", - "steampowered.us", - "steampowered.xyz", - "steampoweredcinema.com", - "steampoweredcommunity.com", - "steampoweredexchange.xyz", - "steampoweredexchanges.xyz", - "steampoweredkey.com", - "steampoweredmarketing.com", - "steampoweredoffer.xyz", - "steampoweredoffers.xyz", - "steampoweredpoetry.com", - "steampoweredshow.com", - "steampoweredswap.xyz", - "steampoweredtrades.xyz", - "steampowereed.com", - "steampowererd.com", - "steampowerered.com", - "steampowerewd.com", - "steampowerred.com", - "steampowers.com", - "steampowers.org", - "steampowerwd.com", - "steampowerwed.com", - "steampowoereid.com", - "steampowored.com", - "steampowrd.com", - "steampowred.ru", - "steampowwered.com", - "steampowwred.com", - "steamppwrred.com", - "steampromo.net.ru", - "steamproxy.net", - "steampunch-twitch.co", - "steampwered.com", - "steampwoered.com", - "steamrccommunity.com", - "steamrcommuniity.com", - "steamrcommunity.ru", - "steamroll.org.ru", - "steamrolll.net.ru", - "steamrolls.net.ru", - "steamrolls.pp.ru", - "steamrommunily.com", - "steamrommunity.org.ru", - "steamru.org", - "steams-community.ru", - "steams-discord.ru", - "steamscommmunity.com", - "steamscommunitey.com", - "steamscommunity.com", - "steamscommunity.pro", - "steamscommunity.ru", - "steamscommunyti.com", - "steamscommynitu.co", - "steamscomnunity.com", - "steamscomnunyti.com", - "steamsconmunity.com", - "steamsdiscord.com", - "steamservice-deals.xyz", - "steamservice-deals1.xyz", - "steamservicedeals.xyz", - "steamservicedeals1.xyz", - "steamshensu.top", - "steamskincs.ru", - "steamsnitro.ru", - "steamsoftware.info", - "steamsommunity.com", - "steamsommunity.ru", - "steamsomunity.com", - "steamsourcecommunity.xyz", - "steamsourcecommunity1.xyz", - "steamstore.map2.ssl.hwcdn.net", - "steamstore.site", - "steamstorecsgo.com", - "steamstorepowered.com", - "steamstoretrade1.xyz", - "steamstradecommunity.xyz", - "steamsupportpowered.icu", - "steamswap.xyz", - "steamtrade-game.xyz", - "steamtrade-home.xyz", - "steamtrade-store.xyz", - "steamtrade-store1.xyz", - "steamtradecommunity.fun", - "steamtradehome.xyz", - "steamtradeoffeer.com", - "steamtradeoffer.net", - "steamtradeprofile.com", - "steamtrades-home.xyz", - "steamtrades-store.xyz", - "steamtrades.com", - "steamtradeshome.xyz", - "steamtradesofer.com", - "steamtradestore.xyz", - "steamtradestore1.xyz", - "steamunlocked.online", - "steamunlocked.pro", - "steamunpowered.com", - "steamuppowered.com", - "steamuserimages-a.akamaid.net", - "steamwalletbd.com", - "steamwalletcodes.net", - "steamwanmeics.ru", - "steamwcommunity.com", - "steamwcommunity.net", - "steamworkspace.com", - "steamzcommunity.com", - "steanammunuty.ml", - "steancammunity.com", - "steancammunity.ru", - "steancammunlte.com", - "steancammunlty.com", - "steancammunyti.com", - "steanccommunity.ru", - "steancimnunity.ru", - "steancommanty.ru.com", - "steancommeuniliy.ru.com", - "steancomminity.com", - "steancomminity.ru", - "steancomminyty.com", - "steancomminyty.ru.com", - "steancommiuniliy.ru.com", - "steancommiunity.com", - "steancommmunity.com", - "steancommnnity.com", - "steancommnuitty.com", - "steancommnuity.com", - "steancommnulty.com", - "steancommnunity.ru", - "steancommnunitytradeoffer.xyz", - "steancommnunlty.ru", - "steancommounity.com", - "steancommrnity.com", - "steancommueniliy.ru.com", - "steancommuhity.com", - "steancommuhity.ru", - "steancommuineliy.ru.com", - "steancommuiniliy.ru.com", - "steancommuinty.ru", - "steancommuinuty.ru", - "steancommuity.com", - "steancommuity.ru", - "steancommumity.com", - "steancommumity.net", - "steancommumlty.com", - "steancommuncity.ru", - "steancommunety.com", - "steancommunety.ru", - "steancommunify.com", - "steancommuniiity.com", - "steancommuniiliy.ru.com", - "steancommuniit.ru.com", - "steancommuniite-xuz.ru", - "steancommuniite.xyz", - "steancommuniitty.com", - "steancommuniity.com", - "steancommuniity.fun", - "steancommuniity.ru", - "steancommunilly.com", - "steancommunilty.com", - "steancommunilty.ru", - "steancommunily.ru", - "steancommunite.site", - "steancommuniti.com.ru", - "steancommuniti.site", - "steancommunitiy.com.ru", - "steancommunitiy.ru", - "steancommunitry.ru", - "steancommunitty.com", - "steancommunitty.xyz", - "steancommunitv.com", - "steancommunity.cc", - "steancommunity.click", - "steancommunity.host", - "steancommunity.link", - "steancommunity.net.ru", - "steancommunity.pw", - "steancommunity.ru.com", - "steancommunity.ru", - "steancommunitytradeaffer.xyz", - "steancommunlity.ru.com", - "steancommunllty.com", - "steancommunlty.business", - "steancommunlty.com", - "steancommunlty.ru.com", - "steancommunlty.ru", - "steancommunmilty.com", - "steancommunniitly.ru", - "steancommunniity.ru", - "steancommunnilty.ru", - "steancommunnily.ru", - "steancommunnitl.ru", - "steancommunnitlly.ru", - "steancommunnity.co", - "steancommunnity.site", - "steancommunnliity.ru", - "steancommunnlity.ru", - "steancommunnlty.com", - "steancommunnlty.ru", - "steancommunnty.com", - "steancommunnuly.me", - "steancommuntiy.ru.com", - "steancommuntly.com", - "steancommunuity.ru", - "steancommunuty.com", - "steancommunyti.com", - "steancommunyti.ru.com", - "steancommurily.xyz", - "steancommutiny.ru", - "steancommuuity.com", - "steancommuuniliiy.ru.com", - "steancommuuniliy.ru.com", - "steancommuunity.com", - "steancommuvity.com", - "steancommynitu.com", - "steancommynity.org.ru", - "steancommynity.ru.com", - "steancommynuti.ru", - "steancommynyty.ru.com", - "steancomnmunity.ru", - "steancomnnunity.com", - "steancomnnunnity.ru", - "steancomnuilty.ru.com", - "steancomnuity.com", - "steancomnumity.com", - "steancomnumlty.com", - "steancomnumlty.ru", - "steancomnuniiity.ru", - "steancomnuniity.com", - "steancomnunilty.ru", - "steancomnunity.com", - "steancomnunity.ru", - "steancomnunitys.ru", - "steancomnunlty.ru", - "steancomnunnity.xyz", - "steancomnunyti.ru.com", - "steancomnunytu.ru.com", - "steancomnunytu.ru", - "steancomnurity.one", - "steancomnurity.xyz", - "steancomnuuniliy.ru.com", - "steancomrnunitiy.com", - "steancomrnunity.com", - "steancomrnunity.ru", - "steancomrnunuty.ru", - "steancomuniiity.com", - "steancomuniite-xuz.ru", - "steancomuniity.com", - "steancomunite-xuz.ru", - "steancomunitiy.ru.com", - "steancomunitly.ru", - "steancomunity.ru.com", - "steancomunitytradeffer.xyz", - "steancomunnity.ru", - "steancomunnity.tk", - "steancomunnlty.me", - "steancomunnlty.ru.com", - "steancomunyiti.ru", - "steancomunyti.ru.com", - "steancomuunity.com", - "steanconmnuity.com", - "steanconmumity.com", - "steanconmumlty.com", - "steanconmunitiy.co", - "steanconmunitly.ru", - "steanconmunity.ru", - "steanconmunlly.ru", - "steanconmunlty.com", - "steanconmunlty.ru", - "steanconmunuty.ru", - "steanconmunuty.xyz", - "steanconmunyti.ru.com", - "steanconmunyti.ru", - "steanconmynmuti.com", - "steanconnunitly.xyz", - "steanconnunity.com", - "steanconnunlty.com", - "steancoommuniity.xyz", - "steancoommunity.com", - "steancoommunity.xyz", - "steancoommunitytradeofferr.com", - "steancoommunnity.com", - "steancoomnuity.com", - "steancoomnunity.com", - "steancoomunnity.com", - "steancornminuty.com", - "steancornmunuty.ru", - "steancouminnuty.org", - "steanecommunlty.site", - "steanfocuak.ru", - "steanfocusd.xyz", - "steanfocusi.ru", - "steanfocusk.ru", - "steanfocusse.ru", - "steanfocussi.ru", - "steanmcommuniitiy.ru", - "steanmcommunily.ru", - "steanmcommunity.com", - "steanmcommunity.ru.com", - "steanmcommunity.ru", - "steanmcommuniuty.ru.com", - "steanmcommunlty.ru", - "steanmcommunlty.xyz", - "steanmcommzunity.ru", - "steanmcomnuinmty.com", - "steanmcomnuity.com", - "steanmcomnumntiy.com", - "steanmcomnumty.com", - "steanmcomnunitiy.com", - "steanmcomnunity.com", - "steanmcomnynuytiy.org.ru", - "steanmcomrninuty.xyz", - "steanmcomumnity.xyz", - "steanmcomunitly.ru", - "steanmconmunity.com", - "steanmconmunnity.ru", - "steanmconnynuytiy.net.ru", - "steanmconynnuytiy.net.ru", - "steanmconynnuytiy.org.ru", - "steanmecommunity.com", - "steanmncommunity.com", - "steanmncomnunity.com", - "steanncammunlte.com", - "steanncammunlte.ru", - "steanncmmunytiy.ru", - "steanncomminity.ru.com", - "steanncommity.co", - "steanncommiuty.com", - "steanncommnunyti.com", - "steanncommuiniuty.com", - "steanncommunily.com", - "steanncommunitv.com", - "steanncommunity.com", - "steanncommuniuity.com", - "steanncommunlty.com", - "steanncomnmunity.com", - "steanncomnuniity.com", - "steanncomnuniity.online", - "steanncomnuniity.ru", - "steanncomnuniity.xyz", - "steanncomnunity.xyz", - "steanncomunitiy.ru.com", - "steanncomunitli.ru.com", - "steanncomunitly.co", - "steanncomunitly.ru.com", - "steanncomunitly.ru", - "steanncomunitty.site", - "steanncomunity.com", - "steanncomunnity.ru", - "steannconmunity.com", - "steannconnmunity.com", - "steannconnnnunity.net.ru", - "steannconnnunity.com", - "steannconnunynity.ru", - "steannecomunlty.com", - "steanpowered.net.ru", - "steanpowered.xyz", - "steanrcommunitiy.com", - "steapowered.com", - "steappowered.com", - "stearamcomminnity.net", - "stearamcomnunitu.xyz", - "stearcommity.com", - "stearcommuity.com", - "stearcommunitly.com", - "stearmcammunity.com", - "stearmcommnity.com", - "stearmcommnumity.com", - "stearmcommnunity.com", - "stearmcommnunnity.org", - "stearmcommrunity.com", - "stearmcommuniity.com", - "stearmcommuniity.ru.com", - "stearmcommuninty.com", - "stearmcommunitly.ru", - "stearmcommunitry.cf", - "stearmcommunitty.ru.com", - "stearmcommunity.com", - "stearmcommunity.one", - "stearmcommunity.ru.com", - "stearmcommunltly.com", - "stearmcommunnitty.online", - "stearmcommunnity.ru.com", - "stearmcommuunity.ru.com", - "stearmcommuunity.ru", - "stearmcommuunnity.ru", - "stearmcommynity.fun", - "stearmcomrmunity.co", - "stearmcomrmunity.com", - "stearmcomrnunitiy.com", - "stearmcomrnunity.com", - "stearmconmmunity.com", - "stearmconmunity.ru", - "stearmconmunnity.com", - "stearmconnrnunity.com", - "stearmcormmunity.com", - "stearmcornmunitiy.com", - "stearmcornmunity.ru", - "stearmcornmunlty.com", - "stearmcornnnunity.com", - "stearmmcommuniity.ru", - "stearmmcomunitty.ru", - "stearmmcomunity.ru", - "stearmmcomuunity.ru", - "stearncomiunity.ru", - "stearncomminhty.com", - "stearncomminutiu.ru", - "stearncomminuty.click", - "stearncomminuty.com", - "stearncomminuty.link", - "stearncomminuty.ru.com", - "stearncomminuty.ru", - "stearncomminytu.com", - "stearncommiunity.com", - "stearncommiuty.co", - "stearncommmnuity.xyz", - "stearncommmunity.online", - "stearncommmunity.ru", - "stearncommninuty.com", - "stearncommnniity.com", - "stearncommnniity.ru", - "stearncommnnity.co.uk", - "stearncommnnity.com", - "stearncommnuinty.com", - "stearncommnuity.ru.com", - "stearncommnunity.ru.com", - "stearncommonity.ru", - "stearncommrunity.com", - "stearncommubity.com", - "stearncommuinuty.co", - "stearncommumitly.com", - "stearncommumity.com", - "stearncommumlty.com", - "stearncommunety.com", - "stearncommunety.ru", - "stearncommungty.com", - "stearncommunhty.com", - "stearncommunigy.com", - "stearncommuniitty.xyz", - "stearncommuniity.click", - "stearncommuniity.ru", - "stearncommuniity.site", - "stearncommuniityt.click", - "stearncommunilly.site", - "stearncommunilty.ru", - "stearncommunilty.site", - "stearncommunily.ru", - "stearncommunily.website", - "stearncommuninity.com", - "stearncommuniry.com", - "stearncommunite.com", - "stearncommunitey.com", - "stearncommunitey.ru", - "stearncommunitly.ru", - "stearncommunitly.website", - "stearncommunitly.xyz", - "stearncommunity.click", - "stearncommunity.link", - "stearncommunity.net.ru", - "stearncommunity.ru", - "stearncommunivy.com", - "stearncommunjty.com", - "stearncommunlity.com", - "stearncommunlty.ru", - "stearncommunlty.site", - "stearncommunlty.store", - "stearncommunnitty.xyz", - "stearncommunnity.ru", - "stearncommunnity.xyz", - "stearncommunrty.com", - "stearncommuntity.com", - "stearncommuntiy.com", - "stearncommuntty.com", - "stearncommunuitiy.com", - "stearncommunuity.net.ru", - "stearncommunutiy.com", - "stearncommunyti.ru", - "stearncommunytiy.ru", - "stearncommunytiyu.ru", - "stearncommurity.ru", - "stearncommutiny.online", - "stearncommutiny.ru", - "stearncommuty.com", - "stearncommynitu.ru.com", - "stearncommynity.fun", - "stearncommynity.ru.com", - "stearncomnmunity.com", - "stearncomnnunity.fun", - "stearncomnnunity.site", - "stearncomnnunity.website", - "stearncomnnunty.com.ru", - "stearncomnumity.com", - "stearncomnunily.com", - "stearncomnunitu.ru", - "stearncomnunitv.ru.com", - "stearncomnunity.com", - "stearncomnunity.org", - "stearncomnunity.ru.com", - "stearncomnunnity.ru", - "stearncomrmunity.co", - "stearncomrmunity.com", - "stearncomrmynity.fun", - "stearncomrninuty.ru", - "stearncomrninuty.xyz", - "stearncomrnrunity.ru.com", - "stearncomrnrunity.ru", - "stearncomrnunety.com", - "stearncomrnunitly.site", - "stearncomrnunitly.xyz", - "stearncomrnunity.com", - "stearncomrnunity.ru", - "stearncomrnunity.store", - "stearncomrnunlity.ru", - "stearncomrnunlty.site", - "stearncomrnunyti.ru", - "stearncomrrnunity.com", - "stearncomrrunity.com", - "stearncomrunity.ru.com", - "stearncomrunity.ru", - "stearncomunitu.ru", - "stearncomunlty.ru.com", - "stearncomynity.ru", - "stearnconmumity.com", - "stearnconmunity.com", - "stearnconmunity.me", - "stearnconmunity.net", - "stearnconmuntiy.ru", - "stearnconmuuity.com", - "stearnconmuulty.ru", - "stearnconnrnunity.xyz", - "stearnconrmunity.com", - "stearncormmunity.com", - "stearncormmunity.ru", - "stearncormunity.ru", - "stearncormunniti.org", - "stearncornminuty.com", - "stearncornminuty.ru", - "stearncornmnuity.ru", - "stearncornmrunity.ru.com", - "stearncornmunitiy.com", - "stearncornmunitly.com", - "stearncornmunity.com", - "stearncornmunity.net", - "stearncornmunity.ru.com", - "stearncornmunity.ru", - "stearncornmunlty.ru", - "stearncornmunuty.ru", - "stearncornmurnity.ru.com", - "stearncornnumyty.com", - "stearncornnunity.ru", - "stearncornrnnity.ru.com", - "stearncornrnuity.com", - "stearncornrnunity.com", - "stearncornrnunity.ru.com", - "stearncornunity.ru", - "stearncornunity.xyz", - "stearncornurniity.xyz", - "stearncorrmunity.com", - "stearncurnmunity.com", - "stearnmcommunnity.com", - "stearnmcomunity.com", - "stearnncomrnunitiy.com", - "stearnncomrnunity.com", - "stearnporewed.ru.com", - "stearnpovvered.com", - "stearnpowered.online", - "stearnpowered.xyz", - "steasmpowered.com", - "steawcammunity.xyz", - "steawcommunity.com", - "steawcommunity.net", - "steawcomunity.net", - "steawconnunity.xyz", - "steawmcommunity.net", - "steawmcomnunnity.ru", - "steawmcomuunity.ru", - "steawmcowmunnity.ru", - "steawmpowered.com", - "steawncomnunity.ru", - "steawpowered.com", - "steawscommunity.net", - "steaxmcommity.com", - "steeaamcomunity.xyz", - "steeacmcommumitiy.com", - "steeamcommmunety.com", - "steeamcommmunitty.site", - "steeamcommmunity.com", - "steeamcommuinitty.com", - "steeamcommunity.me", - "steeamcommunity.ml", - "steeamcommunity.ru.com", - "steeamcommunlity.com", - "steeamcommunlity.ru", - "steeamcommunllty.xyz", - "steeamcommunlty.com", - "steeamcommunnity.ru.com", - "steeamcommunnity.ru", - "steeamcommunnlty.ru", - "steeamcommunnuity.ru.com", - "steeamcommunyti.com", - "steeamcomnnunity.com", - "steeamcomuneety.com", - "steeamcomunitty.com", - "steeamcomunity.net", - "steeamcomunlty.ru.com", - "steeamcomunlty.ru", - "steeamcomunnlty.com", - "steeamcoommunity.ru", - "steeammcomunity.com", - "steeammcomunlty.com", - "steeampowered.tk", - "steeamwins.xyz", - "steemacommunity.com", - "steemcammunllty.com", - "steemcammunlly.com", - "steemcammunlty.com", - "steemcommmunety.com", - "steemcommmunity.com", - "steemcommnnity.com", - "steemcommnunity.ru", - "steemcommnunnity.ru.com", - "steemcommuinty.com", - "steemcommuniity.com", - "steemcommunily.ru.com", - "steemcommuninity.org.ru", - "steemcommuniry.com", - "steemcommunitey.com", - "steemcommuniti.com", - "steemcommunitry.com", - "steemcommunity.co", - "steemcommunity.com", - "steemcommunity.ru.com", - "steemcommunityy.com", - "steemcommuniy.com", - "steemcommunllty.com", - "steemcommunlty.com", - "steemcommunly.com", - "steemcommunnity.co", - "steemcommunnity.net", - "steemcommuntiy.ru.com", - "steemcommuntiy.ru", - "steemcommunty.net.ru", - "steemcommunty.org.ru", - "steemcommunty.pp.ru", - "steemcommunty.ru", - "steemcommuunity.com", - "steemcommynity.ru", - "steemcomnmunity.com", - "steemcomnrunity.com", - "steemcomrnunity.co", - "steemcomrnunity.com", - "steemcomrunity.ru", - "steemcomunatlytradeoffer40034231.ru", - "steemcomuniti.com", - "steemcomuniti.ru", - "steemcomunity.me", - "steemcomunity.net.ru", - "steemcomunity.org.ru", - "steemcomunity.pp.ru", - "steemcomunnity.com", - "steemconnunity.com", - "steemcoommunity.com", - "steemcoommunity.ru", - "steemcoommunlty.ru", - "steemcoommuntiy.ru", - "steemcoommunty.ru", - "steemcoomnunty.ru", - "steemcoomunity.xyz", - "steemcoomuntiy.ru", - "steemcoomuunity.ru", - "steemcoonmuntiy.ru", - "steemcowwunity.xyz", - "steempowerd.ru", - "steempowered.com", - "steemurl.com", - "steencommunilty.com", - "steencommunityy.xyz", - "steiamcommuinity.com", - "steiamcommunityi.com", - "steimcomnunnity.ru.com", - "stemacommunity.net", - "stemacommunlty.com", - "stemacomunity.com", - "stemapowered.com", - "stemcammuniety.ru", - "stemcammuniity.com", - "stemcammuniity.ru", - "stemcamnunity.com", - "stemcamnunity.ru", - "stemccomnmunity.com", - "stemcomiunity.ru", - "stemcomminity.com", - "stemcomminuty.ru", - "stemcommlunity.com", - "stemcommnuity.ru.com", - "stemcommnunity.com", - "stemcommnunity.ru.com", - "stemcommnunlty.ru", - "stemcommnunnity.com", - "stemcommnunulty.com", - "stemcommnuunity.com", - "stemcommouniity.com", - "stemcommounilty.com", - "stemcommounity.ru.com", - "stemcommuinty.ru", - "stemcommuniby.com", - "stemcommuniety.com", - "stemcommuniity.com", - "stemcommuniity.ru", - "stemcommunilty.com", - "stemcommunilty.ru", - "stemcommunite.pp.ru", - "stemcommuniti.ru", - "stemcommunitiy.com", - "stemcommunitly.com", - "stemcommunitty.com", - "stemcommunitty.ru.com", - "stemcommunity.com.ru", - "stemcommunity.ru.com", - "stemcommunity.ru", - "stemcommunitytraade.xyz", - "stemcommunitytrade.com", - "stemcommunitytrade.fun", - "stemcommunjty.com", - "stemcommunlitly.com", - "stemcommunlity.ru", - "stemcommunlty.com", - "stemcommunlty.ru.com", - "stemcommunlty.space", - "stemcommunniity.com", - "stemcommunnilty.com", - "stemcommunnitiy.net.ru", - "stemcommunnity.com.ru", - "stemcommunnity.com", - "stemcommunuity.com", - "stemcommununity.com", - "stemcommuty.ru", - "stemcommuunity.com.ru", - "stemcommynity.ru.com", - "stemcommyunity.ru", - "stemcomnmnnunity.com", - "stemcomnmnunity.com", - "stemcomnmounity.com", - "stemcomnmuity.com", - "stemcomnmuniity.com", - "stemcomnmuniity.ru.com", - "stemcomnmunity.com.ru", - "stemcomnmunity.ru.com", - "stemcomnmunity.ru", - "stemcomnmunniity.com", - "stemcomnmunnity.com", - "stemcomnmunuity.com", - "stemcomnmununity.com", - "stemcomnmuunity.com", - "stemcomnmuunity.ru.com", - "stemcomnnmunity.com", - "stemcomnnmunnity.com", - "stemcomnnmuunity.ru", - "stemcomnuniti.ru", - "stemcomnunity.com", - "stemcomnunity.ru.com", - "stemcomnunity.ru", - "stemcomnunyti.ru.com", - "stemcomrnmunity.com", - "stemcomrnuniity.ru", - "stemcomuniti.ru", - "stemcomunitiy.com", - "stemcomunity.com", - "stemcomunity.net", - "stemcomunity.ru.com", - "stemcomunnity.com.ru", - "stemcomunnity.com", - "stemcomunnity.ru.com", - "stemconmmnunity.com", - "stemconmmunity.com", - "stemconmmunnity.com", - "stemconmmuunnity.com", - "stemconmnmuunity.com", - "stemconmuite.xyz", - "stemconmumity.ru", - "stemcoominuty-alirdrop.xyz", - "stemcoommounity.com", - "stemcoommuniity.com", - "stemcoommunity.com", - "stemcoommuunnity.com", - "stemcoomnmnunity.com", - "stemcoomnmounity.com", - "stemcoomnmuniity.com", - "stemcoomnmunity.com", - "stemcoomnmunity.ru.com", - "stemcoomnmunnity.com", - "stemcoomnnunity.com", - "stemcormmunity.com", - "stemcormmunlty.ru.com", - "stemcornmunitly.ru.com", - "stemcornmunity.com", - "stemcornmunity.ru.com", - "stemcornmunity.ru", - "stemcornmunlty.xyz", - "stemcummnuity.ru.com", - "stemcummnunity.ru.com", - "stemcummunity.com.ru", - "stemcummunity.ru.com", - "stemcummunnity.com.ru", - "stemcummunnity.ru.com", - "stemcumnmunity.com.ru", - "stemcumnmunity.com", - "stemcumnmunity.ru.com", - "stemcumunnity.ru.com", - "stemecommunlty.com", - "stemmcomunity.xyz", - "stemmcomunnityy.xyz", - "stemncornmunity.com", - "stemsell.ml", - "stencommunity.com", - "stenmcommunilty.ru.com", - "stenmcommunitly.ru.com", - "stenncornmuniy.com", - "stennicommuitun.com", - "steomcommunitey.com", - "steomcommunito.con", - "steomcommunity.com", - "steomcommunity.ru", - "steomcommunlty.ml", - "steomcomnunity.ru.com", - "steomconmunity.com", - "steomcoommynity.ru.com", - "stepmscononnity.com", - "steqmcommunity.com", - "steqmpowered.com", - "steramconmunity.com", - "sterampowered.com", - "stermccommunitty.ru", - "stermcommuniity.com", - "stermcommunilty.ru.com", - "stermcommunity.com", - "stermcommunity.ru.com", - "stermcommunityy.ru", - "stermcommunlity.ru.com", - "stermcommunnitty.ru", - "stermcomunitte.xyz", - "stermcomunniity.ru", - "stermconmmunity.com", - "stermmcomuniity.ru", - "stermncommunity.com", - "sterncommunilty.ru.com", - "sterncommunilty.site", - "sterncommunnity.ru", - "sterncommynuty.ru", - "sterncomnurity.one", - "sternconmunity.ru", - "sterncornmunity.ru", - "sternmcommunity.com", - "sternmconmunity.com", - "sternmcornmmunity.com", - "sternmcornnunity.com", - "sterumcommunity.com", - "stetrncommity.com", - "steumcommunity.com", - "steumcommunity.ru", - "steumcornmunity.com", - "steurmcommunity.com", - "steurmconmunity.com", - "stewie2k-giveaway-150days.pro", - "stewmpowered.com", - "stfriendprofile.ru", - "stg.steamcpowered.com", - "stheamcommnitiy.ru", - "stheamcommuniti.com", - "stheamcommunity.ru", - "stheamcommunutiy.ru", - "stheamcommunutly.ru", - "stheamcomunitly.ru", - "stheamcomunutly.ru", - "stheamconmuniity.com", - "stheamconnmunutly.ru", - "stheamcornmunitiy.ru", - "stiamcammunieti.com", - "stiamcommunitly.xyz", - "stiamcommunity.com", - "stiamcommyunlty.ru.com", - "stiamcomunity.xyz", - "stiamcomunlty.ru", - "stiamcomynity.com", - "stieamcommuinity.com", - "stieamcommuniity.com", - "stieamcommuniity.ru", - "stieamcommunitey.ru", - "stieamcommunitiy.com", - "stieamcommunity.com", - "stieamcommunity.org.ru", - "stieamcommunity.pp.ru", - "stieamcommuunitey.us", - "stieamcommynituy.com", - "stieamcomnnunity.com", - "stieamcomuniiti.ru", - "stieamcomunity.com", - "stieamconmuniity.com", - "stieamconnmunity.com", - "stieamcormnynity.ru.com", - "stiemcommunitty.ru", - "stiemconnumity.xyz", - "stimcommunity.ru", - "stimcommunlty.ru", - "stimiache.ru", - "stjeamcoimmunity.com", - "stjeamcommunity.ru", - "stjeamcomnuminiti.ru", - "stjeamcomnunitiy.ru", - "stjeamcomnunity.ru", - "stjeamcomuniity.ru", - "stjeamconmunnitii.com", - "stleaamcommunity.com", - "stleam-communithy.com", - "stleamcommiunity.ru.com", - "stleamcommiynitu.ru", - "stleamcommiynitu.xyz", - "stleamcommiynity.xyz", - "stleamcommnunity.ru", - "stleamcommulnity.xyz", - "stleamcommulnitycom.xyz", - "stleamcommuneety.com", - "stleamcommuniity.com", - "stleamcommuniity.net", - "stleamcommunilty.com", - "stleamcommunithy.com", - "stleamcommunitiy.com", - "stleamcommunitly.com", - "stleamcommunitty.com", - "stleamcommunity.com", - "stleamcommunity.net", - "stleamcommunlty.com", - "stleamcommunlty.xyz", - "stleamcomnmunity.ru.com", - "stleamcomnunity.ru.com", - "stleamcomunity.com", - "stleamconminity.online", - "stleamconminity.ru", - "stleamconmmunity.ru.com", - "stleamconmmunlty.net.ru", - "stleamconmunity.com", - "stleamconnunlty-tyztradeoffernewpartnhr15902271.xyz", - "stleamcormmunity.ru.com", - "stleamcormmynity.ru.com", - "stleamcormunity.ru.com", - "stleamcornmmunity.ru.com", - "stleammcomnnunitycom.buzz", - "stleamncommunity.ru", - "stleancommunity.ru", - "stleanmcommunity.ru", - "stleaomcoommynity.ru.com", - "stlemamcornmunty.me", - "stmawards.xyz", - "stmcornnunnitty.xyz", - "stmcornumnunitty.xyz", - "stmeacomunnitty.ru", - "stmemcomyunity.com", - "stmencommunity.ru", - "stmtrdoffer.xyz", - "stoacommunity.codes", - "stoemcommunity.com", - "stopify.com", - "store-communitiy.com", - "store-discord.com", - "store-steam-csgo.ru", - "store-steamcomminuty.ru.com", - "store-steamcommunity.xyz", - "store-steamcomnunity", - "store-steampoweered.ru", - "store-steampowereb.com", - "store-steampowered.ru", - "store-stempowered.com", - "store-streampowered.me", - "store.stampowered.com", - "store.stempowerd.com", - "storeesteampowered.ru.com", - "storeesteampowereed.ru.com", - "stores-steampowered.com", - "storesleampowecommunity.store", - "storesteam-csgo.ru", - "straemcommonlity.com", - "straemcomunnitry.ru", - "straemcummonilty.com", - "straemcummonity.com", - "stramconmunity.com", - "strcomnunnitly.xyz", - "streaalcommuunnitu.ru", - "streaemcrommunlty.com.ru", - "stream-conmunlty.ru", - "streamc0mmunnlty.xyz", - "streamcammunitly.com", - "streamccomunilty.com", - "streamcolmnty.xyz", - "streamcomlutitly.me", - "streamcomminuty.pw", - "streamcomminuty.ru.com", - "streamcommiumity.com", - "streamcommiunity.com", - "streamcommiunnity.com", - "streamcommlunity.ru.com", - "streamcommmumnity.ru.com", - "streamcommmunify.ru.com", - "streamcommmunitty.ru.com", - "streamcommmunity.com", - "streamcommmunjty.ru.com", - "streamcommmunlty.ru.com", - "streamcommmunnlty.ru.com", - "streamcommnnity.com", - "streamcommnnuity.com", - "streamcommnnutiy.com", - "streamcommnuity.com", - "streamcommnuity.ru", - "streamcommnunilty.com", - "streamcommnunitly.com", - "streamcommnunity.ru", - "streamcommnunlity.ru", - "streamcommnunnity.ml", - "streamcommnunuty.ru.com", - "streamcommnunuty.ru", - "streamcommonlty.ru.com", - "streamcommounity.com", - "streamcommuinity.com", - "streamcommuinty.com", - "streamcommuiny.ru", - "streamcommulinty.com", - "streamcommulnty.com", - "streamcommumity.ru.com", - "streamcommumninty.com", - "streamcommumnity.com", - "streamcommumtiy.ru", - "streamcommunaly.com", - "streamcommunaty.com", - "streamcommuneiley.net", - "streamcommunetly.com", - "streamcommunety.ru", - "streamcommunicate.ru", - "streamcommunication.com", - "streamcommunify.com", - "streamcommuniiley.net.ru", - "streamcommuniiley.net", - "streamcommuniily.com", - "streamcommuniitty.com", - "streamcommuniitu.com", - "streamcommuniity.org", - "streamcommuniity.ru.com", - "streamcommuniity.ru", - "streamcommuniityy.me", - "streamcommuniley.net.ru", - "streamcommuniley.net", - "streamcommuniliey.net.ru", - "streamcommuniliey.xyz", - "streamcommuniliiey.net.ru", - "streamcommuniliiey.org.ru", - "streamcommuniliiey.pp.ru", - "streamcommuniliiy.org.ru", - "streamcommuniliiy.pp.ru", - "streamcommunillty.com", - "streamcommunilly.com", - "streamcommunilty.com", - "streamcommunilty.xyz", - "streamcommunily.cc", - "streamcommunily.co", - "streamcommunily.com", - "streamcommunily.icu", - "streamcommunily.me", - "streamcommunily.net", - "streamcommunily.ru.com", - "streamcommunimty.com", - "streamcommuninllty.com", - "streamcommuninnity.com", - "streamcommuninnuity.com", - "streamcommuninty.com", - "streamcommuninty.me", - "streamcommuninuty.store", - "streamcommunit.com", - "streamcommunit.ru.com", - "streamcommunite.com", - "streamcommunite.ru.com", - "streamcommunitey.com", - "streamcommuniti.ru", - "streamcommuniti.xyz", - "streamcommunitily.com", - "streamcommunitiy.com", - "streamcommunitiy.net", - "streamcommunitiy.ru.com", - "streamcommunitiy.ru", - "streamcommunitly.net", - "streamcommunitly.ru", - "streamcommunitly.xyz", - "streamcommunitry.ru", - "streamcommunitty.ru.com", - "streamcommunitu.com", - "streamcommunitv.me", - "streamcommunitv.net", - "streamcommunity-user.me", - "streamcommunity.com.ru", - "streamcommunity.me", - "streamcommunity.net.ru", - "streamcommunity.one", - "streamcommunity.org.ru", - "streamcommunity.pl", - "streamcommunity.ru.com", - "streamcommunityi.ru", - "streamcommunityy.me", - "streamcommuniunity.com", - "streamcommuniuty.ru.com", - "streamcommuniuty.store", - "streamcommuniy.ru", - "streamcommunjty.com", - "streamcommunjty.ru.com", - "streamcommunlity.ru", - "streamcommunliy.com", - "streamcommunlte.ru", - "streamcommunltiy.com", - "streamcommunlty.net", - "streamcommunly.com", - "streamcommunly.me", - "streamcommunly.net", - "streamcommunly.ru", - "streamcommunminty.com", - "streamcommunmity.com", - "streamcommunniity.com", - "streamcommunnilty.com", - "streamcommunnitty.com", - "streamcommunnity.org", - "streamcommunnty.com", - "streamcommunnty.me", - "streamcommunnuitty.com", - "streamcommuntiiy.org", - "streamcommuntiy.com", - "streamcommuntly.com", - "streamcommuntly.net.ru", - "streamcommuntly.org.ru", - "streamcommuntly.pp.ru", - "streamcommunttly.com", - "streamcommunty.co", - "streamcommunty.me", - "streamcommunty.ru", - "streamcommunuitty.com", - "streamcommunuity.net", - "streamcommununty.com", - "streamcommuny.ru", - "streamcommunyty.com", - "streamcommutiny.net", - "streamcommuuniity.com", - "streamcommuunilty.ru.com", - "streamcommuunity.com", - "streamcommuunniity.com", - "streamcommuunnity.com", - "streamcommuunnity.net", - "streamcommuuty.ru", - "streamcommynitu.com", - "streamcommynuty.com", - "streamcomninuty.xyz", - "streamcomnmunity.ru.com", - "streamcomnmunnity.ru.com", - "streamcomnnunity.net", - "streamcomnnunity.website", - "streamcomnnunity.xyz", - "streamcomnnunlty.com", - "streamcomnnunuty.com", - "streamcomnully.net.ru", - "streamcomnully.org.ru", - "streamcomnullyty.net.ru", - "streamcomnullyty.org.ru", - "streamcomnullyty.pp.ru", - "streamcomnultyy.net.ru", - "streamcomnultyy.org.ru", - "streamcomnumity.ru", - "streamcomnumnity.ru.com", - "streamcomnunely.com", - "streamcomnunetiy.com", - "streamcomnuniity.com", - "streamcomnuniity.net", - "streamcomnunitiy.ru", - "streamcomnunitly.ru", - "streamcomnunitry.ru", - "streamcomnunitty.com", - "streamcomnunity.ru", - "streamcomnunity.site", - "streamcomnuniuty.com", - "streamcomnunlity.com", - "streamcomnunlty.ru", - "streamcomnunnity.ru", - "streamcomnunuty.com", - "streamcomnunuty.ru", - "streamcomnunyti.xyz", - "streamcomrnunitiy.ru", - "streamcomrnunity.com", - "streamcomrnunity.online", - "streamcomrnunity.ru", - "streamcomulty.net.ru", - "streamcomulty.org.ru", - "streamcomuniitty.ru.com", - "streamcomuniity.cf", - "streamcomuniity.com", - "streamcomuniity.net", - "streamcomuniity.pp.ua", - "streamcomunilty.net.ru", - "streamcomunilty.org.ru", - "streamcomunily.net.ru", - "streamcomunily.org.ru", - "streamcomunily.pp.ru", - "streamcomunitly.com", - "streamcomunitly.net.ru", - "streamcomunitly.net", - "streamcomunitly.ru", - "streamcomunitry.com", - "streamcomunitty.net", - "streamcomunitu.ru", - "streamcomunity.com", - "streamcomunity.fun", - "streamcomunity.net", - "streamcomunity.org", - "streamcomunity.ru.com", - "streamcomunlty.net.ru", - "streamcomunlty.org.ru", - "streamcomunlty.pp.ru", - "streamcomunltyy.org.ru", - "streamcomunltyy.pp.ru", - "streamcomunniity.net.ru", - "streamcomunnity.pp.ua", - "streamcomunnity.ru.com", - "streamcomunnity.xyz", - "streamcomuuniltyy.org.ru", - "streamcomuuniltyy.pp.ru", - "streamcomuunltyy.net.ru", - "streamcomuunltyy.org.ru", - "streamcomuunltyy.pp.ru", - "streamcomynity.com", - "streamcomynity.ru.com", - "streamconmmunity.com", - "streamconmmunity.ru.com", - "streamconmumuty.xyz", - "streamconmunilty.com", - "streamconmunitly.com", - "streamconmunitly.ru", - "streamconmunity.com", - "streamconmunlity.com", - "streamconmunlty.ru", - "streamconmunyti.com", - "streamconnmunity.com", - "streamconnuity.com", - "streamconnumity.com", - "streamconnunitly.com", - "streamconnunity.net.ru", - "streamconnunity.ru", - "streamconnunity.site", - "streamconnunity.us", - "streamconunity.net.ru", - "streamcoommounity.com", - "streamcoommuniity.xyz", - "streamcoommunity.com", - "streamcoommunity.net", - "streamcoommunity.xyz", - "streamcormmunity.com", - "streamcormmunity.ru.com", - "streamcormmunlty.ru.com", - "streamcormmunnity.ru.com", - "streamcormmyniity.ru.com", - "streamcormnmunity.ru.com", - "streamcormunnity.ru.com", - "streamcornnunitly.co", - "streamcornnunitly.com", - "streamcoumunniity.org", - "streamcoumunnity.org", - "streamcrommunify.me", - "streamcummonity.ru.com", - "streamcummunity.ru.com", - "streamcummunlty.com", - "streamcummunlty.xyz", - "streamecommuniity.com", - "streamecommunity.com", - "streammcommunity.ru", - "streammcomunittty.ru", - "streammcomunity.com", - "streammcomunnity.ru", - "streammcomuunity.ru", - "streammcornmunnity.com", - "streamncommnunity.com", - "streamnconmumity.com", - "streamnconmunity.com", - "streamnconmunity.ru", - "streampoered.com", - "streampowered.store", - "streampowereed.com", - "streancommumity.ru.com", - "streancommuniity.ru.com", - "streancommuniliy.ru.com", - "streancommuniliy.ru", - "streancommunitiy.co", - "streancommunitiy.net.ru", - "streancommunitiy.ru", - "streancommunity.ru.com", - "streancommunuty.ru", - "streancomunnitiy.com", - "streancomunnuty.com", - "streancoommunity.com", - "streancoommunity.xyz", - "streanncomminity.ru", - "streanncommunity.space", - "streanncomnnunuty.com", - "streanncomunity.ru", - "strearmcommunity.ru", - "strearmcomunity.ru", - "strearncomuniity.ru.com", - "streawcommunity.xyz", - "streeamcommunuti.ru", - "streemcommunhity.org.ru", - "streemcommunitiy.ru.com", - "strempowered.com", - "streomcommunuty.com", - "strieamcommunniity.com", - "striieamcomnmunniitty.ru", - "stteamcommiunity.com", - "stteamcommunitty.com", - "stteamcommunity.net", - "sttemcomnmuty.ru.com", - "stuamcommnuity.com", - "stuamcommunity.com", - "stuemconmunity.com", - "sturemconmunity.com", - "stwsmarket.ru", - "styamcommunity.com", - "styeampowerd.com", - "styeampowered.com", - "stzeamcomnumiti.ru", - "sueamcommunity.com", - "sueamconmunity.com", - "sufficienttime.rocks", - "summer-rust.xyz", - "sunnygamble.com", - "superbalancednow.com", - "superdealgadgets.com", - "support.verifiedbadgehelp-form.ml", - "supremeskins.cf", - "surveysandpromoonline.com", - "swapskins.ga", - "swapskins.live", - "swapslot.tk", - "sweet-fortune.ru", - "ta-sty.info", - "taceitt.com", - "tacelt.com", - "tacticalusa.com", - "takeit100.xyz", - "takeit101.xyz", - "takeit102.xyz", - "takeit103.xyz", - "takeit104.xyz", - "takeit105.xyz", - "takeit106.xyz", - "takeit107.xyz", - "takeit108.xyz", - "takeit109.xyz", - "takeit110.xyz", - "takeit111.xyz", - "takeit112.xyz", - "takeit113.xyz", - "takeit114.xyz", - "takeit115.xyz", - "takeit116.xyz", - "takeit117.xyz", - "takeit118.xyz", - "takeit119.xyz", - "takeit120.xyz", - "takeit121.xyz", - "takeit122.xyz", - "takeit123.xyz", - "takeit124.xyz", - "takeit125.xyz", - "takeit126.xyz", - "takeit127.xyz", - "takeit128.xyz", - "takeit129.xyz", - "takeit130.xyz", - "takeit131.xyz", - "takeit132.xyz", - "takeit133.xyz", - "takeit134.xyz", - "takeit135.xyz", - "takeit136.xyz", - "takeit137.xyz", - "takeit138.xyz", - "takeit139.xyz", - "takeit140.xyz", - "takeit141.xyz", - "takeit142.xyz", - "takeit143.xyz", - "takeit144.xyz", - "takeit145.xyz", - "takeit146.xyz", - "takeit147.xyz", - "takeit148.xyz", - "takeit149.xyz", - "takeit150.xyz", - "takeit151.xyz", - "takeit152.xyz", - "takeit153.xyz", - "takeit154.xyz", - "takeit155.xyz", - "takeit156.xyz", - "takeit157.xyz", - "takeit158.xyz", - "takeit159.xyz", - "takeit160.xyz", - "takeit161.xyz", - "takeit162.xyz", - "takeit163.xyz", - "takeit164.xyz", - "takeit165.xyz", - "takeit166.xyz", - "takeit167.xyz", - "takeit168.xyz", - "takeit169.xyz", - "takeit170.xyz", - "takeit171.xyz", - "takeit172.xyz", - "takeit173.xyz", - "takeit174.xyz", - "takeit175.xyz", - "takeit176.xyz", - "takeit177.xyz", - "takeit178.xyz", - "takeit179.xyz", - "takeit20.xyz", - "takeit21.xyz", - "takeit22.xyz", - "takeit23.xyz", - "takeit24.xyz", - "takeit25.xyz", - "takeit26.xyz", - "takeit260.xyz", - "takeit261.xyz", - "takeit262.xyz", - "takeit263.xyz", - "takeit264.xyz", - "takeit265.xyz", - "takeit266.xyz", - "takeit267.xyz", - "takeit268.xyz", - "takeit269.xyz", - "takeit27.xyz", - "takeit270.xyz", - "takeit271.xyz", - "takeit272.xyz", - "takeit273.xyz", - "takeit274.xyz", - "takeit275.xyz", - "takeit276.xyz", - "takeit277.xyz", - "takeit278.xyz", - "takeit279.xyz", - "takeit28.xyz", - "takeit280.xyz", - "takeit281.xyz", - "takeit282.xyz", - "takeit283.xyz", - "takeit284.xyz", - "takeit285.xyz", - "takeit286.xyz", - "takeit287.xyz", - "takeit288.xyz", - "takeit289.xyz", - "takeit29.xyz", - "takeit290.xyz", - "takeit291.xyz", - "takeit292.xyz", - "takeit293.xyz", - "takeit294.xyz", - "takeit295.xyz", - "takeit296.xyz", - "takeit297.xyz", - "takeit298.xyz", - "takeit299.xyz", - "takeit30.xyz", - "takeit300.xyz", - "takeit301.xyz", - "takeit302.xyz", - "takeit303.xyz", - "takeit304.xyz", - "takeit305.xyz", - "takeit306.xyz", - "takeit307.xyz", - "takeit308.xyz", - "takeit309.xyz", - "takeit31.xyz", - "takeit310.xyz", - "takeit311.xyz", - "takeit312.xyz", - "takeit313.xyz", - "takeit314.xyz", - "takeit315.xyz", - "takeit316.xyz", - "takeit317.xyz", - "takeit318.xyz", - "takeit319.xyz", - "takeit32.xyz", - "takeit321.xyz", - "takeit322.xyz", - "takeit323.xyz", - "takeit324.xyz", - "takeit325.xyz", - "takeit326.xyz", - "takeit327.xyz", - "takeit328.xyz", - "takeit329.xyz", - "takeit33.xyz", - "takeit330.xyz", - "takeit331.xyz", - "takeit332.xyz", - "takeit333.xyz", - "takeit334.xyz", - "takeit335.xyz", - "takeit336.xyz", - "takeit337.xyz", - "takeit338.xyz", - "takeit339.xyz", - "takeit34.xyz", - "takeit340.xyz", - "takeit341.xyz", - "takeit342.xyz", - "takeit343.xyz", - "takeit344.xyz", - "takeit345.xyz", - "takeit346.xyz", - "takeit347.xyz", - "takeit348.xyz", - "takeit349.xyz", - "takeit35.xyz", - "takeit350.xyz", - "takeit351.xyz", - "takeit352.xyz", - "takeit353.xyz", - "takeit354.xyz", - "takeit355.xyz", - "takeit356.xyz", - "takeit357.xyz", - "takeit358.xyz", - "takeit359.xyz", - "takeit36.xyz", - "takeit360.xyz", - "takeit361.xyz", - "takeit362.xyz", - "takeit363.xyz", - "takeit364.xyz", - "takeit365.xyz", - "takeit366.xyz", - "takeit367.xyz", - "takeit368.xyz", - "takeit369.xyz", - "takeit37.xyz", - "takeit370.xyz", - "takeit371.xyz", - "takeit372.xyz", - "takeit373.xyz", - "takeit374.xyz", - "takeit375.xyz", - "takeit376.xyz", - "takeit377.xyz", - "takeit378.xyz", - "takeit379.xyz", - "takeit38.xyz", - "takeit380.xyz", - "takeit381.xyz", - "takeit382.xyz", - "takeit383.xyz", - "takeit384.xyz", - "takeit385.xyz", - "takeit386.xyz", - "takeit388.xyz", - "takeit389.xyz", - "takeit39.xyz", - "takeit390.xyz", - "takeit391.xyz", - "takeit392.xyz", - "takeit393.xyz", - "takeit394.xyz", - "takeit395.xyz", - "takeit396.xyz", - "takeit397.xyz", - "takeit398.xyz", - "takeit399.xyz", - "takeit40.xyz", - "takeit400.xyz", - "takeit401.xyz", - "takeit402.xyz", - "takeit403.xyz", - "takeit404.xyz", - "takeit405.xyz", - "takeit406.xyz", - "takeit407.xyz", - "takeit408.xyz", - "takeit409.xyz", - "takeit41.xyz", - "takeit410.xyz", - "takeit411.xyz", - "takeit412.xyz", - "takeit413.xyz", - "takeit414.xyz", - "takeit415.xyz", - "takeit416.xyz", - "takeit417.xyz", - "takeit418.xyz", - "takeit419.xyz", - "takeit42.xyz", - "takeit420.xyz", - "takeit422.xyz", - "takeit423.xyz", - "takeit424.xyz", - "takeit425.xyz", - "takeit426.xyz", - "takeit427.xyz", - "takeit428.xyz", - "takeit429.xyz", - "takeit43.xyz", - "takeit430.xyz", - "takeit431.xyz", - "takeit432.xyz", - "takeit433.xyz", - "takeit434.xyz", - "takeit435.xyz", - "takeit436.xyz", - "takeit437.xyz", - "takeit438.xyz", - "takeit439.xyz", - "takeit44.xyz", - "takeit440.xyz", - "takeit441.xyz", - "takeit442.xyz", - "takeit443.xyz", - "takeit444.xyz", - "takeit445.xyz", - "takeit446.xyz", - "takeit447.xyz", - "takeit448.xyz", - "takeit449.xyz", - "takeit45.xyz", - "takeit450.xyz", - "takeit451.xyz", - "takeit452.xyz", - "takeit453.xyz", - "takeit454.xyz", - "takeit455.xyz", - "takeit456.xyz", - "takeit457.xyz", - "takeit458.xyz", - "takeit459.xyz", - "takeit46.xyz", - "takeit460.xyz", - "takeit461.xyz", - "takeit462.xyz", - "takeit463.xyz", - "takeit464.xyz", - "takeit465.xyz", - "takeit466.xyz", - "takeit467.xyz", - "takeit468.xyz", - "takeit469.xyz", - "takeit47.xyz", - "takeit470.xyz", - "takeit471.xyz", - "takeit472.xyz", - "takeit473.xyz", - "takeit474.xyz", - "takeit475.xyz", - "takeit476.xyz", - "takeit477.xyz", - "takeit478.xyz", - "takeit479.xyz", - "takeit48.xyz", - "takeit480.xyz", - "takeit481.xyz", - "takeit482.xyz", - "takeit483.xyz", - "takeit484.xyz", - "takeit485.xyz", - "takeit486.xyz", - "takeit487.xyz", - "takeit488.xyz", - "takeit489.xyz", - "takeit49.xyz", - "takeit490.xyz", - "takeit491.xyz", - "takeit492.xyz", - "takeit493.xyz", - "takeit494.xyz", - "takeit495.xyz", - "takeit496.xyz", - "takeit497.xyz", - "takeit498.xyz", - "takeit499.xyz", - "takeit50.xyz", - "takeit500.xyz", - "takeit501.xyz", - "takeit502.xyz", - "takeit503.xyz", - "takeit504.xyz", - "takeit505.xyz", - "takeit506.xyz", - "takeit507.xyz", - "takeit508.xyz", - "takeit509.xyz", - "takeit51.xyz", - "takeit510.xyz", - "takeit511.xyz", - "takeit512.xyz", - "takeit513.xyz", - "takeit514.xyz", - "takeit515.xyz", - "takeit516.xyz", - "takeit517.xyz", - "takeit518.xyz", - "takeit519.xyz", - "takeit520.xyz", - "takeit521.xyz", - "takeit522.xyz", - "takeit523.xyz", - "takeit524.xyz", - "takeit525.xyz", - "takeit526.xyz", - "takeit527.xyz", - "takeit528.xyz", - "takeit529.xyz", - "takeit53.xyz", - "takeit530.xyz", - "takeit531.xyz", - "takeit533.xyz", - "takeit534.xyz", - "takeit535.xyz", - "takeit536.xyz", - "takeit537.xyz", - "takeit538.xyz", - "takeit539.xyz", - "takeit54.xyz", - "takeit540.xyz", - "takeit541.xyz", - "takeit542.xyz", - "takeit543.xyz", - "takeit544.xyz", - "takeit545.xyz", - "takeit546.xyz", - "takeit547.xyz", - "takeit548.xyz", - "takeit549.xyz", - "takeit55.xyz", - "takeit550.xyz", - "takeit551.xyz", - "takeit552.xyz", - "takeit553.xyz", - "takeit554.xyz", - "takeit555.xyz", - "takeit556.xyz", - "takeit557.xyz", - "takeit558.xyz", - "takeit559.xyz", - "takeit56.xyz", - "takeit560.xyz", - "takeit561.xyz", - "takeit562.xyz", - "takeit563.xyz", - "takeit564.xyz", - "takeit565.xyz", - "takeit566.xyz", - "takeit567.xyz", - "takeit568.xyz", - "takeit569.xyz", - "takeit57.xyz", - "takeit570.xyz", - "takeit571.xyz", - "takeit572.xyz", - "takeit573.xyz", - "takeit574.xyz", - "takeit575.xyz", - "takeit576.xyz", - "takeit577.xyz", - "takeit578.xyz", - "takeit579.xyz", - "takeit58.xyz", - "takeit580.xyz", - "takeit581.xyz", - "takeit582.xyz", - "takeit583.xyz", - "takeit584.xyz", - "takeit586.xyz", - "takeit587.xyz", - "takeit588.xyz", - "takeit589.xyz", - "takeit59.xyz", - "takeit590.xyz", - "takeit591.xyz", - "takeit592.xyz", - "takeit594.xyz", - "takeit596.xyz", - "takeit597.xyz", - "takeit598.xyz", - "takeit599.xyz", - "takeit60.xyz", - "takeit601.xyz", - "takeit602.xyz", - "takeit603.xyz", - "takeit604.xyz", - "takeit605.xyz", - "takeit606.xyz", - "takeit607.xyz", - "takeit608.xyz", - "takeit61.xyz", - "takeit610.xyz", - "takeit611.xyz", - "takeit612.xyz", - "takeit613.xyz", - "takeit614.xyz", - "takeit615.xyz", - "takeit616.xyz", - "takeit617.xyz", - "takeit618.xyz", - "takeit619.xyz", - "takeit62.xyz", - "takeit620.xyz", - "takeit621.xyz", - "takeit622.xyz", - "takeit623.xyz", - "takeit624.xyz", - "takeit625.xyz", - "takeit626.xyz", - "takeit627.xyz", - "takeit628.xyz", - "takeit629.xyz", - "takeit63.xyz", - "takeit630.xyz", - "takeit631.xyz", - "takeit632.xyz", - "takeit633.xyz", - "takeit634.xyz", - "takeit635.xyz", - "takeit636.xyz", - "takeit637.xyz", - "takeit638.xyz", - "takeit639.xyz", - "takeit64.xyz", - "takeit640.xyz", - "takeit641.xyz", - "takeit642.xyz", - "takeit643.xyz", - "takeit644.xyz", - "takeit645.xyz", - "takeit646.xyz", - "takeit647.xyz", - "takeit648.xyz", - "takeit649.xyz", - "takeit650.xyz", - "takeit651.xyz", - "takeit652.xyz", - "takeit653.xyz", - "takeit654.xyz", - "takeit655.xyz", - "takeit656.xyz", - "takeit657.xyz", - "takeit658.xyz", - "takeit659.xyz", - "takeit66.xyz", - "takeit660.xyz", - "takeit661.xyz", - "takeit662.xyz", - "takeit67.xyz", - "takeit68.xyz", - "takeit69.xyz", - "takeit70.xyz", - "takeit71.xyz", - "takeit72.xyz", - "takeit73.xyz", - "takeit74.xyz", - "takeit75.xyz", - "takeit76.xyz", - "takeit77.xyz", - "takeit78.xyz", - "takeit79.xyz", - "takeit80.xyz", - "takeit81.xyz", - "takeit82.xyz", - "takeit83.xyz", - "takeit84.xyz", - "takeit85.xyz", - "takeit86.xyz", - "takeit87.xyz", - "takeit88.xyz", - "takeit89.xyz", - "takeit90.xyz", - "takeit91.xyz", - "takeit92.xyz", - "takeit93.xyz", - "takeit94.xyz", - "takeit95.xyz", - "takeit96.xyz", - "takeit97.xyz", - "takeit98.xyz", - "takeit99.xyz", - "tasty-drop.pp.ua", - "tasty-skill.net.ru", - "tastygo.ru.com", - "tastyskill.net.ru", - "taty-dropp.info", - "team-dream.xyz", - "team.the-shrubbery.co.uk", - "teamastrallis.org.ru", - "teamfnat.net.ru", - "teamfnattic.org.ru", - "teamgog.pp.ua", - "terrifvvev.com", - "test-domuin2.com", - "test-domuin3.ru", - "test-domuin4.ru", - "test-domuin5.ru", - "testbot2021.ru", - "testy-drop.pp.ua", - "tf2market.store", - "thediscordapp.com", - "themekaversed.org", - "themekaverses.org", - "think-when.xyz", - "thor-case.net.ru", - "threemeterssky.ru", - "tigers.pp.ua", - "tik-team-topp.org.ru", - "tiktok.verifiedbadgehelp-form.ml", - "tiktokmagic.ru", - "tiktoksupport.ru.com", - "tini.best", - "tipteamgg.xyz", - "toolprotimenow.com", - "toom-skins.xyz", - "toornirs.pp.ua", - "top-team.org.ru", - "topcase.monster", - "topconsumerproductsonline.com", - "topeasyllucky.pp.ua", - "topgadgetneckmassager.com", - "toprobux.site", - "topstteeamleto2021.net.ru", - "topsweeps.com", - "topvincere.net.ru", - "topvincere.org.ru", - "topvincere.pp.ru", - "topw-gamez.xyz", - "topz-games.xyz", - "tourggesports.ru", - "tournament.ru.com", - "tournamentcs.live", - "tournamentcsgo.ga", - "tournamentcsgo.gq", - "tournaments.ru.com", - "tournamentsplay.site", - "tournamentt.com", - "tournrecruit.xyz", - "trabeoffer.ru", - "trabeoffers.xyz", - "trade-csmoney.ru", - "trade-dexter.xyz", - "trade-leagues.com", - "trade-link-offer.ru", - "trade-linkk.ru", - "trade-offers.link", - "trade-offersz.pp.ua", - "trade-profile.fun", - "trade.ru.com", - "tradeaffix.pp.ua", - "tradeandyou.ru", - "tradecs.ru.com", - "tradelink.live", - "tradeoff.space", - "tradeoffer-link.ru.com", - "tradeoffer-new.ru", - "tradeoffer.com.ru", - "tradeoffers.net.ru", - "tradeoffers11.xyz", - "traderlink.ru.com", - "traders-offers.com", - "trades-league.com", - "trades-offers.xyz", - "tradesoffers.com", - "treader-offer.com", - "tredecsgo.com", - "treders-offers.com", - "treplov.pp.ua", - "triumph.tk", - "true-money.xyz", - "truepnl-giveaway.info", - "trustpool.xyz", - "tryinfinitikloud.com", - "tryultrassenceskin.com", - "tugceyumakogullari.tk", - "twitch-facepanch.com", - "twitch-nude.com", - "twitch-starter.com", - "twitch.facepunch-llc.com", - "twitch.facepunch-ltd.com", - "twitch.facepunchs.com", - "twitch.facepunchstudio.com", - "twitch.rust-ltd.com", - "tylofpcasy.xyz", - "u924157p.beget.tech", - "ultimateskins.xyz", - "ultracup.fun", - "umosleep.ru", - "universityteam.xyz", - "up-discord.ru", - "up-nitro.com", - "up-you.ru", - "upcs.monster", - "us-appmonie.yousweeps.com", - "uspringcup.com", - "ut.ntwrk.yunihost.ru", - "v-roblox.com", - "vbucksminer.ru", - "verifapp.us", - "verification-discord.com", - "verifications-discord.com", - "verifiedbadgehelp-form.ml", - "verify-discord.com", - "verifyaccount-for-bluetick.com", - "versus-cup.ru", - "versus-play.ru", - "versuscs.ru", - "versuscsgoplay.pp.ua", - "versusplay.ru", - "vippobrit.ru", - "vippobrit1.ru.com", - "visaxsteam.ru", - "vitality-cyber.net", - "vitality-playtime.com", - "vitality-top.ru", - "vitalityboxs.com", - "vitalitycamp.ru", - "vitalityesports.net", - "vitalitygg.ru", - "viwwzagul.xyz", - "viwwzaguls.xyz", - "viwwzagulw.xyz", - "viwwzaguly.xyz", - "vkbonus.club", - "vm1189661.firstbyte.club", - "vpitems.xyz", - "vqojiorq.ru", - "waccupzero.ru.com", - "waccupzerow.monster", - "wallet-steam.ml", - "wanmei-hy.ru", - "wanmeics6.ru", - "wanmeicsgo1.ru", - "wanmeipt.ru", - "wanmeizi.ru", - "waterbets.ru", - "waucupsz.monster", - "wavebtc.com", - "we-player.ru", - "wearewinagain.xyz", - "webr-roblox.com", - "weplay.ru.com", - "were-want.ru.com", - "wheel-run.ru", - "white-guns.xyz", - "white-list.live", - "whitelampa.xyz", - "widesdays.com", - "win-lems.org.ru", - "win-skin.top", - "win-skin.xyz", - "win-trader.org.ru", - "winknifespin.xyz", - "winner-roll.ru", - "winrbx1s1.pw", - "wins-navi.com", - "winskin-simple.xyz", - "winskins.top", - "wintheskin.xyz", - "withereum.com", - "word-the.xyz", - "wowfnatic.ru", - "wtf-magic.ru", - "wtf-magic.top", - "wtf-magicru.top", - "wtf-win.net.ru", - "ww1.dicsordapp.com", - "ww1.discordapp.org", - "ww11.steamcommunity.download", - "ww16.discordcanary.com", - "ww8.steamcommmunity.ru.com", - "wwdiscord.com", - "www-steamcommunlty.com", - "www2.c2bit.online", - "wwwlog-in.xyz", - "wyxy.ru", - "x33681t2.beget.tech", - "xdiscord.com", - "xesa-nitro.com", - "xess-nitro.com", - "xfxcheats.online", - "xgamercup.com", - "xn--e1agajgahgxri7a.site", - "xn--steamcommunit-ge3g.com", - "xorialloy.xyz", - "xpro.gift", - "xpro.ws", - "xpromo-discord.com", - "xroll.space", - "xscsgo.com", - "xtradefox.com", - "xtradeskin.com", - "yeppymoll.xyz", - "yolock.site", - "youtubers2021.xyz", - "youtubersrwrds.xyz", - "yummy-nitro.com", - "z93729n9.beget.tech", - "zakat.ntwrk.yunihost.ru", - "zerocup.ru", - "zipsetgo.com", - "zonewarco.org.ru", - "zonewarco.org.ru", - // "steamcommunity.co", -]; diff --git a/src/lib/badwords.ts b/src/lib/badwords.ts deleted file mode 100644 index feb74cb..0000000 --- a/src/lib/badwords.ts +++ /dev/null @@ -1,752 +0,0 @@ -import type { BadWords } from "./common/AutoMod.js"; - -const enum Severity { - DELETE, - WARN, - TEMP_MUTE, - PERM_MUTE, -} - -export default { - /* -------------------------------------------------------------------------- */ - /* Slurs */ - /* -------------------------------------------------------------------------- */ - "Slurs": [ - { - match: "faggot", - severity: Severity.TEMP_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "homophobic slur", - regex: false, - }, - { - match: "nigga", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "racial slur", - regex: false, - }, - { - match: "nigger", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "racial slur", - regex: false, - }, - { - match: "nigra", - severity: Severity.PERM_MUTE, - ignoreSpaces: false, - ignoreCapitalization: true, - reason: "racial slur", - regex: false, - }, - { - match: "retard", - severity: Severity.TEMP_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "ableist slur", - regex: false, - }, - { - match: "retarted", - severity: Severity.TEMP_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "ableist slur", - regex: false, - }, - { - match: "slut", - severity: Severity.WARN, - ignoreSpaces: false, - ignoreCapitalization: true, - reason: "derogatory term", - regex: false, - }, - { - match: "tar baby", - severity: Severity.TEMP_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "racial slur", - regex: false, - }, - { - match: "whore", - severity: Severity.WARN, - ignoreSpaces: false, - ignoreCapitalization: true, - reason: "derogatory term", - regex: false, - }, - { - match: "卍", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "racist symbol", - regex: false, - }, - { - //? N word - match: "space movie 1992", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "racial slur", - regex: false, - }, - { - //? N word - match: "黑鬼", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "racial slur", - regex: false, - }, - ], - - /* -------------------------------------------------------------------------- */ - /* Steam Scams */ - /* -------------------------------------------------------------------------- */ - "Steam Scams": [ - { - //? I'm on tilt, in the cop they gave the status "Unreliable" - match: 'Я в тильте, в кс дали статус "Ненадежный"', - severity: Severity.WARN, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "hello i am leaving cs:go", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "hello! I'm done with csgo", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "hi bro, i'm leaving this fucking game, take my skin", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "hi friend, today i am leaving this fucking game", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "hi guys, i'm leaving this fucking game, take my", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "hi, bro h am leaving cs:go and giving away my skin", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "hi, bro i am leaving cs:go and giving away my skin", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "i confirm all exchanges, there won't be enough", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "i quit csgo", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "the first three who send a trade", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "you can choose any skin for yourself", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "Hey, I'm leaving for the army and giving the skins", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "fuck this trash called CS:GO, deleted,", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "please take my skins", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - { - match: "Hi, I stopped playing CS:GO and decided to giveaway my inventory.", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "steam scam phrase", - regex: false, - }, - ], - - /* -------------------------------------------------------------------------- */ - /* Nitro Scams */ - /* -------------------------------------------------------------------------- */ - "Nitro Scams": [ - { - match: "and there is discord hallween's giveaway", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "discord nitro for free - steam store", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "free 3 months of discord nitro", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "free discord nitro airdrop", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "get 3 months of discord nitro", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "get discord nitro for free", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "get free discord nitro from steam", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "lol, jahjajha free discord nitro for 3 month!!", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "steam is giving away 3 months of discord nitro for free to all no limited steam users", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - //? Lol, 1 month free discord nitro! - match: "Лол, бесплатный дискорд нитро на 1 месяц!", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Airdrop Discord FREE NITRO from Steam —", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "take nitro faster, it's already running out", - severity: Severity.PERM_MUTE, - ignoreSpaces: false, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "only the first 10 people will have time to take nitro", - severity: Severity.PERM_MUTE, - ignoreSpaces: false, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Discord is giving away nitro!", - severity: Severity.PERM_MUTE, - ignoreSpaces: false, - ignoreCapitalization: false, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Free gift discord nitro for 1 month!", - severity: Severity.PERM_MUTE, - ignoreSpaces: false, - ignoreCapitalization: false, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Hi i claim this nitro for free 3 months lol!", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "bro watch this, working nitro gen", - severity: Severity.PERM_MUTE, - ignoreSpaces: false, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Free distribution of discord nitro for 3 months from steam!", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Get 3 Months of Discord Nitro. Personalize your profile, screen share in HD, upgrade your emojis, and more!", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Steam is giving away free discord nitro, have time to pick up at my link", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Airdrop Discord NITRO with", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Check this lol, there nitro is handed out for free, take it until everything is sorted out", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "A free Discord Nitro | Steam Store Discord Nitro Distribution.", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Xbox gives away discord nitro for free", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "airdrop discord nitro by steam", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - //? 3 months nitro free from steam, take too - match: "3 месяца нитро бесплатно от стима, забирайте тоже", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Free distributiοn of discοrd nitrο for 3 months from steаm!", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Free discord nitro for 1 month!", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "I got some nitro left over here", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Hey, steam gived nitro", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "nitro giveaway by steam, take it", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "3 months nitro from styme,", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "XBOX and DISCORD are giving away free NITRO FULL for a month.", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Hi,take the Discord Nitro for free", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - //? Discord nitro got free, take it before it's too late - match: "Дискорд нитро получил бесплатно,забирай пока не поздно", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "1 month nitro for free", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Gifts for the new year, nitro for 3 months", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "1 month nitro from steam, take it guys", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Hello, discord and steam are giving away nitro, take it away", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Who is first? :)", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Whо is first? :)", - //? This one uses a different o, prob should make some autodelete if includes link and special char - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Discord Nitro distribution from STEAM", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "3 month nitro for free, take it ", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "3 months nitro from steam, take it guys)", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Gifts from steam nitro, gifts for 3 months", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Free subscription for 3 months DISCORD NITRO", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "who will catch this gift?)", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "take it guys :)", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Discord and Steam are giving away a free 3-month Discord Gift subscription!", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - { - match: "Discord free nitro from steam", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "discord nitro scam phrase", - regex: false, - }, - ], - - /* -------------------------------------------------------------------------- */ - /* Misc Scams */ - /* -------------------------------------------------------------------------- */ - "Misc Scams": [ - { - match: "found a cool software that improves the", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "misc. scam phrase", - regex: false, - }, - { - match: - "there is a possible chance tomorrow there will be a cyber-attack event where on all social networks including Discord there will be people trying", - severity: Severity.WARN, - ignoreSpaces: false, - ignoreCapitalization: true, - reason: "annoying copy pasta", - regex: false, - }, - { - match: "i made a game can you test play ?", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "malware phrase", - regex: false, - }, - { - match: "tell me if something is wrong in the game", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "malware phrase", - regex: false, - }, - { - match: "Hi, can you check out the game I created today:)", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "malware phrase", - regex: false, - }, - { - match: "Just want to get other people's opinions, what to add and what to remove.", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "malware phrase", - regex: false, - }, - { - match: "https://discord.gg/KKnGGvEPVM", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "misc. scam phrase", - regex: false, - }, - { - match: "https://discord.gg/rykjvpTGrB", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "misc. scam phrase", - regex: false, - }, - { - match: "https://discord.gg/XTDQgJ9YMp", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "misc. scam phrase", - regex: false, - }, - ], - - /* -------------------------------------------------------------------------- */ - /* Advertising */ - /* -------------------------------------------------------------------------- */ - "Advertising": [ - { - match: "😀 wow only 13+... 😳 are allowed to see my about me 😏", - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: "advertising", - regex: false, - }, - ], -} as BadWords; diff --git a/src/lib/common/AutoMod.ts b/src/lib/common/AutoMod.ts deleted file mode 100644 index 44c6dee..0000000 --- a/src/lib/common/AutoMod.ts +++ /dev/null @@ -1,529 +0,0 @@ -import { colors, emojis, format, formatError, Moderation, unmuteResponse } from '#lib'; -import assert from 'assert/strict'; -import chalk from 'chalk'; -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - EmbedBuilder, - GuildMember, - PermissionFlagsBits, - type ButtonInteraction, - type Message, - type Snowflake, - type TextChannel -} from 'discord.js'; -import UnmuteCommand from '../../commands/moderation/unmute.js'; - -/** - * Handles auto moderation functionality. - */ -export class AutoMod { - /** - * Whether or not a punishment has already been given to the user - */ - private punished = false; - - /** - * @param message The message to check and potentially perform automod actions to - */ - public constructor(private message: Message) { - if (message.author.id === message.client.user?.id) return; - void this.handle(); - } - - /** - * Whether or not the message author is immune to auto moderation - */ - private get isImmune() { - if (!this.message.inGuild()) return false; - assert(this.message.member); - - if (this.message.author.isOwner()) return true; - if (this.message.guild.ownerId === this.message.author.id) return true; - if (this.message.member.permissions.has('Administrator')) return true; - - return false; - } - - /** - * Handles the auto moderation - */ - private async handle() { - if (!this.message.inGuild()) return; - if (!(await this.message.guild.hasFeature('automod'))) return; - if (this.message.author.bot) return; - - traditional: { - if (this.isImmune) break traditional; - const badLinksArray = this.message.client.utils.getShared('badLinks'); - const badLinksSecretArray = this.message.client.utils.getShared('badLinksSecret'); - const badWordsRaw = this.message.client.utils.getShared('badWords'); - - const customAutomodPhrases = (await this.message.guild.getSetting('autoModPhases')) ?? []; - const uniqueLinks = [...new Set([...badLinksArray, ...badLinksSecretArray])]; - - const badLinks: BadWordDetails[] = uniqueLinks.map((link) => ({ - match: link, - severity: Severity.PERM_MUTE, - ignoreSpaces: false, - ignoreCapitalization: true, - reason: 'malicious link', - regex: false - })); - - const parsedBadWords = Object.values(badWordsRaw).flat(); - - const result = [ - ...this.checkWords(customAutomodPhrases), - ...this.checkWords((await this.message.guild.hasFeature('excludeDefaultAutomod')) ? [] : parsedBadWords), - ...this.checkWords((await this.message.guild.hasFeature('excludeAutomodScamLinks')) ? [] : badLinks) - ]; - - if (result.length === 0) break traditional; - - const highestOffence = result.sort((a, b) => b.severity - a.severity)[0]; - - if (highestOffence.severity === undefined || highestOffence.severity === null) { - void this.message.guild.sendLogChannel('error', { - embeds: [ - { - title: 'AutoMod Error', - description: `Unable to find severity information for ${format.inlineCode(highestOffence.match)}`, - color: colors.error - } - ] - }); - } else { - const color = this.punish(highestOffence); - void this.log(highestOffence, color, result); - } - } - - other: { - if (this.isImmune) break other; - if (!this.punished && (await this.message.guild.hasFeature('delScamMentions'))) void this.checkScamMentions(); - } - - if (!this.punished && (await this.message.guild.hasFeature('perspectiveApi'))) void this.checkPerspectiveApi(); - } - - /** - * Checks if any of the words provided are in the message - * @param words The words to check for - * @returns The blacklisted words found in the message - */ - private checkWords(words: BadWordDetails[]): BadWordDetails[] { - if (words.length === 0) return []; - - const matchedWords: BadWordDetails[] = []; - for (const word of words) { - if (word.regex) { - if (new RegExp(word.match).test(this.format(word.match, word))) { - matchedWords.push(word); - } - } else { - if (this.format(this.message.content, word).includes(this.format(word.match, word))) { - matchedWords.push(word); - } - } - } - return matchedWords; - } - - /** - * If the message contains '@everyone' or '@here' and it contains a common scam phrase, it will be deleted - * @returns - */ - private async checkScamMentions() { - const includes = (c: string) => this.message.content.toLocaleLowerCase().includes(c); - if (!includes('@everyone') && !includes('@here')) return; - // It would be bad if we deleted a message that actually pinged @everyone or @here - if ( - this.message.member?.permissionsIn(this.message.channelId).has(PermissionFlagsBits.MentionEveryone) || - this.message.mentions.everyone - ) - return; - - if ( - includes('steam') || - includes('www.youtube.com') || - includes('youtu.be') || - includes('nitro') || - includes('1 month') || - includes('3 months') || - includes('personalize your profile') || - includes('even more') || - includes('xbox and discord') || - includes('left over') || - includes('check this lol') || - includes('airdrop') - ) { - const color = this.punish({ severity: Severity.TEMP_MUTE, reason: 'everyone mention and scam phrase' } as BadWordDetails); - void this.message.guild!.sendLogChannel('automod', { - embeds: [ - new EmbedBuilder() - .setTitle(`[Severity ${Severity.TEMP_MUTE}] Mention Scam Deleted`) - .setDescription( - `**User:** ${this.message.author} (${this.message.author.tag})\n**Sent From:** <#${this.message.channel.id}> [Jump to context](${this.message.url})` - ) - .addFields({ - name: 'Message Content', - value: `${await this.message.client.utils.codeblock(this.message.content, 1024)}` - }) - .setColor(color) - .setTimestamp() - ], - components: [this.buttons(this.message.author.id, 'everyone mention and scam phrase')] - }); - } - } - - private async checkPerspectiveApi() { - return; - if (!this.message.client.config.isDevelopment) return; - - if (!this.message.content) return; - this.message.client.perspective.comments.analyze( - { - key: this.message.client.config.credentials.perspectiveApiKey, - resource: { - comment: { - text: this.message.content - }, - requestedAttributes: { - TOXICITY: {}, - SEVERE_TOXICITY: {}, - IDENTITY_ATTACK: {}, - INSULT: {}, - PROFANITY: {}, - THREAT: {}, - SEXUALLY_EXPLICIT: {}, - FLIRTATION: {} - } - } - }, - (err: any, response: any) => { - if (err) return console.log(err?.message); - - const normalize = (val: number, min: number, max: number) => (val - min) / (max - min); - - const color = (val: number) => { - if (val >= 0.5) { - const x = 194 - Math.round(normalize(val, 0.5, 1) * 194); - return chalk.rgb(194, x, 0)(val); - } else { - const x = Math.round(normalize(val, 0, 0.5) * 194); - return chalk.rgb(x, 194, 0)(val); - } - }; - - console.log(chalk.cyan(this.message.content)); - Object.entries(response.data.attributeScores) - .sort(([a], [b]) => a.localeCompare(b)) - .forEach(([key, value]: any[]) => console.log(chalk.white(key), color(value.summaryScore.value))); - } - ); - } - - /** - * Format a string according to the word options - * @param string The string to format - * @param wordOptions The word options to format with - * @returns The formatted string - */ - private format(string: string, wordOptions: BadWordDetails) { - const temp = wordOptions.ignoreCapitalization ? string.toLowerCase() : string; - return wordOptions.ignoreSpaces ? temp.replace(/ /g, '') : temp; - } - - /** - * Punishes the user based on the severity of the offense - * @param highestOffence The highest offense to punish the user for - * @returns The color of the embed that the log should, based on the severity of the offense - */ - private punish(highestOffence: BadWordDetails) { - let color; - switch (highestOffence.severity) { - case Severity.DELETE: { - color = colors.lightGray; - void this.message.delete().catch((e) => deleteError.bind(this, e)); - this.punished = true; - break; - } - case Severity.WARN: { - color = colors.yellow; - void this.message.delete().catch((e) => deleteError.bind(this, e)); - void this.message.member?.bushWarn({ - moderator: this.message.guild!.members.me!, - reason: `[AutoMod] ${highestOffence.reason}` - }); - this.punished = true; - break; - } - case Severity.TEMP_MUTE: { - color = colors.orange; - void this.message.delete().catch((e) => deleteError.bind(this, e)); - void this.message.member?.bushMute({ - moderator: this.message.guild!.members.me!, - reason: `[AutoMod] ${highestOffence.reason}`, - duration: 900_000 // 15 minutes - }); - this.punished = true; - break; - } - case Severity.PERM_MUTE: { - color = colors.red; - void this.message.delete().catch((e) => deleteError.bind(this, e)); - void this.message.member?.bushMute({ - moderator: this.message.guild!.members.me!, - reason: `[AutoMod] ${highestOffence.reason}`, - duration: 0 // permanent - }); - this.punished = true; - break; - } - default: { - throw new Error(`Invalid severity: ${highestOffence.severity}`); - } - } - - return color; - - async function deleteError(this: AutoMod, e: Error | any) { - void this.message.guild?.sendLogChannel('error', { - embeds: [ - { - title: 'AutoMod Error', - description: `Unable to delete triggered message.`, - fields: [{ name: 'Error', value: await this.message.client.utils.codeblock(`${formatError(e)}`, 1024, 'js', true) }], - color: colors.error - } - ] - }); - } - } - - /** - * Log an automod infraction to the guild's specified automod log channel - * @param highestOffence The highest severity word found in the message - * @param color The color that the log embed should be (based on the severity) - * @param offenses The other offenses that were also matched in the message - */ - private async log(highestOffence: BadWordDetails, color: number, offenses: BadWordDetails[]) { - void this.message.client.console.info( - 'autoMod', - `Severity <<${highestOffence.severity}>> action performed on <<${this.message.author.tag}>> (<<${ - this.message.author.id - }>>) in <<#${(this.message.channel as TextChannel).name}>> in <<${this.message.guild!.name}>>` - ); - - await this.message.guild!.sendLogChannel('automod', { - embeds: [ - new EmbedBuilder() - .setTitle(`[Severity ${highestOffence.severity}] Automod Action Performed`) - .setDescription( - `**User:** ${this.message.author} (${this.message.author.tag})\n**Sent From:** <#${ - this.message.channel.id - }> [Jump to context](${this.message.url})\n**Blacklisted Words:** ${offenses.map((o) => `\`${o.match}\``).join(', ')}` - ) - .addFields({ - name: 'Message Content', - value: `${await this.message.client.utils.codeblock(this.message.content, 1024)}` - }) - .setColor(color) - .setTimestamp() - .setAuthor({ name: this.message.author.tag, url: this.message.author.displayAvatarURL() }) - ], - components: highestOffence.severity >= 2 ? [this.buttons(this.message.author.id, highestOffence.reason)] : undefined - }); - } - - private buttons(userId: Snowflake, reason: string): ActionRowBuilder { - return new ActionRowBuilder().addComponents( - new ButtonBuilder({ - style: ButtonStyle.Danger, - label: 'Ban User', - customId: `automod;ban;${userId};${reason}` - }), - new ButtonBuilder({ - style: ButtonStyle.Success, - label: 'Unmute User', - customId: `automod;unmute;${userId}` - }) - ); - } - - /** - * Handles the ban button in the automod log. - * @param interaction The button interaction. - */ - public static async handleInteraction(interaction: ButtonInteraction) { - if (!interaction.memberPermissions?.has(PermissionFlagsBits.BanMembers)) - return interaction.reply({ - content: `${emojis.error} You are missing the **Ban Members** permission.`, - ephemeral: true - }); - const [action, userId, reason] = interaction.customId.replace('automod;', '').split(';') as [ - 'ban' | 'unmute', - string, - string - ]; - - if (!(['ban', 'unmute'] as const).includes(action)) throw new TypeError(`Invalid automod button action: ${action}`); - - const victim = await interaction.guild!.members.fetch(userId).catch(() => null); - const moderator = - interaction.member instanceof GuildMember - ? interaction.member - : await interaction.guild!.members.fetch(interaction.user.id); - - switch (action) { - case 'ban': { - if (!interaction.guild?.members.me?.permissions.has('BanMembers')) - return interaction.reply({ - content: `${emojis.error} I do not have permission to ${action} members.`, - ephemeral: true - }); - - const check = victim ? await Moderation.permissionCheck(moderator, victim, 'ban', true) : true; - if (check !== true) return interaction.reply({ content: check, ephemeral: true }); - - const result = await interaction.guild?.bushBan({ - user: userId, - reason, - moderator: interaction.user.id, - evidence: (interaction.message as Message).url ?? undefined - }); - - const victimUserFormatted = (await interaction.client.utils.resolveNonCachedUser(userId))?.tag ?? userId; - - const content = (() => { - if (result === unmuteResponse.SUCCESS) { - return `${emojis.success} Successfully banned ${format.input(victimUserFormatted)}.`; - } else if (result === unmuteResponse.DM_ERROR) { - return `${emojis.warn} Banned ${format.input(victimUserFormatted)} however I could not send them a dm.`; - } else { - return `${emojis.error} Could not ban ${format.input(victimUserFormatted)}: \`${result}\` .`; - } - })(); - - return interaction.reply({ - content: content, - ephemeral: true - }); - } - - case 'unmute': { - if (!victim) - return interaction.reply({ - content: `${emojis.error} Cannot find member, they may have left the server.`, - ephemeral: true - }); - - if (!interaction.guild) - return interaction.reply({ - content: `${emojis.error} This is weird, I don't seem to be in the server...`, - ephemeral: true - }); - - const check = await Moderation.permissionCheck(moderator, victim, 'unmute', true); - if (check !== true) return interaction.reply({ content: check, ephemeral: true }); - - const check2 = await Moderation.checkMutePermissions(interaction.guild); - if (check2 !== true) - return interaction.reply({ content: UnmuteCommand.formatCode('/', victim!, check2), ephemeral: true }); - - const result = await victim.bushUnmute({ - reason, - moderator: interaction.member as GuildMember, - evidence: (interaction.message as Message).url ?? undefined - }); - - const victimUserFormatted = victim.user.tag; - - const content = (() => { - if (result === unmuteResponse.SUCCESS) { - return `${emojis.success} Successfully unmuted ${format.input(victimUserFormatted)}.`; - } else if (result === unmuteResponse.DM_ERROR) { - return `${emojis.warn} Unmuted ${format.input(victimUserFormatted)} however I could not send them a dm.`; - } else { - return `${emojis.error} Could not unmute ${format.input(victimUserFormatted)}: \`${result}\` .`; - } - })(); - - return interaction.reply({ - content: content, - ephemeral: true - }); - } - } - } -} - -/** - * The severity of the blacklisted word - */ -export const enum Severity { - /** - * Delete message - */ - DELETE, - - /** - * Delete message and warn user - */ - WARN, - - /** - * Delete message and mute user for 15 minutes - */ - TEMP_MUTE, - - /** - * Delete message and mute user permanently - */ - PERM_MUTE -} - -/** - * Details about a blacklisted word - */ -export interface BadWordDetails { - /** - * The word that is blacklisted - */ - match: string; - - /** - * The severity of the word - */ - severity: Severity | 1 | 2 | 3; - - /** - * Whether or not to ignore spaces when checking for the word - */ - ignoreSpaces: boolean; - - /** - * Whether or not to ignore case when checking for the word - */ - ignoreCapitalization: boolean; - - /** - * The reason that this word is blacklisted (used for the punishment reason) - */ - reason: string; - - /** - * Whether or not the word is regex - */ - regex: boolean; -} - -/** - * Blacklisted words mapped to their details - */ -export interface BadWords { - [category: string]: BadWordDetails[]; -} diff --git a/src/lib/common/ButtonPaginator.ts b/src/lib/common/ButtonPaginator.ts deleted file mode 100644 index 02c78ea..0000000 --- a/src/lib/common/ButtonPaginator.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { DeleteButton, type CommandMessage, type SlashMessage } from '#lib'; -import { CommandUtil } from 'discord-akairo'; -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - EmbedBuilder, - type APIEmbed, - type Message, - type MessageComponentInteraction -} from 'discord.js'; - -/** - * Sends multiple embeds with controls to switch between them - */ -export class ButtonPaginator { - /** - * The current page of the paginator - */ - protected curPage: number; - - /** - * The paginator message - */ - protected sentMessage: Message | undefined; - - /** - * @param message The message that triggered the command - * @param embeds The embeds to switch between - * @param text The optional text to send with the paginator - * @param {} [deleteOnExit=true] Whether the paginator message gets deleted when the exit button is pressed - * @param startOn The page to start from (**not** the index) - */ - protected constructor( - protected message: CommandMessage | SlashMessage, - protected embeds: EmbedBuilder[] | APIEmbed[], - protected text: string | null, - protected deleteOnExit: boolean, - startOn: number - ) { - this.curPage = startOn - 1; - - // add footers - for (let i = 0; i < embeds.length; i++) { - if (embeds[i] instanceof EmbedBuilder) { - (embeds[i] as EmbedBuilder).setFooter({ text: `Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}` }); - } else { - (embeds[i] as APIEmbed).footer = { - text: `Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}` - }; - } - } - } - - /** - * The number of pages in the paginator - */ - protected get numPages(): number { - return this.embeds.length; - } - - /** - * Sends the paginator message - */ - protected async send() { - this.sentMessage = await this.message.util.reply({ - content: this.text, - embeds: [this.embeds[this.curPage]], - components: [this.getPaginationRow()] - }); - - const collector = this.sentMessage.createMessageComponentCollector({ - filter: (i) => i.customId.startsWith('paginate_'), - time: 300_000 - }); - collector.on('collect', (i) => void this.collect(i)); - collector.on('end', () => void this.end()); - } - - /** - * Handles interactions with the paginator - * @param interaction The interaction received - */ - protected async collect(interaction: MessageComponentInteraction) { - if (interaction.user.id !== this.message.author.id && !this.message.client.config.owners.includes(interaction.user.id)) - return await interaction?.deferUpdate().catch(() => null); - - switch (interaction.customId) { - case 'paginate_beginning': - this.curPage = 0; - await this.edit(interaction); - break; - case 'paginate_back': - this.curPage--; - await this.edit(interaction); - break; - case 'paginate_stop': - if (this.deleteOnExit) { - await interaction.deferUpdate().catch(() => null); - await this.sentMessage!.delete().catch(() => null); - break; - } else { - await interaction - ?.update({ - content: `${this.text ? `${this.text}\n` : ''}Command closed by user.`, - embeds: [], - components: [] - }) - .catch(() => null); - break; - } - case 'paginate_next': - this.curPage++; - await this.edit(interaction); - break; - case 'paginate_end': - this.curPage = this.embeds.length - 1; - await this.edit(interaction); - break; - } - } - - /** - * Ends the paginator - */ - protected async end() { - if (this.sentMessage && !CommandUtil.deletedMessages.has(this.sentMessage.id)) - await this.sentMessage - .edit({ - content: this.text, - embeds: [this.embeds[this.curPage]], - components: [this.getPaginationRow(true)] - }) - .catch(() => null); - } - - /** - * Edits the paginator message - * @param interaction The interaction received - */ - protected async edit(interaction: MessageComponentInteraction) { - await interaction - ?.update({ - content: this.text, - embeds: [this.embeds[this.curPage]], - components: [this.getPaginationRow()] - }) - .catch(() => null); - } - - /** - * Generates the pagination row based on the class properties - * @param disableAll Whether to disable all buttons - * @returns The generated {@link ActionRow} - */ - protected getPaginationRow(disableAll = false) { - return new ActionRowBuilder().addComponents( - new ButtonBuilder({ - style: ButtonStyle.Primary, - customId: 'paginate_beginning', - emoji: PaginateEmojis.BEGINNING, - disabled: disableAll || this.curPage === 0 - }), - new ButtonBuilder({ - style: ButtonStyle.Primary, - customId: 'paginate_back', - emoji: PaginateEmojis.BACK, - disabled: disableAll || this.curPage === 0 - }), - new ButtonBuilder({ - style: ButtonStyle.Primary, - customId: 'paginate_stop', - emoji: PaginateEmojis.STOP, - disabled: disableAll - }), - new ButtonBuilder({ - style: ButtonStyle.Primary, - customId: 'paginate_next', - emoji: PaginateEmojis.FORWARD, - disabled: disableAll || this.curPage === this.numPages - 1 - }), - new ButtonBuilder({ - style: ButtonStyle.Primary, - customId: 'paginate_end', - emoji: PaginateEmojis.END, - disabled: disableAll || this.curPage === this.numPages - 1 - }) - ); - } - - /** - * Sends multiple embeds with controls to switch between them - * @param message The message to respond to - * @param embeds The embeds to switch between - * @param text The text send with the embeds (optional) - * @param deleteOnExit Whether to delete the message when the exit button is clicked (defaults to true) - * @param startOn The page to start from (**not** the index) - */ - public static async send( - message: CommandMessage | SlashMessage, - embeds: EmbedBuilder[] | APIEmbed[], - text: string | null = null, - deleteOnExit = true, - startOn = 1 - ) { - // no need to paginate if there is only one page - if (embeds.length === 1) return DeleteButton.send(message, { embeds: embeds }); - - return await new ButtonPaginator(message, embeds, text, deleteOnExit, startOn).send(); - } -} - -export const PaginateEmojis = { - BEGINNING: { id: '853667381335162910', name: 'w_paginate_beginning', animated: false } as const, - BACK: { id: '853667410203770881', name: 'w_paginate_back', animated: false } as const, - STOP: { id: '853667471110570034', name: 'w_paginate_stop', animated: false } as const, - FORWARD: { id: '853667492680564747', name: 'w_paginate_next', animated: false } as const, - END: { id: '853667514915225640', name: 'w_paginate_end', animated: false } as const -} as const; diff --git a/src/lib/common/ConfirmationPrompt.ts b/src/lib/common/ConfirmationPrompt.ts deleted file mode 100644 index b87d9ef..0000000 --- a/src/lib/common/ConfirmationPrompt.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { type CommandMessage, type SlashMessage } from '#lib'; -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, type MessageComponentInteraction, type MessageOptions } from 'discord.js'; - -/** - * Sends a message with buttons for the user to confirm or cancel the action. - */ -export class ConfirmationPrompt { - /** - * @param message The message that triggered the command - * @param messageOptions Options for sending the message - */ - protected constructor(protected message: CommandMessage | SlashMessage, protected messageOptions: MessageOptions) {} - - /** - * Sends a message with buttons for the user to confirm or cancel the action. - */ - protected async send(): Promise { - this.messageOptions.components = [ - new ActionRowBuilder().addComponents( - new ButtonBuilder({ style: ButtonStyle.Success, customId: 'confirmationPrompt_confirm', label: 'Yes' }), - new ButtonBuilder({ style: ButtonStyle.Danger, customId: 'confirmationPrompt_cancel', label: 'No' }) - ) - ]; - - const msg = await this.message.channel!.send(this.messageOptions); - - return await new Promise((resolve) => { - let responded = false; - const collector = msg.createMessageComponentCollector({ - filter: (interaction) => interaction.message?.id == msg.id, - time: 300_000 - }); - - collector.on('collect', async (interaction: MessageComponentInteraction) => { - await interaction.deferUpdate().catch(() => undefined); - if (interaction.user.id == this.message.author.id || this.message.client.config.owners.includes(interaction.user.id)) { - if (interaction.customId === 'confirmationPrompt_confirm') { - responded = true; - collector.stop(); - resolve(true); - } else if (interaction.customId === 'confirmationPrompt_cancel') { - responded = true; - collector.stop(); - resolve(false); - } - } - }); - - collector.on('end', async () => { - await msg.delete().catch(() => undefined); - if (!responded) resolve(false); - }); - }); - } - - /** - * Sends a message with buttons for the user to confirm or cancel the action. - * @param message The message that triggered the command - * @param sendOptions Options for sending the message - */ - public static async send(message: CommandMessage | SlashMessage, sendOptions: MessageOptions): Promise { - return new ConfirmationPrompt(message, sendOptions).send(); - } -} diff --git a/src/lib/common/DeleteButton.ts b/src/lib/common/DeleteButton.ts deleted file mode 100644 index 340d07f..0000000 --- a/src/lib/common/DeleteButton.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { PaginateEmojis, type CommandMessage, type SlashMessage } from '#lib'; -import { CommandUtil } from 'discord-akairo'; -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - MessageComponentInteraction, - MessageEditOptions, - MessagePayload, - type MessageOptions -} from 'discord.js'; - -/** - * Sends a message with a button for the user to delete it. - */ -export class DeleteButton { - /** - * @param message The message to respond to - * @param messageOptions The send message options - */ - protected constructor(protected message: CommandMessage | SlashMessage, protected messageOptions: MessageOptions) {} - - /** - * Sends a message with a button for the user to delete it. - */ - protected async send() { - this.updateComponents(); - - const msg = await this.message.util.reply(this.messageOptions); - - const collector = msg.createMessageComponentCollector({ - filter: (interaction) => interaction.customId == 'paginate__stop' && interaction.message?.id == msg.id, - time: 300000 - }); - - collector.on('collect', async (interaction: MessageComponentInteraction) => { - await interaction.deferUpdate().catch(() => undefined); - if (interaction.user.id == this.message.author.id || this.message.client.config.owners.includes(interaction.user.id)) { - if (msg.deletable && !CommandUtil.deletedMessages.has(msg.id)) await msg.delete(); - } - }); - - collector.on('end', async () => { - this.updateComponents(true, true); - await msg.edit(this.messageOptions).catch(() => undefined); - }); - } - - /** - * Generates the components for the message - * @param edit Whether or not the message is being edited - * @param disable Whether or not to disable the buttons - */ - protected updateComponents(edit = false, disable = false): void { - this.messageOptions.components = [ - new ActionRowBuilder().addComponents( - new ButtonBuilder({ - style: ButtonStyle.Primary, - customId: 'paginate__stop', - emoji: PaginateEmojis.STOP, - disabled: disable - }) - ) - ]; - if (edit) { - this.messageOptions.reply = undefined; - } - } - - /** - * Sends a message with a button for the user to delete it. - * @param message The message to respond to - * @param options The send message options - */ - public static async send(message: CommandMessage | SlashMessage, options: Omit) { - return new DeleteButton(message, options).send(); - } -} diff --git a/src/lib/common/HighlightManager.ts b/src/lib/common/HighlightManager.ts deleted file mode 100644 index 4f891b7..0000000 --- a/src/lib/common/HighlightManager.ts +++ /dev/null @@ -1,485 +0,0 @@ -import { addToArray, format, Highlight, removeFromArray, timestamp, type HighlightWord } from '#lib'; -import assert from 'assert/strict'; -import { - ChannelType, - Collection, - GuildMember, - type Channel, - type Client, - type Message, - type Snowflake, - type TextBasedChannel -} from 'discord.js'; -import { colors, Time } from '../utils/BushConstants.js'; -import { sanitizeInputForDiscord } from './util/Format.js'; - -const NOTIFY_COOLDOWN = 5 * Time.Minute; -const OWNER_NOTIFY_COOLDOWN = 5 * Time.Minute; -const LAST_MESSAGE_COOLDOWN = 5 * Time.Minute; - -type users = Set; -type channels = Set; -type word = HighlightWord; -type guild = Snowflake; -type user = Snowflake; -type lastMessage = Date; -type lastDM = Message; - -type lastDmInfo = [lastDM: lastDM, guild: guild, channel: Snowflake, highlights: HighlightWord[]]; - -export class HighlightManager { - /** - * Cached guild highlights. - */ - public readonly guildHighlights = new Collection>(); - - //~ /** - //~ * Cached global highlights. - //~ */ - //~ public readonly globalHighlights = new Collection(); - - /** - * A collection of cooldowns of when a user last sent a message in a particular guild. - */ - public readonly userLastTalkedCooldown = new Collection>(); - - /** - * Users that users have blocked - */ - public readonly userBlocks = new Collection>(); - - /** - * Channels that users have blocked - */ - public readonly channelBlocks = new Collection>(); - - /** - * A collection of cooldowns of when the bot last sent each user a highlight message. - */ - public readonly lastedDMedUserCooldown = new Collection(); - - /** - * @param client The client to use. - */ - public constructor(public readonly client: Client) {} - - /** - * Sync the cache with the database. - */ - public async syncCache(): Promise { - const highlights = await Highlight.findAll(); - - this.guildHighlights.clear(); - - for (const highlight of highlights) { - highlight.words.forEach((word) => { - if (!this.guildHighlights.has(highlight.guild)) this.guildHighlights.set(highlight.guild, new Collection()); - const guildCache = this.guildHighlights.get(highlight.guild)!; - if (!guildCache.get(word)) guildCache.set(word, new Set()); - guildCache.get(word)!.add(highlight.user); - }); - - if (!this.userBlocks.has(highlight.guild)) this.userBlocks.set(highlight.guild, new Collection()); - this.userBlocks.get(highlight.guild)!.set(highlight.user, new Set(highlight.blacklistedUsers)); - - if (!this.channelBlocks.has(highlight.guild)) this.channelBlocks.set(highlight.guild, new Collection()); - this.channelBlocks.get(highlight.guild)!.set(highlight.user, new Set(highlight.blacklistedChannels)); - } - } - - /** - * Checks a message for highlights. - * @param message The message to check. - * @returns A collection users mapped to the highlight matched - */ - public checkMessage(message: Message): Collection { - // even if there are multiple matches, only the first one is returned - const ret = new Collection(); - if (!message.content || !message.inGuild()) return ret; - if (!this.guildHighlights.has(message.guildId)) return ret; - - const guildCache = this.guildHighlights.get(message.guildId)!; - - for (const [word, users] of guildCache.entries()) { - if (!this.isMatch(message.content, word)) continue; - - for (const user of users) { - if (ret.has(user)) continue; - - if (!message.channel.permissionsFor(user)?.has('ViewChannel')) continue; - - const blockedUsers = this.userBlocks.get(message.guildId)?.get(user) ?? new Set(); - if (blockedUsers.has(message.author.id)) { - void this.client.console.verbose( - 'Highlight', - `Highlight ignored because <<${this.client.users.cache.get(user)?.tag ?? user}>> blocked the user <<${ - message.author.tag - }>>` - ); - continue; - } - const blockedChannels = this.channelBlocks.get(message.guildId)?.get(user) ?? new Set(); - if (blockedChannels.has(message.channel.id)) { - void this.client.console.verbose( - 'Highlight', - `Highlight ignored because <<${this.client.users.cache.get(user)?.tag ?? user}>> blocked the channel <<${ - message.channel.name - }>>` - ); - continue; - } - if (message.mentions.has(user)) { - void this.client.console.verbose( - 'Highlight', - `Highlight ignored because <<${this.client.users.cache.get(user)?.tag ?? user}>> is already mentioned in the message.` - ); - continue; - } - ret.set(user, word); - } - } - - return ret; - } - - /** - * Checks a user provided phrase for their highlights. - * @param guild The guild to check in. - * @param user The user to get the highlights for. - * @param phrase The phrase for highlights in. - * @returns A collection of the user's highlights mapped to weather or not it was matched. - */ - public async checkPhrase(guild: Snowflake, user: Snowflake, phrase: string): Promise> { - const highlights = await Highlight.findAll({ where: { guild, user } }); - - const results = new Collection(); - - for (const highlight of highlights) { - for (const word of highlight.words) { - results.set(word, this.isMatch(phrase, word)); - } - } - - return results; - } - - /** - * Checks a particular highlight for a match within a phrase. - * @param phrase The phrase to check for the word in. - * @param hl The highlight to check for. - * @returns Whether or not the highlight was matched. - */ - private isMatch(phrase: string, hl: HighlightWord): boolean { - if (hl.regex) { - return new RegExp(hl.word, 'gi').test(phrase); - } else { - if (hl.word.includes(' ')) { - return phrase.toLocaleLowerCase().includes(hl.word.toLocaleLowerCase()); - } else { - const words = phrase.split(/\s*\b\s/); - return words.some((w) => w.toLocaleLowerCase() === hl.word.toLocaleLowerCase()); - } - } - } - - /** - * Adds a new highlight to a user in a particular guild. - * @param guild The guild to add the highlight to. - * @param user The user to add the highlight to. - * @param hl The highlight to add. - * @returns A string representing a user error or a boolean indicating the database success. - */ - public async addHighlight(guild: Snowflake, user: Snowflake, hl: HighlightWord): Promise { - if (!this.guildHighlights.has(guild)) this.guildHighlights.set(guild, new Collection()); - const guildCache = this.guildHighlights.get(guild)!; - - if (!guildCache.has(hl)) guildCache.set(hl, new Set()); - guildCache.get(hl)!.add(user); - - const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); - - if (highlight.words.some((w) => w.word === hl.word)) return `You have already highlighted "${hl.word}".`; - - highlight.words = addToArray(highlight.words, hl); - - return Boolean(await highlight.save().catch(() => false)); - } - - /** - * Removes a highlighted word for a user in a particular guild. - * @param guild The guild to remove the highlight from. - * @param user The user to remove the highlight from. - * @param hl The word to remove. - * @returns A string representing a user error or a boolean indicating the database success. - */ - public async removeHighlight(guild: Snowflake, user: Snowflake, hl: string): Promise { - if (!this.guildHighlights.has(guild)) this.guildHighlights.set(guild, new Collection()); - const guildCache = this.guildHighlights.get(guild)!; - - const wordCache = guildCache.find((_, key) => key.word === hl); - - if (!wordCache?.has(user)) return `You have not highlighted "${hl}".`; - - wordCache!.delete(user); - - const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); - - const toRemove = highlight.words.find((w) => w.word === hl); - if (!toRemove) return `Uhhhhh... This shouldn't happen.`; - - highlight.words = removeFromArray(highlight.words, toRemove); - - return Boolean(await highlight.save().catch(() => false)); - } - - /** - * Remove all highlight words for a user in a particular guild. - * @param guild The guild to remove the highlights from. - * @param user The user to remove the highlights from. - * @returns A boolean indicating the database success. - */ - public async removeAllHighlights(guild: Snowflake, user: Snowflake): Promise { - if (!this.guildHighlights.has(guild)) this.guildHighlights.set(guild, new Collection()); - const guildCache = this.guildHighlights.get(guild)!; - - for (const [word, users] of guildCache.entries()) { - if (users.has(user)) users.delete(user); - if (users.size === 0) guildCache.delete(word); - } - - const highlight = await Highlight.findOne({ where: { guild, user } }); - - if (!highlight) return false; - - highlight.words = []; - - return Boolean(await highlight.save().catch(() => false)); - } - - /** - * Adds a new user or channel block to a user in a particular guild. - * @param guild The guild to add the block to. - * @param user The user that is blocking the target. - * @param target The target that is being blocked. - * @returns The result of the operation. - */ - public async addBlock( - guild: Snowflake, - user: Snowflake, - target: GuildMember | TextBasedChannel - ): Promise { - const cacheKey = `${target instanceof GuildMember ? 'user' : 'channel'}Blocks` as const; - const databaseKey = `blacklisted${target instanceof GuildMember ? 'Users' : 'Channels'}` as const; - - const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); - - if (highlight[databaseKey].includes(target.id)) return HighlightBlockResult.ALREADY_BLOCKED; - - const newBlocks = addToArray(highlight[databaseKey], target.id); - - highlight[databaseKey] = newBlocks; - const res = await highlight.save().catch(() => false); - if (!res) return HighlightBlockResult.ERROR; - - if (!this[cacheKey].has(guild)) this[cacheKey].set(guild, new Collection()); - const guildBlocks = this[cacheKey].get(guild)!; - guildBlocks.set(user, new Set(newBlocks)); - - return HighlightBlockResult.SUCCESS; - } - - /** - * Removes a user or channel block from a user in a particular guild. - * @param guild The guild to remove the block from. - * @param user The user that is unblocking the target. - * @param target The target that is being unblocked. - * @returns The result of the operation. - */ - public async removeBlock(guild: Snowflake, user: Snowflake, target: GuildMember | Channel): Promise { - const cacheKey = `${target instanceof GuildMember ? 'user' : 'channel'}Blocks` as const; - const databaseKey = `blacklisted${target instanceof GuildMember ? 'Users' : 'Channels'}` as const; - - const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); - - if (!highlight[databaseKey].includes(target.id)) return HighlightUnblockResult.NOT_BLOCKED; - - const newBlocks = removeFromArray(highlight[databaseKey], target.id); - - highlight[databaseKey] = newBlocks; - const res = await highlight.save().catch(() => false); - if (!res) return HighlightUnblockResult.ERROR; - - if (!this[cacheKey].has(guild)) this[cacheKey].set(guild, new Collection()); - const guildBlocks = this[cacheKey].get(guild)!; - guildBlocks.set(user, new Set(newBlocks)); - - return HighlightUnblockResult.SUCCESS; - } - - /** - * Sends a user a direct message to alert them of their highlight being triggered. - * @param message The message that triggered the highlight. - * @param user The user who's highlights was triggered. - * @param hl The highlight that was matched. - * @returns Whether or a dm was sent. - */ - public async notify(message: Message, user: Snowflake, hl: HighlightWord): Promise { - assert(message.inGuild()); - - this.client.console.debug(`Notifying ${user} of highlight ${hl.word} in ${message.guild.name}`); - - dmCooldown: { - const lastDM = this.lastedDMedUserCooldown.get(user); - if (!lastDM?.[0]) break dmCooldown; - - const cooldown = this.client.config.owners.includes(user) ? OWNER_NOTIFY_COOLDOWN : NOTIFY_COOLDOWN; - - if (new Date().getTime() - lastDM[0].createdAt.getTime() < cooldown) { - void this.client.console.verbose('Highlight', `User <<${user}>> has been DMed recently.`); - - if (lastDM[0].embeds.length < 10) { - this.client.console.debug(`Trying to add to notification queue for ${user}`); - return this.addToNotification(lastDM, message, hl); - } - - this.client.console.debug(`User has too many embeds (${lastDM[0].embeds.length}).`); - return false; - } - } - - talkCooldown: { - const lastTalked = this.userLastTalkedCooldown.get(message.guildId)?.get(user); - if (!lastTalked) break talkCooldown; - - presence: { - // incase the bot left the guild - if (message.guild) { - const member = message.guild.members.cache.get(user); - if (!member) { - this.client.console.debug(`No member found for ${user} in ${message.guild.name}`); - break presence; - } - - const presence = member.presence ?? (await member.fetch()).presence; - if (!presence) { - this.client.console.debug(`No presence found for ${user} in ${message.guild.name}`); - break presence; - } - - if (presence.status === 'offline') { - void this.client.console.verbose('Highlight', `User <<${user}>> is offline.`); - break talkCooldown; - } - } - } - - const now = new Date().getTime(); - const talked = lastTalked.getTime(); - - if (now - talked < LAST_MESSAGE_COOLDOWN) { - void this.client.console.verbose('Highlight', `User <<${user}>> has talked too recently.`); - - setTimeout(() => { - const newTalked = this.userLastTalkedCooldown.get(message.guildId)?.get(user)?.getTime(); - if (talked !== newTalked) return; - - void this.notify(message, user, hl); - }, LAST_MESSAGE_COOLDOWN).unref(); - - return false; - } - } - - return this.client.users - .send(user, { - // eslint-disable-next-line @typescript-eslint/no-base-to-string - content: `In ${format.input(message.guild.name)} ${message.channel}, your highlight "${hl.word}" was matched:`, - embeds: [this.generateDmEmbed(message, hl)] - }) - .then((dm) => { - this.lastedDMedUserCooldown.set(user, [dm, message.guildId!, message.channelId, [hl]]); - return true; - }) - .catch(() => false); - } - - private async addToNotification( - [originalDm, guild, channel, originalHl]: lastDmInfo, - message: Message, - hl: HighlightWord - ): Promise { - assert(originalDm.embeds.length < 10); - assert(originalDm.embeds.length > 0); - assert(originalDm.channel.type === ChannelType.DM); - this.client.console.debug( - `Adding to notification queue for ${originalDm.channel.recipient?.tag ?? originalDm.channel.recipientId}` - ); - - const sameGuild = guild === message.guildId; - const sameChannel = channel === message.channel.id; - const sameWord = originalHl.every((w) => w.word === hl.word); - - /* eslint-disable @typescript-eslint/no-base-to-string */ - return originalDm - .edit({ - content: `In ${sameGuild ? format.input(message.guild?.name ?? '[Unknown]') : 'multiple servers'} ${ - sameChannel ? message.channel ?? '[Unknown]' : 'multiple channels' - }, ${sameWord ? `your highlight "${hl.word}" was matched:` : 'multiple highlights were matched:'}`, - embeds: [...originalDm.embeds.map((e) => e.toJSON()), this.generateDmEmbed(message, hl)] - }) - .then(() => true) - .catch(() => false); - /* eslint-enable @typescript-eslint/no-base-to-string */ - } - - private generateDmEmbed(message: Message, hl: HighlightWord) { - const recentMessages = message.channel.messages.cache - .filter((m) => m.createdTimestamp <= message.createdTimestamp && m.id !== message.id) - .filter((m) => m.cleanContent?.trim().length > 0) - .sort((a, b) => b.createdTimestamp - a.createdTimestamp) - .first(4) - .reverse(); - - return { - description: [ - // eslint-disable-next-line @typescript-eslint/no-base-to-string - message.channel!.toString(), - ...[...recentMessages, message].map( - (m) => `${timestamp(m.createdAt, 't')} ${format.input(`${m.author.tag}:`)} ${m.cleanContent.trim().substring(0, 512)}` - ) - ].join('\n'), - author: { name: hl.regex ? `/${hl.word}/gi` : hl.word }, - fields: [{ name: 'Source message', value: `[Jump to message](${message.url})` }], - color: colors.default, - footer: { text: `Triggered in ${sanitizeInputForDiscord(`${message.guild}`)}` }, - timestamp: message.createdAt.toISOString() - }; - } - - /** - * Updates the time that a user last talked in a particular guild. - * @param message The message the user sent. - */ - public updateLastTalked(message: Message): void { - if (!message.inGuild()) return; - const lastTalked = ( - this.userLastTalkedCooldown.has(message.guildId) - ? this.userLastTalkedCooldown - : this.userLastTalkedCooldown.set(message.guildId, new Collection()) - ).get(message.guildId)!; - - lastTalked.set(message.author.id, new Date()); - } -} - -export enum HighlightBlockResult { - ALREADY_BLOCKED, - ERROR, - SUCCESS -} - -export enum HighlightUnblockResult { - NOT_BLOCKED, - ERROR, - SUCCESS -} diff --git a/src/lib/common/Sentry.ts b/src/lib/common/Sentry.ts deleted file mode 100644 index 2792203..0000000 --- a/src/lib/common/Sentry.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { RewriteFrames } from '@sentry/integrations'; -import * as SentryNode from '@sentry/node'; -import { Integrations } from '@sentry/node'; -import type { Config } from '../../../config/Config.js'; - -export class Sentry { - public constructor(rootdir: string, config: Config) { - if (config.credentials.sentryDsn === null) throw TypeError('sentryDsn cannot be null'); - - SentryNode.init({ - dsn: config.credentials.sentryDsn, - environment: config.environment, - tracesSampleRate: 1.0, - integrations: [ - new RewriteFrames({ - root: rootdir - }), - new Integrations.OnUnhandledRejection({ - mode: 'none' - }) - ] - }); - } -} diff --git a/src/lib/common/tags.ts b/src/lib/common/tags.ts deleted file mode 100644 index 098cf29..0000000 --- a/src/lib/common/tags.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* these functions are adapted from the common-tags npm package which is licensed under the MIT license */ -/* the js docs are adapted from the @types/common-tags npm package which is licensed under the MIT license */ - -/** - * Strips the **initial** indentation from the beginning of each line in a multiline string. - */ -export function stripIndent(strings: TemplateStringsArray, ...expressions: any[]) { - const str = format(strings, ...expressions); - // remove the shortest leading indentation from each line - const match = str.match(/^[^\S\n]*(?=\S)/gm); - const indent = match && Math.min(...match.map((el) => el.length)); - if (indent) { - const regexp = new RegExp(`^.{${indent}}`, 'gm'); - return str.replace(regexp, ''); - } - return str; -} - -/** - * Strips **all** of the indentation from the beginning of each line in a multiline string. - */ -export function stripIndents(strings: TemplateStringsArray, ...expressions: any[]) { - const str = format(strings, ...expressions); - // remove all indentation from each line - return str.replace(/^[^\S\n]+/gm, ''); -} - -function format(strings: TemplateStringsArray, ...expressions: any[]) { - const str = strings - .reduce((result, string, index) => ''.concat(result, expressions[index - 1], string)) - .replace(/[^\S\n]+$/gm, '') - .replace(/^\n/, ''); - return str; -} diff --git a/src/lib/common/typings/BushInspectOptions.ts b/src/lib/common/typings/BushInspectOptions.ts deleted file mode 100644 index 30ed01a..0000000 --- a/src/lib/common/typings/BushInspectOptions.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { type InspectOptions } from 'util'; - -/** - * {@link https://nodejs.org/api/util.html#utilinspectobject-showhidden-depth-colors util.inspect Options Documentation} - */ -export interface BushInspectOptions extends InspectOptions { - /** - * If `true`, object's non-enumerable symbols and properties are included in the - * formatted result. [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) - * and [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) entries - * are also included as well as user defined prototype properties (excluding method properties). - * - * @default false - */ - showHidden?: boolean | undefined; - - /** - * Specifies the number of times to recurse while formatting `object`. This is useful - * for inspecting large objects. To recurse up to the maximum call stack size pass - * `Infinity` or `null`. - * - * @default 2 - */ - depth?: number | null | undefined; - - /** - * If `true`, the output is styled with ANSI color codes. Colors are customizable. See - * [Customizing util.inspect colors](https://nodejs.org/api/util.html#util_customizing_util_inspect_colors). - * - * @default false - */ - colors?: boolean | undefined; - - /** - * If `false`, `[util.inspect.custom](depth, opts)` functions are not invoked. - * - * @default true - */ - customInspect?: boolean | undefined; - - /** - * If `true`, `Proxy` inspection includes the - * [`target` and `handler`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Terminology) - * objects. - * - * @default false - */ - showProxy?: boolean | undefined; - - /** - * Specifies the maximum number of `Array`, [`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray), - * [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) and - * [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) elements to - * include when formatting. Set to `null` or `Infinity` to show all elements. - * Set to `0` or negative to show no elements. - * - * @default 100 - */ - maxArrayLength?: number | null | undefined; - - /** - * Specifies the maximum number of characters to include when formatting. Set to - * `null` or `Infinity` to show all elements. Set to `0` or negative to show no - * characters. - * - * @default 10000 - */ - maxStringLength?: number | null | undefined; - - /** - * The length at which input values are split across multiple lines. Set to - * `Infinity` to format the input as a single line (in combination with compact set - * to `true` or any number >= `1`). - * - * @default 80 - */ - breakLength?: number | undefined; - - /** - * Setting this to `false` causes each object key to be displayed on a new line. It - * will break on new lines in text that is longer than `breakLength`. If set to a - * number, the most `n` inner elements are united on a single line as long as all - * properties fit into `breakLength`. Short array elements are also grouped together. - * - * @default 3 - */ - compact?: boolean | number | undefined; - - /** - * If set to `true` or a function, all properties of an object, and `Set` and `Map` - * entries are sorted in the resulting string. If set to `true` the - * [default sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) is used. - * If set to a function, it is used as a - * [compare function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters). - * - * @default false - */ - sorted?: boolean | ((a: string, b: string) => number) | undefined; - - /** - * If set to `true`, getters are inspected. If set to `'get'`, only getters without a - * corresponding setter are inspected. If set to `'set'`, only getters with a - * corresponding setter are inspected. This might cause side effects depending on - * the getter function. - * - * @default false - */ - getters?: 'get' | 'set' | boolean | undefined; - - /** - * If set to `true`, an underscore is used to separate every three digits in all bigints and numbers. - * - * @default false - */ - numericSeparator?: boolean; - - /** - * Whether or not to inspect strings. - * - * @default false - */ - inspectStrings?: boolean; -} diff --git a/src/lib/common/typings/CodeBlockLang.ts b/src/lib/common/typings/CodeBlockLang.ts deleted file mode 100644 index d0eb4f3..0000000 --- a/src/lib/common/typings/CodeBlockLang.ts +++ /dev/null @@ -1,311 +0,0 @@ -export type CodeBlockLang = - | '1c' - | 'abnf' - | 'accesslog' - | 'actionscript' - | 'ada' - | 'arduino' - | 'ino' - | 'armasm' - | 'arm' - | 'avrasm' - | 'actionscript' - | 'as' - | 'angelscript' - | 'asc' - | 'apache' - | 'apacheconf' - | 'applescript' - | 'osascript' - | 'arcade' - | 'asciidoc' - | 'adoc' - | 'aspectj' - | 'autohotkey' - | 'autoit' - | 'awk' - | 'mawk' - | 'nawk' - | 'gawk' - | 'bash' - | 'sh' - | 'zsh' - | 'basic' - | 'bnf' - | 'brainfuck' - | 'bf' - | 'csharp' - | 'cs' - | 'c' - | 'h' - | 'cpp' - | 'hpp' - | 'cc' - | 'hh' - | 'c++' - | 'h++' - | 'cxx' - | 'hxx' - | 'cal' - | 'cos' - | 'cls' - | 'cmake' - | 'cmake.in' - | 'coq' - | 'csp' - | 'css' - | 'capnproto' - | 'capnp' - | 'clojure' - | 'clj' - | 'coffeescript' - | 'coffee' - | 'cson' - | 'iced' - | 'crmsh' - | 'crm' - | 'pcmk' - | 'crystal' - | 'cr' - | 'd' - | 'dns' - | 'zone' - | 'bind' - | 'dos' - | 'bat' - | 'cmd' - | 'dart' - | 'dpr' - | 'dfm' - | 'pas' - | 'pascal' - | 'diff' - | 'patch' - | 'django' - | 'jinja' - | 'dockerfile' - | 'docker' - | 'dsconfig' - | 'dts' - | 'dust' - | 'dst' - | 'ebnf' - | 'elixir' - | 'elm' - | 'erlang' - | 'erl' - | 'excel' - | 'xls' - | 'xlsx' - | 'fsharp' - | 'fs' - | 'fix' - | 'fortran' - | 'f90' - | 'f95' - | 'gcode' - | 'nc' - | 'gams' - | 'gms' - | 'gauss' - | 'gss' - | 'gherkin' - | 'go' - | 'golang' - | 'golo' - | 'gololang' - | 'gradle' - | 'groovy' - | 'xml' - | 'html' - | 'xhtml' - | 'rss' - | 'atom' - | 'xjb' - | 'xsd' - | 'xsl' - | 'plist' - | 'svg' - | 'http' - | 'https' - | 'haml' - | 'handlebars' - | 'hbs' - | 'html.hbs' - | 'html.handlebars' - | 'haskell' - | 'hs' - | 'haxe' - | 'hx' - | 'hlsl' - | 'hy' - | 'hylang' - | 'ini' - | 'toml' - | 'inform7' - | 'i7' - | 'irpf90' - | 'json' - | 'java' - | 'jsp' - | 'javascript' - | 'js' - | 'jsx' - | 'julia' - | 'julia-repl' - | 'kotlin' - | 'kt' - | 'tex' - | 'leaf' - | 'lasso' - | 'ls' - | 'lassoscript' - | 'less' - | 'ldif' - | 'lisp' - | 'livecodeserver' - | 'livescript' - | 'ls' - | 'lua' - | 'makefile' - | 'mk' - | 'mak' - | 'make' - | 'markdown' - | 'md' - | 'mkdown' - | 'mkd' - | 'mathematica' - | 'mma' - | 'wl' - | 'matlab' - | 'maxima' - | 'mel' - | 'mercury' - | 'mizar' - | 'mojolicious' - | 'monkey' - | 'moonscript' - | 'moon' - | 'n1ql' - | 'nsis' - | 'nginx' - | 'nginxconf' - | 'nim' - | 'nimrod' - | 'nix' - | 'ocaml' - | 'ml' - | 'objectivec' - | 'mm' - | 'objc' - | 'obj-c' - | 'obj-c++' - | 'objective-c++' - | 'glsl' - | 'openscad' - | 'scad' - | 'ruleslanguage' - | 'oxygene' - | 'pf' - | 'pf.conf' - | 'php' - | 'parser3' - | 'perl' - | 'pl' - | 'pm' - | 'plaintext' - | 'txt' - | 'text' - | 'pony' - | 'pgsql' - | 'postgres' - | 'postgresql' - | 'powershell' - | 'ps' - | 'ps1' - | 'processing' - | 'prolog' - | 'properties' - | 'protobuf' - | 'puppet' - | 'pp' - | 'python' - | 'py' - | 'gyp' - | 'profile' - | 'python-repl' - | 'pycon' - | 'k' - | 'kdb' - | 'qml' - | 'r' - | 'reasonml' - | 're' - | 'rib' - | 'rsl' - | 'graph' - | 'instances' - | 'ruby' - | 'rb' - | 'gemspec' - | 'podspec' - | 'thor' - | 'irb' - | 'rust' - | 'rs' - | 'sas' - | 'scss' - | 'sql' - | 'p21' - | 'step' - | 'stp' - | 'scala' - | 'scheme' - | 'scilab' - | 'sci' - | 'shell' - | 'console' - | 'smali' - | 'smalltalk' - | 'st' - | 'sml' - | 'ml' - | 'stan' - | 'stanfuncs' - | 'stata' - | 'stylus' - | 'styl' - | 'subunit' - | 'swift' - | 'tcl' - | 'tk' - | 'tap' - | 'thrift' - | 'tp' - | 'twig' - | 'craftcms' - | 'typescript' - | 'ts' - | 'vbnet' - | 'vb' - | 'vbscript' - | 'vbs' - | 'vhdl' - | 'vala' - | 'verilog' - | 'v' - | 'vim' - | 'axapta' - | 'x++' - | 'x86asm' - | 'xl' - | 'tao' - | 'xquery' - | 'xpath' - | 'xq' - | 'yml' - | 'yaml' - | 'zephir' - | 'zep' - | 'ansi'; diff --git a/src/lib/common/util/Arg.ts b/src/lib/common/util/Arg.ts deleted file mode 100644 index d362225..0000000 --- a/src/lib/common/util/Arg.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { - type BaseBushArgumentType, - type BushArgumentType, - type BushArgumentTypeCaster, - type CommandMessage, - type SlashMessage -} from '#lib'; -import { Argument, type Command, type Flag, type ParsedValuePredicate } from 'discord-akairo'; -import { type Message } from 'discord.js'; - -/** - * Casts a phrase to this argument's type. - * @param type - The type to cast to. - * @param message - Message that called the command. - * @param phrase - Phrase to process. - */ -export async function cast(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise>; -export async function cast(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise; -export async function cast(type: AT | ATC, message: CommandMessage | SlashMessage, phrase: string): Promise; -export async function cast( - this: ThisType, - type: ATC | AT, - message: CommandMessage | SlashMessage, - phrase: string -): Promise { - return Argument.cast.call(this, type as any, message.client.commandHandler.resolver, message as Message, phrase); -} - -/** - * Creates a type that is the left-to-right composition of the given types. - * If any of the types fails, the entire composition fails. - * @param types - Types to use. - */ -export function compose(...types: T[]): ATCATCR; -export function compose(...types: T[]): ATCBAT; -export function compose(...types: (AT | ATC)[]): ATC; -export function compose(...types: (AT | ATC)[]): ATC { - return Argument.compose(...(types as any)); -} - -/** - * Creates a type that is the left-to-right composition of the given types. - * If any of the types fails, the composition still continues with the failure passed on. - * @param types - Types to use. - */ -export function composeWithFailure(...types: T[]): ATCATCR; -export function composeWithFailure(...types: T[]): ATCBAT; -export function composeWithFailure(...types: (AT | ATC)[]): ATC; -export function composeWithFailure(...types: (AT | ATC)[]): ATC { - return Argument.composeWithFailure(...(types as any)); -} - -/** - * Checks if something is null, undefined, or a fail flag. - * @param value - Value to check. - */ -export function isFailure(value: any): value is null | undefined | (Flag & { value: any }) { - return Argument.isFailure(value); -} - -/** - * Creates a type from multiple types (product type). - * Only inputs where each type resolves with a non-void value are valid. - * @param types - Types to use. - */ -export function product(...types: T[]): ATCATCR; -export function product(...types: T[]): ATCBAT; -export function product(...types: (AT | ATC)[]): ATC; -export function product(...types: (AT | ATC)[]): ATC { - return Argument.product(...(types as any)); -} - -/** - * Creates a type where the parsed value must be within a range. - * @param type - The type to use. - * @param min - Minimum value. - * @param max - Maximum value. - * @param inclusive - Whether or not to be inclusive on the upper bound. - */ -export function range(type: T, min: number, max: number, inclusive?: boolean): ATCATCR; -export function range(type: T, min: number, max: number, inclusive?: boolean): ATCBAT; -export function range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC; -export function range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC { - return Argument.range(type as any, min, max, inclusive); -} - -/** - * Creates a type that parses as normal but also tags it with some data. - * Result is in an object `{ tag, value }` and wrapped in `Flag.fail` when failed. - * @param type - The type to use. - * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. - */ -export function tagged(type: T, tag?: any): ATCATCR; -export function tagged(type: T, tag?: any): ATCBAT; -export function tagged(type: AT | ATC, tag?: any): ATC; -export function tagged(type: AT | ATC, tag?: any): ATC { - return Argument.tagged(type as any, tag); -} - -/** - * Creates a type from multiple types (union type). - * The first type that resolves to a non-void value is used. - * Each type will also be tagged using `tagged` with themselves. - * @param types - Types to use. - */ -export function taggedUnion(...types: T[]): ATCATCR; -export function taggedUnion(...types: T[]): ATCBAT; -export function taggedUnion(...types: (AT | ATC)[]): ATC; -export function taggedUnion(...types: (AT | ATC)[]): ATC { - return Argument.taggedUnion(...(types as any)); -} - -/** - * Creates a type that parses as normal but also tags it with some data and carries the original input. - * Result is in an object `{ tag, input, value }` and wrapped in `Flag.fail` when failed. - * @param type - The type to use. - * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. - */ -export function taggedWithInput(type: T, tag?: any): ATCATCR; -export function taggedWithInput(type: T, tag?: any): ATCBAT; -export function taggedWithInput(type: AT | ATC, tag?: any): ATC; -export function taggedWithInput(type: AT | ATC, tag?: any): ATC { - return Argument.taggedWithInput(type as any, tag); -} - -/** - * Creates a type from multiple types (union type). - * The first type that resolves to a non-void value is used. - * @param types - Types to use. - */ -export function union(...types: T[]): ATCATCR; -export function union(...types: T[]): ATCBAT; -export function union(...types: (AT | ATC)[]): ATC; -export function union(...types: (AT | ATC)[]): ATC { - return Argument.union(...(types as any)); -} - -/** - * Creates a type with extra validation. - * If the predicate is not true, the value is considered invalid. - * @param type - The type to use. - * @param predicate - The predicate function. - */ -export function validate(type: T, predicate: ParsedValuePredicate): ATCATCR; -export function validate(type: T, predicate: ParsedValuePredicate): ATCBAT; -export function validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC; -export function validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC { - return Argument.validate(type as any, predicate); -} - -/** - * Creates a type that parses as normal but also carries the original input. - * Result is in an object `{ input, value }` and wrapped in `Flag.fail` when failed. - * @param type - The type to use. - */ -export function withInput(type: T): ATC>; -export function withInput(type: T): ATCBAT; -export function withInput(type: AT | ATC): ATC; -export function withInput(type: AT | ATC): ATC { - return Argument.withInput(type as any); -} - -type BushArgumentTypeCasterReturn = R extends BushArgumentTypeCaster ? S : R; -/** ```ts - * = BushArgumentTypeCaster - * ``` */ -type ATC = BushArgumentTypeCaster; -/** ```ts - * keyof BaseBushArgumentType - * ``` */ -type KBAT = keyof BaseBushArgumentType; -/** ```ts - * = BushArgumentTypeCasterReturn - * ``` */ -type ATCR = BushArgumentTypeCasterReturn; -/** ```ts - * BushArgumentType - * ``` */ -type AT = BushArgumentType; -/** ```ts - * BaseBushArgumentType - * ``` */ -type BAT = BaseBushArgumentType; - -/** ```ts - * = BushArgumentTypeCaster> - * ``` */ -type ATCATCR = BushArgumentTypeCaster>; -/** ```ts - * = BushArgumentTypeCaster - * ``` */ -type ATCBAT = BushArgumentTypeCaster; diff --git a/src/lib/common/util/Format.ts b/src/lib/common/util/Format.ts deleted file mode 100644 index debaf4b..0000000 --- a/src/lib/common/util/Format.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { type CodeBlockLang } from '#lib'; -import { - bold as discordBold, - codeBlock as discordCodeBlock, - escapeBold as discordEscapeBold, - escapeCodeBlock as discordEscapeCodeBlock, - escapeInlineCode as discordEscapeInlineCode, - escapeItalic as discordEscapeItalic, - escapeMarkdown, - escapeSpoiler as discordEscapeSpoiler, - escapeStrikethrough as discordEscapeStrikethrough, - escapeUnderline as discordEscapeUnderline, - inlineCode as discordInlineCode, - italic as discordItalic, - spoiler as discordSpoiler, - strikethrough as discordStrikethrough, - underscore as discordUnderscore -} from 'discord.js'; - -/** - * Wraps the content inside a codeblock with no language. - * @param content The content to wrap. - */ -export function codeBlock(content: string): string; - -/** - * Wraps the content inside a codeblock with the specified language. - * @param language The language for the codeblock. - * @param content The content to wrap. - */ -export function codeBlock(language: CodeBlockLang, content: string): string; -export function codeBlock(languageOrContent: string, content?: string): string { - return typeof content === 'undefined' - ? discordCodeBlock(discordEscapeCodeBlock(`${languageOrContent}`)) - : discordCodeBlock(`${languageOrContent}`, discordEscapeCodeBlock(`${content}`)); -} - -/** - * Wraps the content inside \`backticks\`, which formats it as inline code. - * @param content The content to wrap. - */ -export function inlineCode(content: string): string { - return discordInlineCode(discordEscapeInlineCode(`${content}`)); -} - -/** - * Formats the content into italic text. - * @param content The content to wrap. - */ -export function italic(content: string): string { - return discordItalic(discordEscapeItalic(`${content}`)); -} - -/** - * Formats the content into bold text. - * @param content The content to wrap. - */ -export function bold(content: string): string { - return discordBold(discordEscapeBold(`${content}`)); -} - -/** - * Formats the content into underscored text. - * @param content The content to wrap. - */ -export function underscore(content: string): string { - return discordUnderscore(discordEscapeUnderline(`${content}`)); -} - -/** - * Formats the content into strike-through text. - * @param content The content to wrap. - */ -export function strikethrough(content: string): string { - return discordStrikethrough(discordEscapeStrikethrough(`${content}`)); -} - -/** - * Wraps the content inside spoiler (hidden text). - * @param content The content to wrap. - */ -export function spoiler(content: string): string { - return discordSpoiler(discordEscapeSpoiler(`${content}`)); -} - -/** - * Formats input: makes it bold and escapes any other markdown - * @param text The input - */ -export function input(text: string): string { - return bold(sanitizeInputForDiscord(`${text}`)); -} - -/** - * Formats input for logs: makes it highlighted - * @param text The input - */ -export function inputLog(text: string): string { - return `<<${sanitizeWtlAndControl(`${text}`)}>>`; -} - -/** - * Removes all characters in a string that are either control characters or change the direction of text etc. - * @param str The string you would like sanitized - */ -export function sanitizeWtlAndControl(str: string) { - // eslint-disable-next-line no-control-regex - return `${str}`.replace(/[\u0000-\u001F\u007F-\u009F\u200B]/g, ''); -} - -/** - * Removed wtl and control characters and escapes any other markdown - * @param text The input - */ -export function sanitizeInputForDiscord(text: string): string { - return escapeMarkdown(sanitizeWtlAndControl(`${text}`)); -} - -export { escapeMarkdown } from 'discord.js'; diff --git a/src/lib/common/util/Minecraft.ts b/src/lib/common/util/Minecraft.ts deleted file mode 100644 index a12ebf2..0000000 --- a/src/lib/common/util/Minecraft.ts +++ /dev/null @@ -1,349 +0,0 @@ -import { Byte, Int, parse } from '@ironm00n/nbt-ts'; -import { BitField } from 'discord.js'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -export enum FormattingCodes { - Black = '§0', - DarkBlue = '§1', - DarkGreen = '§2', - DarkAqua = '§3', - DarkRed = '§4', - DarkPurple = '§5', - Gold = '§6', - Gray = '§7', - DarkGray = '§8', - Blue = '§9', - Green = '§a', - Aqua = '§b', - Red = '§c', - LightPurple = '§d', - Yellow = '§e', - White = '§f', - - Obfuscated = '§k', - Bold = '§l', - Strikethrough = '§m', - Underline = '§n', - Italic = '§o', - Reset = '§r' -} - -// https://minecraft.fandom.com/wiki/Formatting_codes -export const formattingInfo = { - [FormattingCodes.Black]: { - foreground: 'rgb(0, 0, 0)', - foregroundDarker: 'rgb(0, 0, 0)', - background: 'rgb(0, 0, 0)', - backgroundDarker: 'rgb(0, 0, 0)', - ansi: '\u001b[0;30m' - }, - [FormattingCodes.DarkBlue]: { - foreground: 'rgb(0, 0, 170)', - foregroundDarker: 'rgb(0, 0, 118)', - background: 'rgb(0, 0, 42)', - backgroundDarker: 'rgb(0, 0, 29)', - ansi: '\u001b[0;34m' - }, - [FormattingCodes.DarkGreen]: { - foreground: 'rgb(0, 170, 0)', - foregroundDarker: 'rgb(0, 118, 0)', - background: 'rgb(0, 42, 0)', - backgroundDarker: 'rgb(0, 29, 0)', - ansi: '\u001b[0;32m' - }, - [FormattingCodes.DarkAqua]: { - foreground: 'rgb(0, 170, 170)', - foregroundDarker: 'rgb(0, 118, 118)', - background: 'rgb(0, 42, 42)', - backgroundDarker: 'rgb(0, 29, 29)', - ansi: '\u001b[0;36m' - }, - [FormattingCodes.DarkRed]: { - foreground: 'rgb(170, 0, 0)', - foregroundDarker: 'rgb(118, 0, 0)', - background: 'rgb(42, 0, 0)', - backgroundDarker: 'rgb(29, 0, 0)', - ansi: '\u001b[0;31m' - }, - [FormattingCodes.DarkPurple]: { - foreground: 'rgb(170, 0, 170)', - foregroundDarker: 'rgb(118, 0, 118)', - background: 'rgb(42, 0, 42)', - backgroundDarker: 'rgb(29, 0, 29)', - ansi: '\u001b[0;35m' - }, - [FormattingCodes.Gold]: { - foreground: 'rgb(255, 170, 0)', - foregroundDarker: 'rgb(178, 118, 0)', - background: 'rgb(42, 42, 0)', - backgroundDarker: 'rgb(29, 29, 0)', - ansi: '\u001b[0;33m' - }, - [FormattingCodes.Gray]: { - foreground: 'rgb(170, 170, 170)', - foregroundDarker: 'rgb(118, 118, 118)', - background: 'rgb(42, 42, 42)', - backgroundDarker: 'rgb(29, 29, 29)', - ansi: '\u001b[0;37m' - }, - [FormattingCodes.DarkGray]: { - foreground: 'rgb(85, 85, 85)', - foregroundDarker: 'rgb(59, 59, 59)', - background: 'rgb(21, 21, 21)', - backgroundDarker: 'rgb(14, 14, 14)', - ansi: '\u001b[0;90m' - }, - [FormattingCodes.Blue]: { - foreground: 'rgb(85, 85, 255)', - foregroundDarker: 'rgb(59, 59, 178)', - background: 'rgb(21, 21, 63)', - backgroundDarker: 'rgb(14, 14, 44)', - ansi: '\u001b[0;94m' - }, - [FormattingCodes.Green]: { - foreground: 'rgb(85, 255, 85)', - foregroundDarker: 'rgb(59, 178, 59)', - background: 'rgb(21, 63, 21)', - backgroundDarker: 'rgb(14, 44, 14)', - ansi: '\u001b[0;92m' - }, - [FormattingCodes.Aqua]: { - foreground: 'rgb(85, 255, 255)', - foregroundDarker: 'rgb(59, 178, 178)', - background: 'rgb(21, 63, 63)', - backgroundDarker: 'rgb(14, 44, 44)', - ansi: '\u001b[0;96m' - }, - [FormattingCodes.Red]: { - foreground: 'rgb(255, 85, 85)', - foregroundDarker: 'rgb(178, 59, 59)', - background: 'rgb(63, 21, 21)', - backgroundDarker: 'rgb(44, 14, 14)', - ansi: '\u001b[0;91m' - }, - [FormattingCodes.LightPurple]: { - foreground: 'rgb(255, 85, 255)', - foregroundDarker: 'rgb(178, 59, 178)', - background: 'rgb(63, 21, 63)', - backgroundDarker: 'rgb(44, 14, 44)', - ansi: '\u001b[0;95m' - }, - [FormattingCodes.Yellow]: { - foreground: 'rgb(255, 255, 85)', - foregroundDarker: 'rgb(178, 178, 59)', - background: 'rgb(63, 63, 21)', - backgroundDarker: 'rgb(44, 44, 14)', - ansi: '\u001b[0;93m' - }, - [FormattingCodes.White]: { - foreground: 'rgb(255, 255, 255)', - foregroundDarker: 'rgb(178, 178, 178)', - background: 'rgb(63, 63, 63)', - backgroundDarker: 'rgb(44, 44, 44)', - ansi: '\u001b[0;97m' - }, - - [FormattingCodes.Obfuscated]: { ansi: '\u001b[8m' }, - [FormattingCodes.Bold]: { ansi: '\u001b[1m' }, - [FormattingCodes.Strikethrough]: { ansi: '\u001b[9m' }, - [FormattingCodes.Underline]: { ansi: '\u001b[4m' }, - [FormattingCodes.Italic]: { ansi: '\u001b[3m' }, - [FormattingCodes.Reset]: { ansi: '\u001b[0m' } -} as const; - -export type McItemId = Lowercase; -export type SbItemId = Uppercase; -export type MojangJson = string; -export type SbRecipeItem = `${SbItemId}:${number}` | ''; -export type SbRecipe = { - [Location in `${'A' | 'B' | 'C'}${1 | 2 | 3}`]: SbRecipeItem; -}; -export type InfoType = 'WIKI_URL' | ''; - -export type Slayer = `${'WOLF' | 'BLAZE' | 'EMAN'}_${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`; - -export interface RawNeuItem { - itemid: McItemId; - displayname: string; - nbttag: MojangJson; - damage: number; - lore: string[]; - recipe?: SbRecipe; - internalname: SbItemId; - modver: string; - infoType: InfoType; - info?: string[]; - crafttext: string; - vanilla?: boolean; - useneucraft?: boolean; - slayer_req?: Slayer; - clickcommand?: string; - x?: number; - y?: number; - z?: number; - island?: string; - recipes?: { type: string; cost: any[]; result: SbItemId }[]; - /** @deprecated */ - parent?: SbItemId; - noseal?: boolean; -} - -export enum HideFlagsBits { - Enchantments = 1, - AttributeModifiers = 2, - Unbreakable = 4, - CanDestroy = 8, - CanPlaceOn = 16, - /** - * potion effects, shield pattern info, "StoredEnchantments", written book - * "generation" and "author", "Explosion", "Fireworks", and map tooltips - */ - OtherInformation = 32, - Dyed = 64 -} - -export type HideFlagsString = keyof typeof HideFlagsBits; - -export class HideFlags extends BitField { - public static override Flags = HideFlagsBits; -} - -export const formattingCode = new RegExp( - `§[${Object.values(FormattingCodes) - .filter((v) => v.startsWith('§')) - .map((v) => v.substring(1)) - .join('')}]` -); - -export function removeMCFormatting(str: string) { - return str.replaceAll(formattingCode, ''); -} - -const repo = path.join(__dirname, '..', '..', '..', '..', '..', 'neu-item-repo-dangerous'); - -export interface NbtTag { - overrideMeta?: Byte; - Unbreakable?: Int; - ench?: string[]; - HideFlags?: HideFlags; - SkullOwner?: SkullOwner; - display?: NbtTagDisplay; - ExtraAttributes?: ExtraAttributes; -} - -export interface SkullOwner { - Id?: string; - Properties?: { - textures?: { Value?: string }[]; - }; -} - -export interface NbtTagDisplay { - Lore?: string[]; - color?: Int; - Name?: string; -} - -export type RuneId = string; - -export interface ExtraAttributes { - originTag?: Origin; - id?: string; - generator_tier?: Int; - boss_tier?: Int; - enchantments?: { hardened_mana?: Int }; - dungeon_item_level?: Int; - runes?: { [key: RuneId]: Int }; - petInfo?: PetInfo; -} - -export interface PetInfo { - type: 'ZOMBIE'; - active: boolean; - exp: number; - tier: 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'; - hideInfo: boolean; - candyUsed: number; -} - -export type Origin = 'SHOP_PURCHASE'; - -const neuConstantsPath = path.join(repo, 'constants'); -const neuPetsPath = path.join(neuConstantsPath, 'pets.json'); -const neuPets = (await import(neuPetsPath, { assert: { type: 'json' } })) as PetsConstants; -const neuPetNumsPath = path.join(neuConstantsPath, 'petnums.json'); -const neuPetNums = (await import(neuPetNumsPath, { assert: { type: 'json' } })) as PetNums; - -export interface PetsConstants { - pet_rarity_offset: Record; - pet_levels: number[]; - custom_pet_leveling: Record; - pet_types: Record; -} - -export interface PetNums { - [key: string]: { - [key: string]: { - '1': { - otherNums: number[]; - statNums: Record; - }; - '100': { - otherNums: number[]; - statNums: Record; - }; - 'stats_levelling_curve'?: `${number};${number};${number}`; - }; - }; -} - -export class NeuItem { - public itemId: McItemId; - public displayName: string; - public nbtTag: NbtTag; - public internalName: SbItemId; - public lore: string[]; - - public constructor(raw: RawNeuItem) { - this.itemId = raw.itemid; - this.nbtTag = parse(raw.nbttag); - this.displayName = raw.displayname; - this.internalName = raw.internalname; - this.lore = raw.lore; - - this.petLoreReplacements(); - } - - private petLoreReplacements(level = -1) { - if (/.*?;[0-5]$/.test(this.internalName) && this.displayName.includes('LVL')) { - const maxLevel = neuPets?.custom_pet_leveling?.[this.internalName]?.max_level ?? 100; - this.displayName = this.displayName.replace('LVL', `1➡${maxLevel}`); - - const nums = neuPetNums[this.internalName]; - if (!nums) throw new Error(`Pet (${this.internalName}) has no pet nums.`); - - const teir = ['COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY', 'MYTHIC'][+this.internalName.at(-1)!]; - const petInfoTier = nums[teir]; - if (!petInfoTier) throw new Error(`Pet (${this.internalName}) has no pet nums for ${teir} rarity.`); - - const curve = petInfoTier?.stats_levelling_curve?.split(';'); - - // todo: finish copying from neu - - const minStatsLevel = parseInt(curve?.[0] ?? '0'); - const maxStatsLevel = parseInt(curve?.[0] ?? '100'); - - const lore = ''; - } - } -} - -export function mcToAnsi(str: string) { - for (const format in formattingInfo) { - str = str.replaceAll(format, formattingInfo[format as keyof typeof formattingInfo].ansi); - } - return `${str}\u001b[0m`; -} diff --git a/src/lib/common/util/Minecraft_Test.ts b/src/lib/common/util/Minecraft_Test.ts deleted file mode 100644 index 26ca648..0000000 --- a/src/lib/common/util/Minecraft_Test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import fs from 'fs/promises'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { mcToAnsi, RawNeuItem } from './Minecraft.js'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const repo = path.join(__dirname, '..', '..', '..', '..', '..', 'neu-item-repo-dangerous'); -const itemPath = path.join(repo, 'items'); -const items = await fs.readdir(itemPath); - -// for (let i = 0; i < 5; i++) { -for (const path_ of items) { - // const randomItem = items[Math.floor(Math.random() * items.length)]; - // console.log(randomItem); - const item = (await import(path.join(itemPath, /* randomItem */ path_), { assert: { type: 'json' } })).default as RawNeuItem; - if (/.*?((_MONSTER)|(_NPC)|(_ANIMAL)|(_MINIBOSS)|(_BOSS)|(_SC))$/.test(item.internalname)) continue; - if (!/.*?;[0-5]$/.test(item.internalname)) continue; - /* console.log(path_); - console.dir(item, { depth: Infinity }); */ - - /* console.log('==========='); */ - // const nbt = parse(item.nbttag) as NbtTag; - - // if (nbt?.SkullOwner?.Properties?.textures?.[0]?.Value) { - // nbt.SkullOwner.Properties.textures[0].Value = parse( - // Buffer.from(nbt.SkullOwner.Properties.textures[0].Value, 'base64').toString('utf-8') - // ) as string; - // } - - // if (nbt.ExtraAttributes?.petInfo) { - // nbt.ExtraAttributes.petInfo = JSON.parse(nbt.ExtraAttributes.petInfo as any as string); - // } - - // delete nbt.display?.Lore; - - // console.dir(nbt, { depth: Infinity }); - // console.log('==========='); - - /* if (nbt?.display && nbt.display.Name !== item.displayname) - console.log(`${path_} display name mismatch: ${mcToAnsi(nbt.display.Name)} != ${mcToAnsi(item.displayname)}`); - - if (nbt?.ExtraAttributes && nbt?.ExtraAttributes.id !== item.internalname) - console.log(`${path_} internal name mismatch: ${mcToAnsi(nbt?.ExtraAttributes.id)} != ${mcToAnsi(item.internalname)}`); */ - - // console.log('==========='); - - console.log(mcToAnsi(item.displayname)); - console.log(item.lore.map((l) => mcToAnsi(l)).join('\n')); - - /* const keys = [ - 'itemid', - 'displayname', - 'nbttag', - 'damage', - 'lore', - 'recipe', - 'internalname', - 'modver', - 'infoType', - 'info', - 'crafttext', - 'vanilla', - 'useneucraft', - 'slayer_req', - 'clickcommand', - 'x', - 'y', - 'z', - 'island', - 'recipes', - 'parent', - 'noseal' - ]; - - Object.keys(item).forEach((k) => { - if (!keys.includes(k)) throw new Error(`Unknown key: ${k}`); - }); - - if ( - 'slayer_req' in item && - !new Array(10).flatMap((_, i) => ['WOLF', 'BLAZE', 'EMAN'].map((e) => e + (i + 1)).includes(item.slayer_req!)) - ) - throw new Error(`Unknown slayer req: ${item.slayer_req!}`); */ - - /* console.log('=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-'); */ -} diff --git a/src/lib/common/util/Moderation.ts b/src/lib/common/util/Moderation.ts deleted file mode 100644 index 60e32c0..0000000 --- a/src/lib/common/util/Moderation.ts +++ /dev/null @@ -1,556 +0,0 @@ -import { - ActivePunishment, - ActivePunishmentType, - baseMuteResponse, - colors, - emojis, - format, - Guild as GuildDB, - humanizeDuration, - ModLog, - permissionsResponse, - type ModLogType, - type ValueOf -} from '#lib'; -import assert from 'assert/strict'; -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - Client, - EmbedBuilder, - PermissionFlagsBits, - type Guild, - type GuildMember, - type GuildMemberResolvable, - type GuildResolvable, - type Snowflake, - type UserResolvable -} from 'discord.js'; - -enum punishMap { - 'warned' = 'warn', - 'muted' = 'mute', - 'unmuted' = 'unmute', - 'kicked' = 'kick', - 'banned' = 'ban', - 'unbanned' = 'unban', - 'timedout' = 'timeout', - 'untimedout' = 'untimeout', - 'blocked' = 'block', - 'unblocked' = 'unblock' -} -enum reversedPunishMap { - 'warn' = 'warned', - 'mute' = 'muted', - 'unmute' = 'unmuted', - 'kick' = 'kicked', - 'ban' = 'banned', - 'unban' = 'unbanned', - 'timeout' = 'timedout', - 'untimeout' = 'untimedout', - 'block' = 'blocked', - 'unblock' = 'unblocked' -} - -/** - * Checks if a moderator can perform a moderation action on another user. - * @param moderator The person trying to perform the action. - * @param victim The person getting punished. - * @param type The type of punishment - used to format the response. - * @param checkModerator Whether or not to check if the victim is a moderator. - * @param force Override permissions checks. - * @returns `true` if the moderator can perform the action otherwise a reason why they can't. - */ -export async function permissionCheck( - moderator: GuildMember, - victim: GuildMember, - type: - | 'mute' - | 'unmute' - | 'warn' - | 'kick' - | 'ban' - | 'unban' - | 'add a punishment role to' - | 'remove a punishment role from' - | 'block' - | 'unblock' - | 'timeout' - | 'untimeout', - checkModerator = true, - force = false -): Promise { - if (force) return true; - - // If the victim is not in the guild anymore it will be undefined - if ((!victim || !victim.guild) && !['ban', 'unban'].includes(type)) return true; - - if (moderator.guild.id !== victim.guild.id) { - throw new Error('moderator and victim not in same guild'); - } - - const isOwner = moderator.guild.ownerId === moderator.id; - if (moderator.id === victim.id && !type.startsWith('un')) { - return `${emojis.error} You cannot ${type} yourself.`; - } - if ( - moderator.roles.highest.position <= victim.roles.highest.position && - !isOwner && - !(type.startsWith('un') && moderator.id === victim.id) - ) { - return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`; - } - if ( - victim.roles.highest.position >= victim.guild.members.me!.roles.highest.position && - !(type.startsWith('un') && moderator.id === victim.id) - ) { - return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`; - } - if ( - checkModerator && - victim.permissions.has(PermissionFlagsBits.ManageMessages) && - !(type.startsWith('un') && moderator.id === victim.id) - ) { - if (await moderator.guild.hasFeature('modsCanPunishMods')) { - return true; - } else { - return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`; - } - } - return true; -} - -/** - * Performs permission checks that are required in order to (un)mute a member. - * @param guild The guild to check the mute permissions in. - * @returns A {@link MuteResponse} or true if nothing failed. - */ -export async function checkMutePermissions( - guild: Guild -): Promise | ValueOf | true> { - if (!guild.members.me!.permissions.has('ManageRoles')) return permissionsResponse.MISSING_PERMISSIONS; - const muteRoleID = await guild.getSetting('muteRole'); - if (!muteRoleID) return baseMuteResponse.NO_MUTE_ROLE; - const muteRole = guild.roles.cache.get(muteRoleID); - if (!muteRole) return baseMuteResponse.MUTE_ROLE_INVALID; - if (muteRole.position >= guild.members.me!.roles.highest.position || muteRole.managed) - return baseMuteResponse.MUTE_ROLE_NOT_MANAGEABLE; - - return true; -} - -/** - * Creates a modlog entry for a punishment. - * @param options Options for creating a modlog entry. - * @param getCaseNumber Whether or not to get the case number of the entry. - * @returns An object with the modlog and the case number. - */ -export async function createModLogEntry( - options: CreateModLogEntryOptions, - getCaseNumber = false -): Promise<{ log: ModLog | null; caseNum: number | null }> { - const user = (await options.client.utils.resolveNonCachedUser(options.user))!.id; - const moderator = (await options.client.utils.resolveNonCachedUser(options.moderator))!.id; - const guild = options.client.guilds.resolveId(options.guild)!; - - return createModLogEntrySimple( - { - ...options, - user: user, - moderator: moderator, - guild: guild - }, - getCaseNumber - ); -} - -/** - * Creates a modlog entry with already resolved ids. - * @param options Options for creating a modlog entry. - * @param getCaseNumber Whether or not to get the case number of the entry. - * @returns An object with the modlog and the case number. - */ -export async function createModLogEntrySimple( - options: SimpleCreateModLogEntryOptions, - getCaseNumber = false -): Promise<{ log: ModLog | null; caseNum: number | null }> { - // If guild does not exist create it so the modlog can reference a guild. - await GuildDB.findOrCreate({ - where: { id: options.guild }, - defaults: { id: options.guild } - }); - - const modLogEntry = ModLog.build({ - type: options.type, - user: options.user, - moderator: options.moderator, - reason: options.reason, - duration: options.duration ? options.duration : undefined, - guild: options.guild, - pseudo: options.pseudo ?? false, - evidence: options.evidence, - hidden: options.hidden ?? false - }); - const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => { - await options.client.utils.handleError('createModLogEntry', e); - return null; - }); - - if (!getCaseNumber) return { log: saveResult, caseNum: null }; - - const caseNum = ( - await ModLog.findAll({ where: { type: options.type, user: options.user, guild: options.guild, hidden: false } }) - )?.length; - return { log: saveResult, caseNum }; -} - -/** - * Creates a punishment entry. - * @param options Options for creating the punishment entry. - * @returns The database entry, or null if no entry is created. - */ -export async function createPunishmentEntry(options: CreatePunishmentEntryOptions): Promise { - const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined; - const user = (await options.client.utils.resolveNonCachedUser(options.user))!.id; - const guild = options.client.guilds.resolveId(options.guild)!; - const type = findTypeEnum(options.type)!; - - const entry = ActivePunishment.build( - options.extraInfo - ? { user, type, guild, expires, modlog: options.modlog, extraInfo: options.extraInfo } - : { user, type, guild, expires, modlog: options.modlog } - ); - return await entry.save().catch(async (e) => { - await options.client.utils.handleError('createPunishmentEntry', e); - return null; - }); -} - -/** - * Destroys a punishment entry. - * @param options Options for destroying the punishment entry. - * @returns Whether or not the entry was destroyed. - */ -export async function removePunishmentEntry(options: RemovePunishmentEntryOptions): Promise { - const user = await options.client.utils.resolveNonCachedUser(options.user); - const guild = options.client.guilds.resolveId(options.guild); - const type = findTypeEnum(options.type); - - if (!user || !guild) return false; - - let success = true; - - const entries = await ActivePunishment.findAll({ - // finding all cases of a certain type incase there were duplicates or something - where: options.extraInfo - ? { user: user.id, guild: guild, type, extraInfo: options.extraInfo } - : { user: user.id, guild: guild, type } - }).catch(async (e) => { - await options.client.utils.handleError('removePunishmentEntry', e); - success = false; - }); - if (entries) { - const promises = entries.map(async (entry) => - entry.destroy().catch(async (e) => { - await options.client.utils.handleError('removePunishmentEntry', e); - success = false; - }) - ); - - await Promise.all(promises); - } - return success; -} - -/** - * Returns the punishment type enum for the given type. - * @param type The type of the punishment. - * @returns The punishment type enum. - */ -function findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') { - const typeMap = { - ['mute']: ActivePunishmentType.MUTE, - ['ban']: ActivePunishmentType.BAN, - ['role']: ActivePunishmentType.ROLE, - ['block']: ActivePunishmentType.BLOCK - }; - return typeMap[type]; -} - -export function punishmentToPresentTense(punishment: PunishmentTypeDM): PunishmentTypePresent { - return punishMap[punishment]; -} - -export function punishmentToPastTense(punishment: PunishmentTypePresent): PunishmentTypeDM { - return reversedPunishMap[punishment]; -} - -/** - * Notifies the specified user of their punishment. - * @param options Options for notifying the user. - * @returns Whether or not the dm was successfully sent. - */ -export async function punishDM(options: PunishDMOptions): Promise { - const ending = await options.guild.getSetting('punishmentEnding'); - const dmEmbed = - ending && ending.length && options.sendFooter - ? new EmbedBuilder().setDescription(ending).setColor(colors.newBlurple) - : undefined; - - const appealsEnabled = !!( - (await options.guild.hasFeature('punishmentAppeals')) && (await options.guild.getLogChannel('appeals')) - ); - - let content = `You have been ${options.punishment} `; - if (options.punishment.includes('blocked')) { - assert(options.channel); - content += `from <#${options.channel}> `; - } - content += `in ${format.input(options.guild.name)} `; - if (options.duration !== null && options.duration !== undefined) - content += options.duration ? `for ${humanizeDuration(options.duration)} ` : 'permanently '; - const reason = options.reason?.trim() ? options.reason?.trim() : 'No reason provided'; - content += `for ${format.input(reason)}.`; - - let components; - if (appealsEnabled && options.modlog) - components = [ - new ActionRowBuilder({ - components: [ - new ButtonBuilder({ - customId: `appeal;${punishmentToPresentTense(options.punishment)};${ - options.guild.id - };${options.client.users.resolveId(options.user)};${options.modlog}`, - style: ButtonStyle.Primary, - label: 'Appeal' - }).toJSON() - ] - }) - ]; - - const dmSuccess = await options.client.users - .send(options.user, { - content, - embeds: dmEmbed ? [dmEmbed] : undefined, - components - }) - .catch(() => false); - return !!dmSuccess; -} - -interface BaseCreateModLogEntryOptions extends BaseOptions { - /** - * The type of modlog entry. - */ - type: ModLogType; - - /** - * The reason for the punishment. - */ - reason: string | undefined | null; - - /** - * The duration of the punishment. - */ - duration?: number; - - /** - * Whether the punishment is a pseudo punishment. - */ - pseudo?: boolean; - - /** - * The evidence for the punishment. - */ - evidence?: string; - - /** - * Makes the modlog entry hidden. - */ - hidden?: boolean; -} - -/** - * Options for creating a modlog entry. - */ -export interface CreateModLogEntryOptions extends BaseCreateModLogEntryOptions { - /** - * The client. - */ - client: Client; - - /** - * The user that a modlog entry is created for. - */ - user: GuildMemberResolvable; - - /** - * The moderator that created the modlog entry. - */ - moderator: GuildMemberResolvable; - - /** - * The guild that the punishment is created for. - */ - guild: GuildResolvable; -} - -/** - * Simple options for creating a modlog entry. - */ -export interface SimpleCreateModLogEntryOptions extends BaseCreateModLogEntryOptions { - /** - * The user that a modlog entry is created for. - */ - user: Snowflake; - - /** - * The moderator that created the modlog entry. - */ - moderator: Snowflake; - - /** - * The guild that the punishment is created for. - */ - guild: Snowflake; -} - -/** - * Options for creating a punishment entry. - */ -export interface CreatePunishmentEntryOptions extends BaseOptions { - /** - * The type of punishment. - */ - type: 'mute' | 'ban' | 'role' | 'block'; - - /** - * The user that the punishment is created for. - */ - user: GuildMemberResolvable; - - /** - * The length of time the punishment lasts for. - */ - duration: number | undefined; - - /** - * The guild that the punishment is created for. - */ - guild: GuildResolvable; - - /** - * The id of the modlog that is linked to the punishment entry. - */ - modlog: string; - - /** - * Extra information for the punishment. The role for role punishments and the channel for blocks. - */ - extraInfo?: Snowflake; -} - -/** - * Options for removing a punishment entry. - */ -export interface RemovePunishmentEntryOptions extends BaseOptions { - /** - * The type of punishment. - */ - type: 'mute' | 'ban' | 'role' | 'block'; - - /** - * The user that the punishment is destroyed for. - */ - user: GuildMemberResolvable; - - /** - * The guild that the punishment was in. - */ - guild: GuildResolvable; - - /** - * Extra information for the punishment. The role for role punishments and the channel for blocks. - */ - extraInfo?: Snowflake; -} - -/** - * Options for sending a user a punishment dm. - */ -export interface PunishDMOptions extends BaseOptions { - /** - * The modlog case id so the user can make an appeal. - */ - modlog?: string; - - /** - * The guild that the punishment is taking place in. - */ - guild: Guild; - - /** - * The user that is being punished. - */ - user: UserResolvable; - - /** - * The punishment that the user has received. - */ - punishment: PunishmentTypeDM; - - /** - * The reason the user's punishment. - */ - reason?: string; - - /** - * The duration of the punishment. - */ - duration?: number; - - /** - * Whether or not to send the guild's punishment footer with the dm. - * @default true - */ - sendFooter: boolean; - - /** - * The channel that the user was (un)blocked from. - */ - channel?: Snowflake; -} - -interface BaseOptions { - /** - * The client. - */ - client: Client; -} - -export type PunishmentTypeDM = - | 'warned' - | 'muted' - | 'unmuted' - | 'kicked' - | 'banned' - | 'unbanned' - | 'timedout' - | 'untimedout' - | 'blocked' - | 'unblocked'; - -export type PunishmentTypePresent = - | 'warn' - | 'mute' - | 'unmute' - | 'kick' - | 'ban' - | 'unban' - | 'timeout' - | 'untimeout' - | 'block' - | 'unblock'; - -export type AppealButtonId = `appeal;${PunishmentTypePresent};${Snowflake};${Snowflake};${string}`; diff --git a/src/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts b/src/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts deleted file mode 100644 index def7ad6..0000000 --- a/src/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { type CommandMessage } from '#lib'; - -export type BushArgumentTypeCaster = (message: CommandMessage, phrase: string) => R; diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts deleted file mode 100644 index 9ca02a2..0000000 --- a/src/lib/extensions/discord-akairo/BushClient.ts +++ /dev/null @@ -1,586 +0,0 @@ -import { - abbreviatedNumber, - contentWithDuration, - discordEmoji, - duration, - durationSeconds, - globalUser, - messageLink, - permission, - roleWithDuration, - snowflake -} from '#args'; -import { BushClientEvents, emojis, formatError, inspect } from '#lib'; -import { patch, type PatchedElements } from '@notenoughupdates/events-intercept'; -import * as Sentry from '@sentry/node'; -import { - AkairoClient, - ArgumentTypeCaster, - ContextMenuCommandHandler, - version as akairoVersion, - type ArgumentPromptData, - type OtherwiseContentSupplier -} from 'discord-akairo'; -import { - ActivityType, - GatewayIntentBits, - MessagePayload, - Options, - Partials, - Structures, - version as discordJsVersion, - type Awaitable, - type If, - type InteractionReplyOptions, - type Message, - type MessageEditOptions, - type MessageOptions, - type ReplyMessageOptions, - type Snowflake, - type UserResolvable, - type WebhookEditMessageOptions -} from 'discord.js'; -import type EventEmitter from 'events'; -import { google } from 'googleapis'; -import path from 'path'; -import readline from 'readline'; -import type { Options as SequelizeOptions, Sequelize as SequelizeType } from 'sequelize'; -import { fileURLToPath } from 'url'; -import type { Config } from '../../../../config/Config.js'; -import { tinyColor } from '../../../arguments/tinyColor.js'; -import UpdateCacheTask from '../../../tasks/cache/updateCache.js'; -import UpdateStatsTask from '../../../tasks/feature/updateStats.js'; -import { HighlightManager } from '../../common/HighlightManager.js'; -import { ActivePunishment } from '../../models/instance/ActivePunishment.js'; -import { Guild as GuildDB } from '../../models/instance/Guild.js'; -import { Highlight } from '../../models/instance/Highlight.js'; -import { Level } from '../../models/instance/Level.js'; -import { ModLog } from '../../models/instance/ModLog.js'; -import { Reminder } from '../../models/instance/Reminder.js'; -import { StickyRole } from '../../models/instance/StickyRole.js'; -import { Global } from '../../models/shared/Global.js'; -import { GuildCount } from '../../models/shared/GuildCount.js'; -import { MemberCount } from '../../models/shared/MemberCount.js'; -import { Shared } from '../../models/shared/Shared.js'; -import { Stat } from '../../models/shared/Stat.js'; -import { AllowedMentions } from '../../utils/AllowedMentions.js'; -import { BushCache } from '../../utils/BushCache.js'; -import { BushClientUtils } from '../../utils/BushClientUtils.js'; -import { BushLogger } from '../../utils/BushLogger.js'; -import { ExtendedGuild } from '../discord.js/ExtendedGuild.js'; -import { ExtendedGuildMember } from '../discord.js/ExtendedGuildMember.js'; -import { ExtendedMessage } from '../discord.js/ExtendedMessage.js'; -import { ExtendedUser } from '../discord.js/ExtendedUser.js'; -import { BushCommandHandler } from './BushCommandHandler.js'; -import { BushInhibitorHandler } from './BushInhibitorHandler.js'; -import { BushListenerHandler } from './BushListenerHandler.js'; -import { BushTaskHandler } from './BushTaskHandler.js'; -const { Sequelize } = (await import('sequelize')).default; - -declare module 'discord.js' { - export interface Client extends EventEmitter { - /** The ID of the owner(s). */ - ownerID: Snowflake | Snowflake[]; - /** The ID of the superUser(s). */ - superUserID: Snowflake | Snowflake[]; - /** Whether or not the client is ready. */ - customReady: boolean; - /** The configuration for the client. */ - readonly config: Config; - /** Stats for the client. */ - readonly stats: BushStats; - /** The handler for the bot's listeners. */ - readonly listenerHandler: BushListenerHandler; - /** The handler for the bot's command inhibitors. */ - readonly inhibitorHandler: BushInhibitorHandler; - /** The handler for the bot's commands. */ - readonly commandHandler: BushCommandHandler; - /** The handler for the bot's tasks. */ - readonly taskHandler: BushTaskHandler; - /** The handler for the bot's context menu commands. */ - readonly contextMenuCommandHandler: ContextMenuCommandHandler; - /** The database connection for this instance of the bot (production, beta, or development). */ - readonly instanceDB: SequelizeType; - /** The database connection that is shared between all instances of the bot. */ - readonly sharedDB: SequelizeType; - /** A custom logging system for the bot. */ - readonly logger: BushLogger; - /** Cached global and guild database data. */ - readonly cache: BushCache; - /** Sentry error reporting for the bot. */ - readonly sentry: typeof Sentry; - /** Manages most aspects of the highlight command */ - readonly highlightManager: HighlightManager; - /** The perspective api */ - perspective: any; - /** Client utilities. */ - readonly utils: BushClientUtils; - /** A custom logging system for the bot. */ - get console(): BushLogger; - on(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; - once(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; - emit(event: K, ...args: BushClientEvents[K]): boolean; - off(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; - removeAllListeners(event?: K): this; - /** - * Checks if a user is the owner of this bot. - * @param user - User to check. - */ - isOwner(user: UserResolvable): boolean; - /** - * Checks if a user is a super user of this bot. - * @param user - User to check. - */ - isSuperUser(user: UserResolvable): boolean; - } -} - -export type ReplyMessageType = string | MessagePayload | ReplyMessageOptions; -export type EditMessageType = string | MessageEditOptions | MessagePayload; -export type SlashSendMessageType = string | MessagePayload | InteractionReplyOptions; -export type SlashEditMessageType = string | MessagePayload | WebhookEditMessageOptions; -export type SendMessageType = string | MessagePayload | MessageOptions; - -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: false -}); - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -/** - * The main hub for interacting with the Discord API. - */ -export class BushClient extends AkairoClient { - public declare ownerID: Snowflake[]; - public declare superUserID: Snowflake[]; - - /** - * Whether or not the client is ready. - */ - public override customReady = false; - - /** - * Stats for the client. - */ - public override readonly stats: BushStats = { cpu: undefined, commandsUsed: 0n, slashCommandsUsed: 0n }; - - /** - * The handler for the bot's listeners. - */ - public override readonly listenerHandler: BushListenerHandler; - - /** - * The handler for the bot's command inhibitors. - */ - public override readonly inhibitorHandler: BushInhibitorHandler; - - /** - * The handler for the bot's commands. - */ - public override readonly commandHandler: BushCommandHandler; - - /** - * The handler for the bot's tasks. - */ - public override readonly taskHandler: BushTaskHandler; - - /** - * The handler for the bot's context menu commands. - */ - public override readonly contextMenuCommandHandler: ContextMenuCommandHandler; - - /** - * The database connection for this instance of the bot (production, beta, or development). - */ - public override readonly instanceDB: SequelizeType; - - /** - * The database connection that is shared between all instances of the bot. - */ - public override readonly sharedDB: SequelizeType; - - /** - * A custom logging system for the bot. - */ - public override readonly logger: BushLogger = new BushLogger(this); - - /** - * Cached global and guild database data. - */ - public override readonly cache = new BushCache(); - - /** - * Sentry error reporting for the bot. - */ - public override readonly sentry!: typeof Sentry; - - /** - * Manages most aspects of the highlight command - */ - public override readonly highlightManager: HighlightManager = new HighlightManager(this); - - /** - * The perspective api - */ - public override perspective: any; - - /** - * Client utilities. - */ - public override readonly utils: BushClientUtils = new BushClientUtils(this); - - /** - * @param config The configuration for the client. - */ - public constructor( - /** - * The configuration for the client. - */ - public override readonly config: Config - ) { - super({ - ownerID: config.owners, - intents: Object.keys(GatewayIntentBits) - .map((i) => (typeof i === 'string' ? GatewayIntentBits[i as keyof typeof GatewayIntentBits] : i)) - .reduce((acc, p) => acc | p, 0), - partials: Object.keys(Partials).map((p) => Partials[p as keyof typeof Partials]), - presence: { - activities: [{ name: 'Beep Boop', type: ActivityType.Watching }], - status: 'online' - }, - allowedMentions: AllowedMentions.none(), // no mentions by default - makeCache: Options.cacheWithLimits({}), - failIfNotExists: false, - rest: { api: 'https://canary.discord.com/api' } - }); - patch(this); - - this.token = config.token as If; - - /* =-=-= handlers =-=-= */ - this.listenerHandler = new BushListenerHandler(this, { - directory: path.join(__dirname, '..', '..', '..', 'listeners'), - automateCategories: true - }); - this.inhibitorHandler = new BushInhibitorHandler(this, { - directory: path.join(__dirname, '..', '..', '..', 'inhibitors'), - automateCategories: true - }); - this.taskHandler = new BushTaskHandler(this, { - directory: path.join(__dirname, '..', '..', '..', 'tasks'), - automateCategories: true - }); - - const modify = async ( - message: Message, - text: string | MessagePayload | MessageOptions | OtherwiseContentSupplier, - data: ArgumentPromptData, - replaceError: boolean - ) => { - const ending = '\n\n Type **cancel** to cancel the command'; - const options = typeof text === 'function' ? await text(message, data) : text; - const search = '{error}', - replace = emojis.error; - - if (typeof options === 'string') return (replaceError ? options.replace(search, replace) : options) + ending; - - if (options instanceof MessagePayload) { - if (options.options.content) { - if (replaceError) options.options.content = options.options.content.replace(search, replace); - options.options.content += ending; - } - } else if (options.content) { - if (replaceError) options.content = options.content.replace(search, replace); - options.content += ending; - } - return options; - }; - - this.commandHandler = new BushCommandHandler(this, { - directory: path.join(__dirname, '..', '..', '..', 'commands'), - prefix: async ({ guild }: Message) => { - if (this.config.isDevelopment) return 'dev '; - if (!guild) return this.config.prefix; - const prefix = await guild.getSetting('prefix'); - return (prefix ?? this.config.prefix) as string; - }, - allowMention: true, - handleEdits: true, - commandUtil: true, - commandUtilLifetime: 300_000, // 5 minutes - argumentDefaults: { - prompt: { - start: 'Placeholder argument prompt. **If you see this please tell my developers**.', - retry: 'Placeholder failed argument prompt. **If you see this please tell my developers**.', - modifyStart: (message, text, data) => modify(message, text, data, false), - modifyRetry: (message, text, data) => modify(message, text, data, true), - timeout: ':hourglass: You took too long the command has been cancelled.', - ended: 'You exceeded the maximum amount of tries the command has been cancelled', - cancel: 'The command has been cancelled', - retries: 3, - time: 3e4 - }, - otherwise: '' - }, - automateCategories: false, - autoRegisterSlashCommands: true, - skipBuiltInPostInhibitors: true, - aliasReplacement: /-/g - }); - this.contextMenuCommandHandler = new ContextMenuCommandHandler(this, { - directory: path.join(__dirname, '..', '..', '..', 'context-menu-commands'), - automateCategories: true - }); - - /* =-=-= databases =-=-= */ - const sharedDBOptions: SequelizeOptions = { - username: this.config.db.username, - password: this.config.db.password, - dialect: 'postgres', - host: this.config.db.host, - port: this.config.db.port, - logging: this.config.logging.db ? (sql) => this.logger.debug(sql) : false, - timezone: 'America/New_York' - }; - this.instanceDB = new Sequelize({ - ...sharedDBOptions, - database: this.config.isDevelopment ? 'bushbot-dev' : this.config.isBeta ? 'bushbot-beta' : 'bushbot' - }); - this.sharedDB = new Sequelize({ - ...sharedDBOptions, - database: 'bushbot-shared' - }); - - this.sentry = Sentry; - } - - /** - * A custom logging system for the bot. - */ - public override get console(): BushLogger { - return this.logger; - } - - /** - * Extends discord.js structures before the client is instantiated. - */ - public static extendStructures(): void { - Structures.extend('GuildMember', () => ExtendedGuildMember); - Structures.extend('Guild', () => ExtendedGuild); - Structures.extend('Message', () => ExtendedMessage); - Structures.extend('User', () => ExtendedUser); - } - - /** - * Initializes the bot. - */ - public async init() { - if (parseInt(process.versions.node.split('.')[0]) < 17) { - void (await this.console.error('version', `Please use node <>, not <<${process.version}>>.`, false)); - process.exit(2); - } - - this.setMaxListeners(20); - - this.perspective = await google.discoverAPI('https://commentanalyzer.googleapis.com/$discovery/rest?version=v1alpha1'); - - this.commandHandler.useInhibitorHandler(this.inhibitorHandler); - this.commandHandler.useListenerHandler(this.listenerHandler); - this.commandHandler.useTaskHandler(this.taskHandler); - this.commandHandler.useContextMenuCommandHandler(this.contextMenuCommandHandler); - this.commandHandler.ignorePermissions = this.config.owners; - this.commandHandler.ignoreCooldown = [...new Set([...this.config.owners, ...this.cache.shared.superUsers])]; - const emitters: Emitters = { - client: this, - commandHandler: this.commandHandler, - inhibitorHandler: this.inhibitorHandler, - listenerHandler: this.listenerHandler, - taskHandler: this.taskHandler, - contextMenuCommandHandler: this.contextMenuCommandHandler, - process, - stdin: rl, - gateway: this.ws, - rest: this.rest, - ws: this.ws - }; - this.listenerHandler.setEmitters(emitters); - this.commandHandler.resolver.addTypes({ - duration: duration, - contentWithDuration: contentWithDuration, - permission: permission, - snowflake: snowflake, - discordEmoji: discordEmoji, - roleWithDuration: roleWithDuration, - abbreviatedNumber: abbreviatedNumber, - durationSeconds: durationSeconds, - globalUser: globalUser, - messageLink: messageLink, - tinyColor: tinyColor - }); - - this.sentry.setTag('process', process.pid.toString()); - this.sentry.setTag('discord.js', discordJsVersion); - this.sentry.setTag('discord-akairo', akairoVersion); - void this.logger.success('startup', `Successfully connected to <>.`, false); - - // loads all the handlers - const handlers = { - commands: this.commandHandler, - contextMenuCommands: this.contextMenuCommandHandler, - listeners: this.listenerHandler, - inhibitors: this.inhibitorHandler, - tasks: this.taskHandler - }; - const handlerPromises = Object.entries(handlers).map(([handlerName, handler]) => - handler - .loadAll() - .then(() => { - void this.logger.success('startup', `Successfully loaded <<${handlerName}>>.`, false); - }) - .catch((e) => { - void this.logger.error('startup', `Unable to load loader <<${handlerName}>> with error:\n${formatError(e)}`, false); - if (process.argv.includes('dry')) process.exit(1); - }) - ); - await Promise.allSettled(handlerPromises); - } - - /** - * Connects to the database, initializes models, and creates tables if they do not exist. - */ - public async dbPreInit() { - try { - await this.instanceDB.authenticate(); - GuildDB.initModel(this.instanceDB, this); - ModLog.initModel(this.instanceDB); - ActivePunishment.initModel(this.instanceDB); - Level.initModel(this.instanceDB); - StickyRole.initModel(this.instanceDB); - Reminder.initModel(this.instanceDB); - Highlight.initModel(this.instanceDB); - await this.instanceDB.sync({ alter: true }); // Sync all tables to fix everything if updated - await this.console.success('startup', `Successfully connected to <>.`, false); - } catch (e) { - await this.console.error( - 'startup', - `Failed to connect to <> with error:\n${inspect(e, { colors: true, depth: 1 })}`, - false - ); - process.exit(2); - } - try { - await this.sharedDB.authenticate(); - Stat.initModel(this.sharedDB); - Global.initModel(this.sharedDB); - Shared.initModel(this.sharedDB); - MemberCount.initModel(this.sharedDB); - GuildCount.initModel(this.sharedDB); - await this.sharedDB.sync({ - // Sync all tables to fix everything if updated - // if another instance restarts we don't want to overwrite new changes made in development - alter: this.config.isDevelopment - }); - await this.console.success('startup', `Successfully connected to <>.`, false); - } catch (e) { - await this.console.error( - 'startup', - `Failed to connect to <> with error:\n${inspect(e, { colors: true, depth: 1 })}`, - false - ); - process.exit(2); - } - } - - /** - * Starts the bot - */ - public async start() { - this.intercept('ready', async (arg, done) => { - const promises = this.guilds.cache - .filter((g) => g.large) - .map((guild) => { - return guild.members.fetch(); - }); - await Promise.all(promises); - this.customReady = true; - this.taskHandler.startAll(); - return done(null, `intercepted ${arg}`); - }); - - try { - await this.highlightManager.syncCache(); - await UpdateCacheTask.init(this); - void this.console.success('startup', `Successfully created <>.`, false); - const stats = await UpdateStatsTask.init(this); - this.stats.commandsUsed = stats.commandsUsed; - this.stats.slashCommandsUsed = stats.slashCommandsUsed; - await this.login(this.token!); - } catch (e) { - await this.console.error('start', inspect(e, { colors: true, depth: 1 }), false); - process.exit(1); - } - } - - /** - * Logs out, terminates the connection to Discord, and destroys the client. - */ - public override destroy(relogin = false): void | Promise { - super.destroy(); - if (relogin) { - return this.login(this.token!); - } - } - - public override isOwner(user: UserResolvable): boolean { - return this.config.owners.includes(this.users.resolveId(user!)!); - } - - public override isSuperUser(user: UserResolvable): boolean { - const userID = this.users.resolveId(user)!; - return this.cache.shared.superUsers.includes(userID) || this.config.owners.includes(userID); - } -} - -export interface BushClient extends EventEmitter, PatchedElements, AkairoClient { - on(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; - once(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; - emit(event: K, ...args: BushClientEvents[K]): boolean; - off(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; - removeAllListeners(event?: K): this; -} - -/** - * Various statistics - */ -export interface BushStats { - /** - * The average cpu usage of the bot from the past 60 seconds. - */ - cpu: number | undefined; - - /** - * The total number of times any command has been used. - */ - commandsUsed: bigint; - - /** - * The total number of times any slash command has been used. - */ - slashCommandsUsed: bigint; -} - -export interface Emitters { - client: BushClient; - commandHandler: BushClient['commandHandler']; - inhibitorHandler: BushClient['inhibitorHandler']; - listenerHandler: BushClient['listenerHandler']; - taskHandler: BushClient['taskHandler']; - contextMenuCommandHandler: BushClient['contextMenuCommandHandler']; - process: NodeJS.Process; - stdin: readline.Interface; - gateway: BushClient['ws']; - rest: BushClient['rest']; - ws: BushClient['ws']; -} diff --git a/src/lib/extensions/discord-akairo/BushCommand.ts b/src/lib/extensions/discord-akairo/BushCommand.ts deleted file mode 100644 index dc2295f..0000000 --- a/src/lib/extensions/discord-akairo/BushCommand.ts +++ /dev/null @@ -1,586 +0,0 @@ -import { type DiscordEmojiInfo, type RoleWithDuration } from '#args'; -import { - type BushArgumentTypeCaster, - type BushClient, - type BushCommandHandler, - type BushInhibitor, - type BushListener, - type BushTask, - type ParsedDuration -} from '#lib'; -import { - ArgumentMatch, - Command, - CommandUtil, - type AkairoApplicationCommandAutocompleteOption, - type AkairoApplicationCommandChannelOptionData, - type AkairoApplicationCommandChoicesData, - type AkairoApplicationCommandNonOptionsData, - type AkairoApplicationCommandNumericOptionData, - type AkairoApplicationCommandOptionData, - type AkairoApplicationCommandSubCommandData, - type AkairoApplicationCommandSubGroupData, - type ArgumentOptions, - type ArgumentType, - type ArgumentTypeCaster, - type BaseArgumentType, - type CommandOptions, - type ContextMenuCommand, - type MissingPermissionSupplier, - type SlashOption, - type SlashResolveType -} from 'discord-akairo'; -import { - Message, - User, - type ApplicationCommandOptionChoiceData, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type ApplicationCommandOptionType, - type PermissionResolvable, - type PermissionsString, - type Snowflake -} from 'discord.js'; -import _ from 'lodash'; -import { SlashMessage } from './SlashMessage.js'; - -export interface OverriddenBaseArgumentType extends BaseArgumentType { - commandAlias: BushCommand | null; - command: BushCommand | null; - inhibitor: BushInhibitor | null; - listener: BushListener | null; - task: BushTask | null; - contextMenuCommand: ContextMenuCommand | null; -} - -export interface BaseBushArgumentType extends OverriddenBaseArgumentType { - duration: number | null; - contentWithDuration: ParsedDuration; - permission: PermissionsString | null; - snowflake: Snowflake | null; - discordEmoji: DiscordEmojiInfo | null; - roleWithDuration: RoleWithDuration | null; - abbreviatedNumber: number | null; - globalUser: User | null; - messageLink: Message | null; - durationSeconds: number | null; - tinyColor: string | null; -} - -export type BushArgumentType = keyof BaseBushArgumentType | RegExp; - -interface BaseBushArgumentOptions extends Omit, ExtraArgumentOptions { - id: string; - description: string; - - /** - * The message sent for the prompt and the slash command description. - */ - prompt?: string; - - /** - * The message set for the retry prompt. - */ - retry?: string; - - /** - * Whether or not the argument is optional. - */ - optional?: boolean; - - /** - * The type used for slash commands. Set to false to disable this argument for slash commands. - */ - slashType: AkairoApplicationCommandOptionData['type'] | false; - - /** - * Allows you to get a discord resolved object - * - * ex. get the resolved member object when the type is {@link ApplicationCommandOptionType.User User} - */ - slashResolve?: SlashResolveType; - - /** - * The choices of the option for the user to pick from - */ - choices?: ApplicationCommandOptionChoiceData[]; - - /** - * Whether the option is an autocomplete option - */ - autocomplete?: boolean; - - /** - * When the option type is channel, the allowed types of channels that can be selected - */ - channelTypes?: AkairoApplicationCommandChannelOptionData['channelTypes']; - - /** - * The minimum value for an {@link ApplicationCommandOptionType.Integer Integer} or {@link ApplicationCommandOptionType.Number Number} option - */ - minValue?: number; - - /** - * The maximum value for an {@link ApplicationCommandOptionType.Integer Integer} or {@link ApplicationCommandOptionType.Number Number} option - */ - maxValue?: number; -} - -interface ExtraArgumentOptions { - /** - * Restrict this argument to only slash or only text commands. - */ - only?: 'slash' | 'text'; - - /** - * Readable type for the help command. - */ - readableType?: string; - - /** - * Whether the argument is only accessible to the owners. - * @default false - */ - ownerOnly?: boolean; - - /** - * Whether the argument is only accessible to the super users. - * @default false - */ - superUserOnly?: boolean; -} - -export interface BushArgumentOptions extends BaseBushArgumentOptions { - /** - * The type that the argument should be cast to. - * - `string` does not cast to any type. - * - `lowercase` makes the input lowercase. - * - `uppercase` makes the input uppercase. - * - `charCodes` transforms the input to an array of char codes. - * - `number` casts to a number. - * - `integer` casts to an integer. - * - `bigint` casts to a big integer. - * - `url` casts to an `URL` object. - * - `date` casts to a `Date` object. - * - `color` casts a hex code to an integer. - * - `commandAlias` tries to resolve to a command from an alias. - * - `command` matches the ID of a command. - * - `inhibitor` matches the ID of an inhibitor. - * - `listener` matches the ID of a listener. - * - * Possible Discord-related types. - * These types can be plural (add an 's' to the end) and a collection of matching objects will be used. - * - `user` tries to resolve to a user. - * - `member` tries to resolve to a member. - * - `relevant` tries to resolve to a relevant user, works in both guilds and DMs. - * - `channel` tries to resolve to a channel. - * - `textChannel` tries to resolve to a text channel. - * - `voiceChannel` tries to resolve to a voice channel. - * - `stageChannel` tries to resolve to a stage channel. - * - `threadChannel` tries to resolve a thread channel. - * - `role` tries to resolve to a role. - * - `emoji` tries to resolve to a custom emoji. - * - `guild` tries to resolve to a guild. - * - `permission` tries to resolve to a permissions. - * - * Other Discord-related types: - * - `message` tries to fetch a message from an ID within the channel. - * - `guildMessage` tries to fetch a message from an ID within the guild. - * - `relevantMessage` is a combination of the above, works in both guilds and DMs. - * - `invite` tries to fetch an invite object from a link. - * - `userMention` matches a mention of a user. - * - `memberMention` matches a mention of a guild member. - * - `channelMention` matches a mention of a channel. - * - `roleMention` matches a mention of a role. - * - `emojiMention` matches a mention of an emoji. - * - * Misc: - * - `duration` tries to parse duration in milliseconds - * - `contentWithDuration` tries to parse duration in milliseconds and returns the remaining content with the duration - * removed - */ - type?: BushArgumentType | (keyof BaseBushArgumentType)[] | BushArgumentTypeCaster; -} - -export interface CustomBushArgumentOptions extends BaseBushArgumentOptions { - /** - * An array of strings can be used to restrict input to only those strings, case insensitive. - * The array can also contain an inner array of strings, for aliases. - * If so, the first entry of the array will be used as the final argument. - * - * A regular expression can also be used. - * The evaluated argument will be an object containing the `match` and `matches` if global. - */ - customType?: (string | string[])[] | RegExp | string | null; -} - -export type BushMissingPermissionSupplier = (message: CommandMessage | SlashMessage) => Promise | any; - -interface ExtendedCommandOptions { - /** - * Whether the command is hidden from the help command. - */ - hidden?: boolean; - - /** - * The channels the command is limited to run in. - */ - restrictedChannels?: Snowflake[]; - - /** - * The guilds the command is limited to run in. - */ - restrictedGuilds?: Snowflake[]; - - /** - * Show how to use the command. - */ - usage: string[]; - - /** - * Examples for how to use the command. - */ - examples: string[]; - - /** - * A fake command, completely hidden from the help command. - */ - pseudo?: boolean; - - /** - * Allow this command to be run in channels that are blacklisted. - */ - bypassChannelBlacklist?: boolean; - - /** - * Use instead of {@link BaseBushCommandOptions.args} when using argument generators or custom slashOptions - */ - helpArgs?: ArgsInfo[]; - - /** - * Extra information about the command, displayed in the help command. - */ - note?: string; -} - -export interface BaseBushCommandOptions - extends Omit, - ExtendedCommandOptions { - /** - * The description of the command. - */ - description: string; - - /** - * The arguments for the command. - */ - args?: BushArgumentOptions[] & CustomBushArgumentOptions[]; - - category: string; - - /** - * Permissions required by the client to run this command. - */ - clientPermissions: bigint | bigint[] | BushMissingPermissionSupplier; - - /** - * Permissions required by the user to run this command. - */ - userPermissions: bigint | bigint[] | BushMissingPermissionSupplier; - - /** - * Whether the argument is only accessible to the owners. - */ - ownerOnly?: boolean; - - /** - * Whether the argument is only accessible to the super users. - */ - superUserOnly?: boolean; -} - -export type BushCommandOptions = Omit | Omit; - -export interface ArgsInfo { - /** - * The name of the argument. - */ - name: string; - - /** - * The description of the argument. - */ - description: string; - - /** - * Whether the argument is optional. - * @default false - */ - optional?: boolean; - - /** - * Whether or not the argument has autocomplete enabled. - * @default false - */ - autocomplete?: boolean; - - /** - * Whether the argument is restricted a certain command. - * @default 'slash & text' - */ - only?: 'slash & text' | 'slash' | 'text'; - - /** - * The method that arguments are matched for text commands. - * @default 'phrase' - */ - match?: ArgumentMatch; - - /** - * The readable type of the argument. - */ - type: string; - - /** - * If {@link match} is 'flag' or 'option', these are the flags that are matched - * @default [] - */ - flag?: string[]; - - /** - * Whether the argument is only accessible to the owners. - * @default false - */ - ownerOnly?: boolean; - - /** - * Whether the argument is only accessible to the super users. - * @default false - */ - superUserOnly?: boolean; -} - -export abstract class BushCommand extends Command { - public declare client: BushClient; - public declare handler: BushCommandHandler; - public declare description: string; - - /** - * Show how to use the command. - */ - public usage: string[]; - - /** - * Examples for how to use the command. - */ - public examples: string[]; - - /** - * The options sent to the constructor - */ - public options: BushCommandOptions; - - /** - * The options sent to the super call - */ - public parsedOptions: CommandOptions; - - /** - * The channels the command is limited to run in. - */ - public restrictedChannels: Snowflake[] | undefined; - - /** - * The guilds the command is limited to run in. - */ - public restrictedGuilds: Snowflake[] | undefined; - - /** - * Whether the command is hidden from the help command. - */ - public hidden: boolean; - - /** - * A fake command, completely hidden from the help command. - */ - public pseudo: boolean; - - /** - * Allow this command to be run in channels that are blacklisted. - */ - public bypassChannelBlacklist: boolean; - - /** - * Info about the arguments for the help command. - */ - public argsInfo?: ArgsInfo[]; - - /** - * Extra information about the command, displayed in the help command. - */ - public note?: string; - - public constructor(id: string, options: BushCommandOptions) { - const options_ = options as BaseBushCommandOptions; - - if (options_.args && typeof options_.args !== 'function') { - options_.args.forEach((_, index: number) => { - if ('customType' in (options_.args?.[index] ?? {})) { - if (!options_.args![index]['type']) options_.args![index]['type'] = options_.args![index]['customType']! as any; - delete options_.args![index]['customType']; - } - }); - } - - const newOptions: Partial = {}; - for (const _key in options_) { - const key = _key as keyof typeof options_; // you got to love typescript - if (key === 'args' && 'args' in options_ && typeof options_.args === 'object') { - const newTextArgs: (ArgumentOptions & ExtraArgumentOptions)[] = []; - const newSlashArgs: SlashOption[] = []; - for (const arg of options_.args) { - if (arg.only !== 'slash' && !options_.slashOnly) { - const newArg: ArgumentOptions & ExtraArgumentOptions = {}; - if ('default' in arg) newArg.default = arg.default; - if ('description' in arg) newArg.description = arg.description; - if ('flag' in arg) newArg.flag = arg.flag; - if ('id' in arg) newArg.id = arg.id; - if ('index' in arg) newArg.index = arg.index; - if ('limit' in arg) newArg.limit = arg.limit; - if ('match' in arg) newArg.match = arg.match; - if ('modifyOtherwise' in arg) newArg.modifyOtherwise = arg.modifyOtherwise; - if ('multipleFlags' in arg) newArg.multipleFlags = arg.multipleFlags; - if ('otherwise' in arg) newArg.otherwise = arg.otherwise; - if ('prompt' in arg || 'retry' in arg || 'optional' in arg) { - newArg.prompt = {}; - if ('prompt' in arg) newArg.prompt.start = arg.prompt; - if ('retry' in arg) newArg.prompt.retry = arg.retry; - if ('optional' in arg) newArg.prompt.optional = arg.optional; - } - if ('type' in arg) newArg.type = arg.type as ArgumentType | ArgumentTypeCaster; - if ('unordered' in arg) newArg.unordered = arg.unordered; - if ('ownerOnly' in arg) newArg.ownerOnly = arg.ownerOnly; - if ('superUserOnly' in arg) newArg.superUserOnly = arg.superUserOnly; - newTextArgs.push(newArg); - } - if ( - arg.only !== 'text' && - !('slashOptions' in options_) && - (options_.slash || options_.slashOnly) && - arg.slashType !== false - ) { - const newArg: { - [key in SlashOptionKeys]?: any; - } = { - name: arg.id, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - description: arg.prompt || arg.description || 'No description provided.', - type: arg.slashType - }; - if ('slashResolve' in arg) newArg.resolve = arg.slashResolve; - if ('autocomplete' in arg) newArg.autocomplete = arg.autocomplete; - if ('channelTypes' in arg) newArg.channelTypes = arg.channelTypes; - if ('choices' in arg) newArg.choices = arg.choices; - if ('minValue' in arg) newArg.minValue = arg.minValue; - if ('maxValue' in arg) newArg.maxValue = arg.maxValue; - newArg.required = 'optional' in arg ? !arg.optional : true; - newSlashArgs.push(newArg as SlashOption); - } - } - if (newTextArgs.length > 0) newOptions.args = newTextArgs; - if (newSlashArgs.length > 0) newOptions.slashOptions = options_.slashOptions ?? newSlashArgs; - } else if (key === 'clientPermissions' || key === 'userPermissions') { - newOptions[key] = options_[key] as PermissionResolvable | PermissionResolvable[] | MissingPermissionSupplier; - } else { - newOptions[key] = options_[key]; - } - } - - super(id, newOptions); - - if (options_.args ?? options_.helpArgs) { - const argsInfo: ArgsInfo[] = []; - const combined = (options_.args ?? options_.helpArgs)!.map((arg) => { - const norm = options_.args - ? options_.args.find((_arg) => _arg.id === ('id' in arg ? arg.id : arg.name)) ?? ({} as BushArgumentOptions) - : ({} as BushArgumentOptions); - const help = options_.helpArgs - ? options_.helpArgs.find((_arg) => _arg.name === ('id' in arg ? arg.id : arg.name)) ?? ({} as ArgsInfo) - : ({} as ArgsInfo); - return { ...norm, ...help }; - }); - - for (const arg of combined) { - const name = _.camelCase('id' in arg ? arg.id : arg.name), - description = arg.description || '*No description provided.*', - optional = arg.optional ?? false, - autocomplete = arg.autocomplete ?? false, - only = arg.only ?? 'slash & text', - match = arg.match ?? 'phrase', - type = match === 'flag' ? 'flag' : arg.readableType ?? arg.type ?? 'string', - flag = arg.flag ? (Array.isArray(arg.flag) ? arg.flag : [arg.flag]) : [], - ownerOnly = arg.ownerOnly ?? false, - superUserOnly = arg.superUserOnly ?? false; - - argsInfo.push({ name, description, optional, autocomplete, only, match, type, flag, ownerOnly, superUserOnly }); - } - - this.argsInfo = argsInfo; - } - - this.description = options_.description; - this.usage = options_.usage; - this.examples = options_.examples; - this.options = options_; - this.parsedOptions = newOptions; - this.hidden = !!options_.hidden; - this.restrictedChannels = options_.restrictedChannels; - this.restrictedGuilds = options_.restrictedGuilds; - this.pseudo = !!options_.pseudo; - this.bypassChannelBlacklist = !!options_.bypassChannelBlacklist; - this.note = options_.note; - } - - /** - * Executes the command. - * @param message - Message that triggered the command. - * @param args - Evaluated arguments. - */ - public abstract override exec(message: CommandMessage, args: any): any; - /** - * Executes the command. - * @param message - Message that triggered the command. - * @param args - Evaluated arguments. - */ - public abstract override exec(message: CommandMessage | SlashMessage, args: any): any; -} - -type SlashOptionKeys = - | keyof AkairoApplicationCommandSubGroupData - | keyof AkairoApplicationCommandNonOptionsData - | keyof AkairoApplicationCommandChannelOptionData - | keyof AkairoApplicationCommandChoicesData - | keyof AkairoApplicationCommandAutocompleteOption - | keyof AkairoApplicationCommandNumericOptionData - | keyof AkairoApplicationCommandSubCommandData; - -interface PseudoArguments extends BaseBushArgumentType { - boolean: boolean; - flag: boolean; - regex: { match: RegExpMatchArray; matches: RegExpExecArray[] }; -} - -export type ArgType = NonNullable; -export type OptArgType = PseudoArguments[T]; - -/** - * `util` is always defined for messages after `'all'` inhibitors - */ -export type CommandMessage = Message & { - /** - * Extra properties applied to the Discord.js message object. - * Utilities for command responding. - * Available on all messages after 'all' inhibitors and built-in inhibitors (bot, client). - * Not all properties of the util are available, depending on the input. - * */ - util: CommandUtil; -}; diff --git a/src/lib/extensions/discord-akairo/BushCommandHandler.ts b/src/lib/extensions/discord-akairo/BushCommandHandler.ts deleted file mode 100644 index da49af9..0000000 --- a/src/lib/extensions/discord-akairo/BushCommandHandler.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { type BushCommand, type CommandMessage, type SlashMessage } from '#lib'; -import { CommandHandler, type Category, type CommandHandlerEvents, type CommandHandlerOptions } from 'discord-akairo'; -import { type Collection, type Message, type PermissionsString } from 'discord.js'; - -export type BushCommandHandlerOptions = CommandHandlerOptions; - -export interface BushCommandHandlerEvents extends CommandHandlerEvents { - commandBlocked: [message: CommandMessage, command: BushCommand, reason: string]; - commandBreakout: [message: CommandMessage, command: BushCommand, /* no util */ breakMessage: Message]; - commandCancelled: [message: CommandMessage, command: BushCommand, /* no util */ retryMessage?: Message]; - commandFinished: [message: CommandMessage, command: BushCommand, args: any, returnValue: any]; - commandInvalid: [message: CommandMessage, command: BushCommand]; - commandLocked: [message: CommandMessage, command: BushCommand]; - commandStarted: [message: CommandMessage, command: BushCommand, args: any]; - cooldown: [message: CommandMessage | SlashMessage, command: BushCommand, remaining: number]; - error: [error: Error, message: /* no util */ Message, command?: BushCommand]; - inPrompt: [message: /* no util */ Message]; - load: [command: BushCommand, isReload: boolean]; - messageBlocked: [message: /* no util */ Message | CommandMessage | SlashMessage, reason: string]; - messageInvalid: [message: CommandMessage]; - missingPermissions: [message: CommandMessage, command: BushCommand, type: 'client' | 'user', missing: PermissionsString[]]; - remove: [command: BushCommand]; - slashBlocked: [message: SlashMessage, command: BushCommand, reason: string]; - slashError: [error: Error, message: SlashMessage, command: BushCommand]; - slashFinished: [message: SlashMessage, command: BushCommand, args: any, returnValue: any]; - slashMissingPermissions: [message: SlashMessage, command: BushCommand, type: 'client' | 'user', missing: PermissionsString[]]; - slashStarted: [message: SlashMessage, command: BushCommand, args: any]; -} - -export class BushCommandHandler extends CommandHandler { - public declare modules: Collection; - public declare categories: Collection>; -} - -export interface BushCommandHandler extends CommandHandler { - findCommand(name: string): BushCommand; -} diff --git a/src/lib/extensions/discord-akairo/BushInhibitor.ts b/src/lib/extensions/discord-akairo/BushInhibitor.ts deleted file mode 100644 index be396cf..0000000 --- a/src/lib/extensions/discord-akairo/BushInhibitor.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type BushCommand, type CommandMessage, type SlashMessage } from '#lib'; -import { Inhibitor } from 'discord-akairo'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { Message } from 'discord.js'; - -export abstract class BushInhibitor extends Inhibitor { - /** - * Checks if message should be blocked. - * A return value of true will block the message. - * If returning a Promise, a resolved value of true will block the message. - * - * **Note:** `'all'` type inhibitors do not have {@link Message.util} defined. - * - * @param message - Message being handled. - * @param command - Command to check. - */ - public abstract override exec(message: CommandMessage, command: BushCommand): any; - public abstract override exec(message: CommandMessage | SlashMessage, command: BushCommand): any; -} diff --git a/src/lib/extensions/discord-akairo/BushInhibitorHandler.ts b/src/lib/extensions/discord-akairo/BushInhibitorHandler.ts deleted file mode 100644 index 5e4fb6c..0000000 --- a/src/lib/extensions/discord-akairo/BushInhibitorHandler.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { InhibitorHandler } from 'discord-akairo'; - -export class BushInhibitorHandler extends InhibitorHandler {} diff --git a/src/lib/extensions/discord-akairo/BushListener.ts b/src/lib/extensions/discord-akairo/BushListener.ts deleted file mode 100644 index 6917641..0000000 --- a/src/lib/extensions/discord-akairo/BushListener.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Listener } from 'discord-akairo'; - -export abstract class BushListener extends Listener {} diff --git a/src/lib/extensions/discord-akairo/BushListenerHandler.ts b/src/lib/extensions/discord-akairo/BushListenerHandler.ts deleted file mode 100644 index 9c3e4af..0000000 --- a/src/lib/extensions/discord-akairo/BushListenerHandler.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ListenerHandler } from 'discord-akairo'; - -export class BushListenerHandler extends ListenerHandler {} diff --git a/src/lib/extensions/discord-akairo/BushTask.ts b/src/lib/extensions/discord-akairo/BushTask.ts deleted file mode 100644 index 1b70c88..0000000 --- a/src/lib/extensions/discord-akairo/BushTask.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Task } from 'discord-akairo'; - -export abstract class BushTask extends Task {} diff --git a/src/lib/extensions/discord-akairo/BushTaskHandler.ts b/src/lib/extensions/discord-akairo/BushTaskHandler.ts deleted file mode 100644 index 6535abb..0000000 --- a/src/lib/extensions/discord-akairo/BushTaskHandler.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { TaskHandler } from 'discord-akairo'; - -export class BushTaskHandler extends TaskHandler {} diff --git a/src/lib/extensions/discord-akairo/SlashMessage.ts b/src/lib/extensions/discord-akairo/SlashMessage.ts deleted file mode 100644 index 0a6669b..0000000 --- a/src/lib/extensions/discord-akairo/SlashMessage.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { AkairoMessage } from 'discord-akairo'; - -export class SlashMessage extends AkairoMessage {} diff --git a/src/lib/extensions/discord.js/BushClientEvents.ts b/src/lib/extensions/discord.js/BushClientEvents.ts deleted file mode 100644 index 22bae65..0000000 --- a/src/lib/extensions/discord.js/BushClientEvents.ts +++ /dev/null @@ -1,200 +0,0 @@ -import type { - BanResponse, - CommandMessage, - Guild as GuildDB, - GuildSettings -} from '#lib'; -import type { AkairoClientEvents } from 'discord-akairo'; -import type { - ButtonInteraction, - Collection, - Guild, - GuildMember, - GuildTextBasedChannel, - Message, - ModalSubmitInteraction, - Role, - SelectMenuInteraction, - Snowflake, - User -} from 'discord.js'; - -export interface BushClientEvents extends AkairoClientEvents { - bushBan: [ - victim: GuildMember | User, - moderator: User, - guild: Guild, - reason: string | undefined, - caseID: string, - duration: number, - dmSuccess?: boolean, - evidence?: string - ]; - bushBlock: [ - victim: GuildMember, - moderator: User, - guild: Guild, - reason: string | undefined, - caseID: string, - duration: number, - dmSuccess: boolean, - channel: GuildTextBasedChannel, - evidence?: string - ]; - bushKick: [ - victim: GuildMember, - moderator: User, - guild: Guild, - reason: string | undefined, - caseID: string, - dmSuccess: boolean, - evidence?: string - ]; - bushMute: [ - victim: GuildMember, - moderator: User, - guild: Guild, - reason: string | undefined, - caseID: string, - duration: number, - dmSuccess: boolean, - evidence?: string - ]; - bushPunishRole: [ - victim: GuildMember, - moderator: User, - guild: Guild, - reason: string | undefined, - caseID: string, - duration: number, - role: Role, - evidence?: string - ]; - bushPunishRoleRemove: [ - victim: GuildMember, - moderator: User, - guild: Guild, - reason: string | undefined, - caseID: string, - role: Role, - evidence?: string - ]; - bushPurge: [ - moderator: User, - guild: Guild, - channel: GuildTextBasedChannel, - messages: Collection - ]; - bushRemoveTimeout: [ - victim: GuildMember, - moderator: User, - guild: Guild, - reason: string | undefined, - caseID: string, - dmSuccess: boolean, - evidence?: string - ]; - bushTimeout: [ - victim: GuildMember, - moderator: User, - guild: Guild, - reason: string | undefined, - caseID: string, - duration: number, - dmSuccess: boolean, - evidence?: string - ]; - bushUnban: [ - victim: User, - moderator: User, - guild: Guild, - reason: string | undefined, - caseID: string, - dmSuccess: boolean, - evidence?: string - ]; - bushUnblock: [ - victim: GuildMember | User, - moderator: User, - guild: Guild, - reason: string | undefined, - caseID: string, - dmSuccess: boolean, - channel: GuildTextBasedChannel, - evidence?: string - ]; - bushUnmute: [ - victim: GuildMember, - moderator: User, - guild: Guild, - reason: string | undefined, - caseID: string, - dmSuccess: boolean, - evidence?: string - ]; - bushUpdateModlog: [ - moderator: GuildMember, - modlogID: string, - key: 'evidence' | 'hidden', - oldModlog: string | boolean, - newModlog: string | boolean - ]; - bushUpdateSettings: [ - setting: Setting, - guild: Guild, - oldValue: GuildDB[Setting], - newValue: GuildDB[Setting], - moderator?: GuildMember - ]; - bushWarn: [ - victim: GuildMember, - moderator: User, - guild: Guild, - reason: string | undefined, - caseID: string, - dmSuccess: boolean, - evidence?: string - ]; - bushLevelUpdate: [ - member: GuildMember, - oldLevel: number, - newLevel: number, - currentXp: number, - message: CommandMessage - ]; - bushLockdown: [ - moderator: GuildMember, - reason: string | undefined, - channelsSuccessMap: Collection, - all?: boolean - ]; - bushUnlockdown: [ - moderator: GuildMember, - reason: string | undefined, - channelsSuccessMap: Collection, - all?: boolean - ]; - massBan: [ - moderator: GuildMember, - guild: Guild, - reason: string | undefined, - results: Collection - ]; - massEvidence: [ - moderator: GuildMember, - guild: Guild, - evidence: string, - lines: string[] - ]; - /* components */ - button: [button: ButtonInteraction]; - selectMenu: [selectMenu: SelectMenuInteraction]; - modal: [modal: ModalSubmitInteraction]; -} - -type Setting = - | GuildSettings - | 'enabledFeatures' - | 'blacklistedChannels' - | 'blacklistedUsers' - | 'disabledCommands'; diff --git a/src/lib/extensions/discord.js/ExtendedGuild.ts b/src/lib/extensions/discord.js/ExtendedGuild.ts deleted file mode 100644 index 3dce7ca..0000000 --- a/src/lib/extensions/discord.js/ExtendedGuild.ts +++ /dev/null @@ -1,916 +0,0 @@ -import { - AllowedMentions, - banResponse, - colors, - dmResponse, - emojis, - permissionsResponse, - punishmentEntryRemove, - type BanResponse, - type GuildFeatures, - type GuildLogType, - type GuildModel -} from '#lib'; -import assert from 'assert/strict'; -import { - AttachmentBuilder, - AttachmentPayload, - Collection, - Guild, - JSONEncodable, - Message, - MessageType, - PermissionFlagsBits, - SnowflakeUtil, - ThreadChannel, - type APIMessage, - type GuildMember, - type GuildMemberResolvable, - type GuildTextBasedChannel, - type MessageOptions, - type MessagePayload, - type NewsChannel, - type Snowflake, - type TextChannel, - type User, - type UserResolvable, - type VoiceChannel, - type Webhook, - type WebhookMessageOptions -} from 'discord.js'; -import _ from 'lodash'; -import * as Moderation from '../../common/util/Moderation.js'; -import { Guild as GuildDB } from '../../models/instance/Guild.js'; -import { ModLogType } from '../../models/instance/ModLog.js'; -import { addOrRemoveFromArray } from '../../utils/BushUtils.js'; - -declare module 'discord.js' { - export interface Guild { - /** - * Checks if the guild has a certain custom feature. - * @param feature The feature to check for - */ - hasFeature(feature: GuildFeatures): Promise; - /** - * Adds a custom feature to the guild. - * @param feature The feature to add - * @param moderator The moderator responsible for adding a feature - */ - addFeature(feature: GuildFeatures, moderator?: GuildMember): Promise; - /** - * Removes a custom feature from the guild. - * @param feature The feature to remove - * @param moderator The moderator responsible for removing a feature - */ - removeFeature(feature: GuildFeatures, moderator?: GuildMember): Promise; - /** - * Makes a custom feature the opposite of what it was before - * @param feature The feature to toggle - * @param moderator The moderator responsible for toggling a feature - */ - toggleFeature(feature: GuildFeatures, moderator?: GuildMember): Promise; - /** - * Fetches a custom setting for the guild - * @param setting The setting to get - */ - getSetting(setting: K): Promise; - /** - * Sets a custom setting for the guild - * @param setting The setting to change - * @param value The value to change the setting to - * @param moderator The moderator to responsible for changing the setting - */ - setSetting>( - setting: K, - value: GuildModel[K], - moderator?: GuildMember - ): Promise; - /** - * Get a the log channel configured for a certain log type. - * @param logType The type of log channel to get. - * @returns Either the log channel or undefined if not configured. - */ - getLogChannel(logType: GuildLogType): Promise; - /** - * Sends a message to the guild's specified logging channel - * @param logType The corresponding channel that the message will be sent to - * @param message The parameters for {@link BushTextChannel.send} - */ - sendLogChannel(logType: GuildLogType, message: string | MessagePayload | MessageOptions): Promise; - /** - * Sends a formatted error message in a guild's error log channel - * @param title The title of the error embed - * @param message The description of the error embed - */ - error(title: string, message: string): Promise; - /** - * Bans a user, dms them, creates a mod log entry, and creates a punishment entry. - * @param options Options for banning the user. - * @returns A string status message of the ban. - */ - bushBan(options: GuildBushBanOptions): Promise; - /** - * {@link bushBan} with less resolving and checks - * @param options Options for banning the user. - * @returns A string status message of the ban. - * **Preconditions:** - * - {@link me} has the `BanMembers` permission - * **Warning:** - * - Doesn't emit bushBan Event - */ - massBanOne(options: GuildMassBanOneOptions): Promise; - /** - * Unbans a user, dms them, creates a mod log entry, and destroys the punishment entry. - * @param options Options for unbanning the user. - * @returns A status message of the unban. - */ - bushUnban(options: GuildBushUnbanOptions): Promise; - /** - * Denies send permissions in specified channels - * @param options The options for locking down the guild - */ - lockdown(options: LockdownOptions): Promise; - quote(rawQuote: APIMessage, channel: GuildTextBasedChannel): Promise; - } -} - -/** - * Represents a guild (or a server) on Discord. - * It's recommended to see if a guild is available before performing operations or reading data from it. You can - * check this with {@link ExtendedGuild.available}. - */ -export class ExtendedGuild extends Guild { - /** - * Checks if the guild has a certain custom feature. - * @param feature The feature to check for - */ - public override async hasFeature(feature: GuildFeatures): Promise { - const features = await this.getSetting('enabledFeatures'); - return features.includes(feature); - } - - /** - * Adds a custom feature to the guild. - * @param feature The feature to add - * @param moderator The moderator responsible for adding a feature - */ - public override async addFeature(feature: GuildFeatures, moderator?: GuildMember): Promise { - const features = await this.getSetting('enabledFeatures'); - const newFeatures = addOrRemoveFromArray('add', features, feature); - return (await this.setSetting('enabledFeatures', newFeatures, moderator)).enabledFeatures; - } - - /** - * Removes a custom feature from the guild. - * @param feature The feature to remove - * @param moderator The moderator responsible for removing a feature - */ - public override async removeFeature(feature: GuildFeatures, moderator?: GuildMember): Promise { - const features = await this.getSetting('enabledFeatures'); - const newFeatures = addOrRemoveFromArray('remove', features, feature); - return (await this.setSetting('enabledFeatures', newFeatures, moderator)).enabledFeatures; - } - - /** - * Makes a custom feature the opposite of what it was before - * @param feature The feature to toggle - * @param moderator The moderator responsible for toggling a feature - */ - public override async toggleFeature(feature: GuildFeatures, moderator?: GuildMember): Promise { - return (await this.hasFeature(feature)) - ? await this.removeFeature(feature, moderator) - : await this.addFeature(feature, moderator); - } - - /** - * Fetches a custom setting for the guild - * @param setting The setting to get - */ - public override async getSetting(setting: K): Promise { - return ( - this.client.cache.guilds.get(this.id)?.[setting] ?? - ((await GuildDB.findByPk(this.id)) ?? GuildDB.build({ id: this.id }))[setting] - ); - } - - /** - * Sets a custom setting for the guild - * @param setting The setting to change - * @param value The value to change the setting to - * @param moderator The moderator to responsible for changing the setting - */ - public override async setSetting>( - setting: K, - value: GuildDB[K], - moderator?: GuildMember - ): Promise { - const row = (await GuildDB.findByPk(this.id)) ?? GuildDB.build({ id: this.id }); - const oldValue = row[setting] as GuildDB[K]; - row[setting] = value; - this.client.cache.guilds.set(this.id, row.toJSON() as GuildDB); - this.client.emit('bushUpdateSettings', setting, this, oldValue, row[setting], moderator); - return await row.save(); - } - - /** - * Get a the log channel configured for a certain log type. - * @param logType The type of log channel to get. - * @returns Either the log channel or undefined if not configured. - */ - public override async getLogChannel(logType: GuildLogType): Promise { - const channelId = (await this.getSetting('logChannels'))[logType]; - if (!channelId) return undefined; - return ( - (this.channels.cache.get(channelId) as TextChannel | undefined) ?? - ((await this.channels.fetch(channelId)) as TextChannel | null) ?? - undefined - ); - } - - /** - * Sends a message to the guild's specified logging channel - * @param logType The corresponding channel that the message will be sent to - * @param message The parameters for {@link BushTextChannel.send} - */ - public override async sendLogChannel( - logType: GuildLogType, - message: string | MessagePayload | MessageOptions - ): Promise { - const logChannel = await this.getLogChannel(logType); - if (!logChannel || !logChannel.isTextBased()) return; - if ( - !logChannel - .permissionsFor(this.members.me!.id) - ?.has([PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.EmbedLinks]) - ) - return; - - return await logChannel.send(message).catch(() => null); - } - - /** - * Sends a formatted error message in a guild's error log channel - * @param title The title of the error embed - * @param message The description of the error embed - */ - public override async error(title: string, message: string): Promise { - void this.client.console.info(_.camelCase(title), message.replace(/\*\*(.*?)\*\*/g, '<<$1>>')); - void this.sendLogChannel('error', { embeds: [{ title: title, description: message, color: colors.error }] }); - } - - /** - * Bans a user, dms them, creates a mod log entry, and creates a punishment entry. - * @param options Options for banning the user. - * @returns A string status message of the ban. - */ - public override async bushBan(options: GuildBushBanOptions): Promise { - // checks - if (!this.members.me!.permissions.has(PermissionFlagsBits.BanMembers)) return banResponse.MISSING_PERMISSIONS; - - let caseID: string | undefined = undefined; - let dmSuccessEvent: boolean | undefined = undefined; - const user = await this.client.utils.resolveNonCachedUser(options.user); - const moderator = this.client.users.resolve(options.moderator ?? this.client.user!); - if (!user || !moderator) return banResponse.CANNOT_RESOLVE_USER; - - if ((await this.bans.fetch()).has(user.id)) return banResponse.ALREADY_BANNED; - - const ret = await (async () => { - // add modlog entry - const { log: modlog } = await Moderation.createModLogEntry({ - client: this.client, - type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, - user: user, - moderator: moderator.id, - reason: options.reason, - duration: options.duration, - guild: this, - evidence: options.evidence - }); - if (!modlog) return banResponse.MODLOG_ERROR; - caseID = modlog.id; - - // dm user - dmSuccessEvent = await Moderation.punishDM({ - client: this.client, - modlog: modlog.id, - guild: this, - user: user, - punishment: 'banned', - duration: options.duration ?? 0, - reason: options.reason ?? undefined, - sendFooter: true - }); - - // ban - const banSuccess = await this.bans - .create(user?.id ?? options.user, { - reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, - deleteMessageDays: options.deleteDays - }) - .catch(() => false); - if (!banSuccess) return banResponse.ACTION_ERROR; - - // add punishment entry so they can be unbanned later - const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ - client: this.client, - type: 'ban', - user: user, - guild: this, - duration: options.duration, - modlog: modlog.id - }); - if (!punishmentEntrySuccess) return banResponse.PUNISHMENT_ENTRY_ADD_ERROR; - - if (!dmSuccessEvent) return banResponse.DM_ERROR; - return banResponse.SUCCESS; - })(); - - if (!([banResponse.ACTION_ERROR, banResponse.MODLOG_ERROR, banResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes(ret)) - this.client.emit( - 'bushBan', - user, - moderator, - this, - options.reason ?? undefined, - caseID!, - options.duration ?? 0, - dmSuccessEvent, - options.evidence - ); - return ret; - } - - /** - * {@link bushBan} with less resolving and checks - * @param options Options for banning the user. - * @returns A string status message of the ban. - * **Preconditions:** - * - {@link me} has the `BanMembers` permission - * **Warning:** - * - Doesn't emit bushBan Event - */ - public override async massBanOne(options: GuildMassBanOneOptions): Promise { - if (this.bans.cache.has(options.user)) return banResponse.ALREADY_BANNED; - - const ret = await (async () => { - // add modlog entry - const { log: modlog } = await Moderation.createModLogEntrySimple({ - client: this.client, - type: ModLogType.PERM_BAN, - user: options.user, - moderator: options.moderator, - reason: options.reason, - duration: 0, - guild: this.id - }); - if (!modlog) return banResponse.MODLOG_ERROR; - - let dmSuccessEvent: boolean | undefined = undefined; - // dm user - if (this.members.cache.has(options.user)) { - dmSuccessEvent = await Moderation.punishDM({ - client: this.client, - modlog: modlog.id, - guild: this, - user: options.user, - punishment: 'banned', - duration: 0, - reason: options.reason ?? undefined, - sendFooter: true - }); - } - - // ban - const banSuccess = await this.bans - .create(options.user, { - reason: `${options.moderator} | ${options.reason}`, - deleteMessageDays: options.deleteDays - }) - .catch(() => false); - if (!banSuccess) return banResponse.ACTION_ERROR; - - // add punishment entry so they can be unbanned later - const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ - client: this.client, - type: 'ban', - user: options.user, - guild: this, - duration: 0, - modlog: modlog.id - }); - if (!punishmentEntrySuccess) return banResponse.PUNISHMENT_ENTRY_ADD_ERROR; - - if (!dmSuccessEvent) return banResponse.DM_ERROR; - return banResponse.SUCCESS; - })(); - return ret; - } - - /** - * Unbans a user, dms them, creates a mod log entry, and destroys the punishment entry. - * @param options Options for unbanning the user. - * @returns A status message of the unban. - */ - public override async bushUnban(options: GuildBushUnbanOptions): Promise { - // checks - if (!this.members.me!.permissions.has(PermissionFlagsBits.BanMembers)) return unbanResponse.MISSING_PERMISSIONS; - - let caseID: string | undefined = undefined; - let dmSuccessEvent: boolean | undefined = undefined; - const user = await this.client.utils.resolveNonCachedUser(options.user); - const moderator = this.client.users.resolve(options.moderator ?? this.client.user!); - if (!user || !moderator) return unbanResponse.CANNOT_RESOLVE_USER; - - const ret = await (async () => { - const bans = await this.bans.fetch(); - - let notBanned = false; - if (!bans.has(user.id)) notBanned = true; - - const unbanSuccess = await this.bans - .remove(user, `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) - .catch((e) => { - if (e?.code === 'UNKNOWN_BAN') { - notBanned = true; - return true; - } else return false; - }); - - if (notBanned) return unbanResponse.NOT_BANNED; - if (!unbanSuccess) return unbanResponse.ACTION_ERROR; - - // add modlog entry - const { log: modlog } = await Moderation.createModLogEntry({ - client: this.client, - type: ModLogType.UNBAN, - user: user.id, - moderator: moderator.id, - reason: options.reason, - guild: this, - evidence: options.evidence - }); - if (!modlog) return unbanResponse.MODLOG_ERROR; - caseID = modlog.id; - - // remove punishment entry - const removePunishmentEntrySuccess = await Moderation.removePunishmentEntry({ - client: this.client, - type: 'ban', - user: user.id, - guild: this - }); - if (!removePunishmentEntrySuccess) return unbanResponse.PUNISHMENT_ENTRY_REMOVE_ERROR; - - // dm user - dmSuccessEvent = await Moderation.punishDM({ - client: this.client, - guild: this, - user: user, - punishment: 'unbanned', - reason: options.reason ?? undefined, - sendFooter: false - }); - - if (!dmSuccessEvent) return unbanResponse.DM_ERROR; - return unbanResponse.SUCCESS; - })(); - if ( - !([unbanResponse.ACTION_ERROR, unbanResponse.MODLOG_ERROR, unbanResponse.PUNISHMENT_ENTRY_REMOVE_ERROR] as const).includes( - ret - ) - ) - this.client.emit( - 'bushUnban', - user, - moderator, - this, - options.reason ?? undefined, - caseID!, - dmSuccessEvent!, - options.evidence - ); - return ret; - } - - /** - * Denies send permissions in specified channels - * @param options The options for locking down the guild - */ - public override async lockdown(options: LockdownOptions): Promise { - if (!options.all && !options.channel) return 'all not chosen and no channel specified'; - const channelIds = options.all ? await this.getSetting('lockdownChannels') : [options.channel!.id]; - - if (!channelIds.length) return 'no channels configured'; - const mappedChannels = channelIds.map((id) => this.channels.cache.get(id)); - - const invalidChannels = mappedChannels.filter((c) => c === undefined); - if (invalidChannels.length) return `invalid channel configured: ${invalidChannels.join(', ')}`; - - const moderator = this.members.resolve(options.moderator); - if (!moderator) return 'moderator not found'; - - const errors = new Collection(); - const success = new Collection(); - const ret = await (async (): Promise => { - for (const _channel of mappedChannels) { - const channel = _channel!; - if (!channel.isTextBased()) { - errors.set(channel.id, new Error('wrong channel type')); - success.set(channel.id, false); - continue; - } - if (!channel.permissionsFor(this.members.me!.id)?.has([PermissionFlagsBits.ManageChannels])) { - errors.set(channel.id, new Error('client no permission')); - success.set(channel.id, false); - continue; - } else if (!channel.permissionsFor(moderator)?.has([PermissionFlagsBits.ManageChannels])) { - errors.set(channel.id, new Error('moderator no permission')); - success.set(channel.id, false); - continue; - } - - const reason = `[${options.unlock ? 'Unlockdown' : 'Lockdown'}] ${moderator.user.tag} | ${ - options.reason ?? 'No reason provided' - }`; - - const permissionOverwrites = channel.isThread() ? channel.parent!.permissionOverwrites : channel.permissionOverwrites; - const perms = { - SendMessagesInThreads: options.unlock ? null : false, - SendMessages: options.unlock ? null : false - }; - const permsForMe = { - [channel.isThread() ? 'SendMessagesInThreads' : 'SendMessages']: options.unlock ? null : true - }; // so I can send messages in the channel - - const changePermSuccess = await permissionOverwrites.edit(this.id, perms, { reason }).catch((e) => e); - if (changePermSuccess instanceof Error) { - errors.set(channel.id, changePermSuccess); - success.set(channel.id, false); - } else { - success.set(channel.id, true); - await permissionOverwrites.edit(this.members.me!, permsForMe, { reason }); - await channel.send({ - embeds: [ - { - author: { name: moderator.user.tag, icon_url: moderator.displayAvatarURL() }, - title: `This channel has been ${options.unlock ? 'un' : ''}locked`, - description: options.reason ?? 'No reason provided', - color: options.unlock ? colors.Green : colors.Red, - timestamp: new Date().toISOString() - } - ] - }); - } - } - - if (errors.size) return errors; - else return `success: ${success.filter((c) => c === true).size}`; - })(); - - this.client.emit(options.unlock ? 'bushUnlockdown' : 'bushLockdown', moderator, options.reason, success, options.all); - return ret; - } - - public override async quote(rawQuote: APIMessage, channel: GuildTextBasedChannel): Promise { - if (!channel.isTextBased() || channel.isDMBased() || channel.guildId !== this.id || !this.members.me) return null; - if (!channel.permissionsFor(this.members.me).has('ManageWebhooks')) return null; - - const quote = new Message(this.client, rawQuote); - - const target = channel instanceof ThreadChannel ? channel.parent : channel; - if (!target) return null; - - const webhooks: Collection = await target.fetchWebhooks().catch((e) => e); - if (!(webhooks instanceof Collection)) return null; - - // find a webhook that we can use - let webhook = webhooks.find((w) => !!w.token) ?? null; - if (!webhook) - webhook = await target - .createWebhook({ - name: `${this.client.user!.username} Quotes #${target.name}`, - avatar: this.client.user!.displayAvatarURL({ size: 2048 }), - reason: 'Creating a webhook for quoting' - }) - .catch(() => null); - - if (!webhook) return null; - - const sendOptions: Omit = {}; - - const displayName = quote.member?.displayName ?? quote.author.username; - - switch (quote.type) { - case MessageType.Default: - case MessageType.Reply: - case MessageType.ChatInputCommand: - case MessageType.ContextMenuCommand: - case MessageType.ThreadStarterMessage: - sendOptions.content = quote.content || undefined; - sendOptions.threadId = channel instanceof ThreadChannel ? channel.id : undefined; - sendOptions.embeds = quote.embeds.length ? quote.embeds : undefined; - //@ts-expect-error: jank - sendOptions.attachments = quote.attachments.size - ? [...quote.attachments.values()].map((a) => AttachmentBuilder.from(a as JSONEncodable)) - : undefined; - - if (quote.stickers.size && !(quote.content || quote.embeds.length || quote.attachments.size)) - sendOptions.content = '[[This message has a sticker but not content]]'; - - break; - case MessageType.RecipientAdd: { - const recipient = rawQuote.mentions[0]; - if (!recipient) { - sendOptions.content = `${emojis.error} Cannot resolve recipient.`; - break; - } - - if (quote.channel.isThread()) { - const recipientDisplay = quote.guild?.members.cache.get(recipient.id)?.displayName ?? recipient.username; - sendOptions.content = `${emojis.join} ${displayName} added ${recipientDisplay} to the thread.`; - } else { - // this should never happen - sendOptions.content = `${emojis.join} ${displayName} added ${recipient.username} to the group.`; - } - - break; - } - case MessageType.RecipientRemove: { - const recipient = rawQuote.mentions[0]; - if (!recipient) { - sendOptions.content = `${emojis.error} Cannot resolve recipient.`; - break; - } - - if (quote.channel.isThread()) { - const recipientDisplay = quote.guild?.members.cache.get(recipient.id)?.displayName ?? recipient.username; - sendOptions.content = `${emojis.leave} ${displayName} removed ${recipientDisplay} from the thread.`; - } else { - // this should never happen - sendOptions.content = `${emojis.leave} ${displayName} removed ${recipient.username} from the group.`; - } - - break; - } - - case MessageType.ChannelNameChange: - sendOptions.content = `<:pencil:957988608994861118> ${displayName} changed the channel name: **${quote.content}**`; - - break; - - case MessageType.ChannelPinnedMessage: - throw new Error('Not implemented yet: MessageType.ChannelPinnedMessage case'); - case MessageType.UserJoin: { - const messages = [ - '{username} joined the party.', - '{username} is here.', - 'Welcome, {username}. We hope you brought pizza.', - 'A wild {username} appeared.', - '{username} just landed.', - '{username} just slid into the server.', - '{username} just showed up!', - 'Welcome {username}. Say hi!', - '{username} hopped into the server.', - 'Everyone welcome {username}!', - "Glad you're here, {username}.", - 'Good to see you, {username}.', - 'Yay you made it, {username}!' - ]; - - const timestamp = SnowflakeUtil.timestampFrom(quote.id); - - // this is the same way that the discord client decides what message to use. - const message = messages[timestamp % messages.length].replace(/{username}/g, displayName); - - sendOptions.content = `${emojis.join} ${message}`; - break; - } - case MessageType.GuildBoost: - sendOptions.content = `<:NitroBoost:585558042309820447> ${displayName} just boosted the server${ - quote.content ? ` **${quote.content}** times` : '' - }!`; - - break; - case MessageType.GuildBoostTier1: - case MessageType.GuildBoostTier2: - case MessageType.GuildBoostTier3: - sendOptions.content = `<:NitroBoost:585558042309820447> ${displayName} just boosted the server${ - quote.content ? ` **${quote.content}** times` : '' - }! ${quote.guild?.name} has achieved **Level ${quote.type - 8}!**`; - - break; - case MessageType.ChannelFollowAdd: - sendOptions.content = `${displayName} has added **${quote.content}** to this channel. Its most important updates will show up here.`; - - break; - case MessageType.GuildDiscoveryDisqualified: - sendOptions.content = - '<:SystemMessageCross:842172192418693173> This server has been removed from Server Discovery because it no longer passes all the requirements. Check Server Settings for more details.'; - - break; - case MessageType.GuildDiscoveryRequalified: - sendOptions.content = - '<:SystemMessageCheck:842172191801212949> This server is eligible for Server Discovery again and has been automatically relisted!'; - - break; - case MessageType.GuildDiscoveryGracePeriodInitialWarning: - sendOptions.content = - '<:SystemMessageWarn:842172192401915971> This server has failed Discovery activity requirements for 1 week. If this server fails for 4 weeks in a row, it will be automatically removed from Discovery.'; - - break; - case MessageType.GuildDiscoveryGracePeriodFinalWarning: - sendOptions.content = - '<:SystemMessageWarn:842172192401915971> This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery.'; - - break; - case MessageType.ThreadCreated: { - const threadId = rawQuote.message_reference?.channel_id; - - sendOptions.content = `<:thread:865033845753249813> ${displayName} started a thread: **[${quote.content}](https://discord.com/channels/${quote.guildId}/${threadId} - )**. See all threads.`; - - break; - } - case MessageType.GuildInviteReminder: - sendOptions.content = 'Wondering who to invite? Start by inviting anyone who can help you build the server!'; - - break; - // todo: use enum for this - case 24 as MessageType: { - const embed = quote.embeds[0]; - // eslint-disable-next-line deprecation/deprecation - assert.equal(embed.data.type, 'auto_moderation_message'); - const ruleName = embed.fields!.find((f) => f.name === 'rule_name')!.value; - const channelId = embed.fields!.find((f) => f.name === 'channel_id')!.value; - const keyword = embed.fields!.find((f) => f.name === 'keyword')!.value; - - sendOptions.username = `AutoMod (${quote.member?.displayName ?? quote.author.username})`; - sendOptions.content = `Automod has blocked a message in <#${channelId}>`; - sendOptions.embeds = [ - { - title: quote.member?.displayName ?? quote.author.username, - description: embed.description ?? 'There is no content???', - footer: { - text: `Keyword: ${keyword} • Rule: ${ruleName}` - }, - color: 0x36393f - } - ]; - - break; - } - case MessageType.ChannelIconChange: - case MessageType.Call: - default: - sendOptions.content = `${emojis.error} I cannot quote messages of type **${ - MessageType[quote.type] || quote.type - }** messages, please report this to my developers.`; - - break; - } - - sendOptions.allowedMentions = AllowedMentions.none(); - sendOptions.username ??= quote.member?.displayName ?? quote.author.username; - sendOptions.avatarURL = quote.member?.displayAvatarURL({ size: 2048 }) ?? quote.author.displayAvatarURL({ size: 2048 }); - - return await webhook.send(sendOptions); /* .catch((e: any) => e); */ - } -} - -/** - * Options for unbanning a user - */ -export interface GuildBushUnbanOptions { - /** - * The user to unban - */ - user: UserResolvable | User; - - /** - * The reason for unbanning the user - */ - reason?: string | null; - - /** - * The moderator who unbanned the user - */ - moderator?: UserResolvable; - - /** - * The evidence for the unban - */ - evidence?: string; -} - -export interface GuildMassBanOneOptions { - /** - * The user to ban - */ - user: Snowflake; - - /** - * The reason to ban the user - */ - reason: string; - - /** - * The moderator who banned the user - */ - moderator: Snowflake; - - /** - * The number of days to delete the user's messages for - */ - deleteDays?: number; -} - -/** - * Options for banning a user - */ -export interface GuildBushBanOptions { - /** - * The user to ban - */ - user: UserResolvable; - - /** - * The reason to ban the user - */ - reason?: string | null; - - /** - * The moderator who banned the user - */ - moderator?: UserResolvable; - - /** - * The duration of the ban - */ - duration?: number; - - /** - * The number of days to delete the user's messages for - */ - deleteDays?: number; - - /** - * The evidence for the ban - */ - evidence?: string; -} - -type ValueOf = T[keyof T]; - -export const unbanResponse = Object.freeze({ - ...dmResponse, - ...permissionsResponse, - ...punishmentEntryRemove, - NOT_BANNED: 'user not banned' -} as const); - -/** - * Response returned when unbanning a user - */ -export type UnbanResponse = ValueOf; - -/** - * Options for locking down channel(s) - */ -export interface LockdownOptions { - /** - * The moderator responsible for the lockdown - */ - moderator: GuildMemberResolvable; - - /** - * Whether to lock down all (specified) channels - */ - all: boolean; - - /** - * Reason for the lockdown - */ - reason?: string; - - /** - * A specific channel to lockdown - */ - channel?: ThreadChannel | NewsChannel | TextChannel | VoiceChannel; - - /** - * Whether or not to unlock the channel(s) instead of locking them - */ - unlock?: boolean; -} - -/** - * Response returned when locking down a channel - */ -export type LockdownResponse = - | `success: ${number}` - | 'all not chosen and no channel specified' - | 'no channels configured' - | `invalid channel configured: ${string}` - | 'moderator not found' - | Collection; diff --git a/src/lib/extensions/discord.js/ExtendedGuildMember.ts b/src/lib/extensions/discord.js/ExtendedGuildMember.ts deleted file mode 100644 index f8add83..0000000 --- a/src/lib/extensions/discord.js/ExtendedGuildMember.ts +++ /dev/null @@ -1,1255 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { formatError, Moderation, ModLogType, Time, type BushClientEvents, type PunishmentTypeDM, type ValueOf } from '#lib'; -import { - ChannelType, - GuildMember, - PermissionFlagsBits, - type GuildChannelResolvable, - type GuildTextBasedChannel, - type Role -} from 'discord.js'; -/* eslint-enable @typescript-eslint/no-unused-vars */ - -declare module 'discord.js' { - export interface GuildMember { - /** - * Send a punishment dm to the user. - * @param punishment The punishment that the user has received. - * @param reason The reason for the user's punishment. - * @param duration The duration of the punishment. - * @param modlog The modlog case id so the user can make an appeal. - * @param sendFooter Whether or not to send the guild's punishment footer with the dm. - * @returns Whether or not the dm was sent successfully. - */ - bushPunishDM( - punishment: PunishmentTypeDM, - reason?: string | null, - duration?: number, - modlog?: string, - sendFooter?: boolean - ): Promise; - /** - * Warn the user, create a modlog entry, and send a dm to the user. - * @param options Options for warning the user. - * @returns An object with the result of the warning, and the case number of the warn. - * @emits {@link BushClientEvents.bushWarn} - */ - bushWarn(options: BushPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number | null }>; - /** - * Add a role to the user, if it is a punishment create a modlog entry, and create a punishment entry if it is temporary or a punishment. - * @param options Options for adding a role to the user. - * @returns A status message for adding the add. - * @emits {@link BushClientEvents.bushPunishRole} - */ - bushAddRole(options: AddRoleOptions): Promise; - /** - * Remove a role from the user, if it is a punishment create a modlog entry, and destroy a punishment entry if it was temporary or a punishment. - * @param options Options for removing a role from the user. - * @returns A status message for removing the role. - * @emits {@link BushClientEvents.bushPunishRoleRemove} - */ - bushRemoveRole(options: RemoveRoleOptions): Promise; - /** - * Mute the user, create a modlog entry, creates a punishment entry, and dms the user. - * @param options Options for muting the user. - * @returns A status message for muting the user. - * @emits {@link BushClientEvents.bushMute} - */ - bushMute(options: BushTimedPunishmentOptions): Promise; - /** - * Unmute the user, create a modlog entry, remove the punishment entry, and dm the user. - * @param options Options for unmuting the user. - * @returns A status message for unmuting the user. - * @emits {@link BushClientEvents.bushUnmute} - */ - bushUnmute(options: BushPunishmentOptions): Promise; - /** - * Kick the user, create a modlog entry, and dm the user. - * @param options Options for kicking the user. - * @returns A status message for kicking the user. - * @emits {@link BushClientEvents.bushKick} - */ - bushKick(options: BushPunishmentOptions): Promise; - /** - * Ban the user, create a modlog entry, create a punishment entry, and dm the user. - * @param options Options for banning the user. - * @returns A status message for banning the user. - * @emits {@link BushClientEvents.bushBan} - */ - bushBan(options: BushBanOptions): Promise>; - /** - * Prevents a user from speaking in a channel. - * @param options Options for blocking the user. - */ - bushBlock(options: BlockOptions): Promise; - /** - * Allows a user to speak in a channel. - * @param options Options for unblocking the user. - */ - bushUnblock(options: UnblockOptions): Promise; - /** - * Mutes a user using discord's timeout feature. - * @param options Options for timing out the user. - */ - bushTimeout(options: BushTimeoutOptions): Promise; - /** - * Removes a timeout from a user. - * @param options Options for removing the timeout. - */ - bushRemoveTimeout(options: BushPunishmentOptions): Promise; - /** - * Whether or not the user is an owner of the bot. - */ - isOwner(): boolean; - /** - * Whether or not the user is a super user of the bot. - */ - isSuperUser(): boolean; - } -} - -/** - * Represents a member of a guild on Discord. - */ -export class ExtendedGuildMember extends GuildMember { - /** - * Send a punishment dm to the user. - * @param punishment The punishment that the user has received. - * @param reason The reason for the user's punishment. - * @param duration The duration of the punishment. - * @param modlog The modlog case id so the user can make an appeal. - * @param sendFooter Whether or not to send the guild's punishment footer with the dm. - * @returns Whether or not the dm was sent successfully. - */ - public override async bushPunishDM( - punishment: PunishmentTypeDM, - reason?: string | null, - duration?: number, - modlog?: string, - sendFooter = true - ): Promise { - return Moderation.punishDM({ - client: this.client, - modlog, - guild: this.guild, - user: this, - punishment, - reason: reason ?? undefined, - duration, - sendFooter - }); - } - - /** - * Warn the user, create a modlog entry, and send a dm to the user. - * @param options Options for warning the user. - * @returns An object with the result of the warning, and the case number of the warn. - * @emits {@link BushClientEvents.bushWarn} - */ - public override async bushWarn(options: BushPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number | null }> { - let caseID: string | undefined = undefined; - let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); - if (!moderator) return { result: warnResponse.CANNOT_RESOLVE_USER, caseNum: null }; - - const ret = await (async (): Promise<{ result: WarnResponse; caseNum: number | null }> => { - // add modlog entry - const result = await Moderation.createModLogEntry( - { - client: this.client, - type: ModLogType.WARN, - user: this, - moderator: moderator.id, - reason: options.reason, - guild: this.guild, - evidence: options.evidence, - hidden: options.silent ?? false - }, - true - ); - caseID = result.log?.id; - if (!result || !result.log) return { result: warnResponse.MODLOG_ERROR, caseNum: null }; - - if (!options.silent) { - // dm user - const dmSuccess = await this.bushPunishDM('warned', options.reason); - dmSuccessEvent = dmSuccess; - if (!dmSuccess) return { result: warnResponse.DM_ERROR, caseNum: result.caseNum }; - } - - return { result: warnResponse.SUCCESS, caseNum: result.caseNum }; - })(); - if (!([warnResponse.MODLOG_ERROR] as const).includes(ret.result) && !options.silent) - this.client.emit('bushWarn', this, moderator, this.guild, options.reason ?? undefined, caseID!, dmSuccessEvent!); - return ret; - } - - /** - * Add a role to the user, if it is a punishment create a modlog entry, and create a punishment entry if it is temporary or a punishment. - * @param options Options for adding a role to the user. - * @returns A status message for adding the add. - * @emits {@link BushClientEvents.bushPunishRole} - */ - public override async bushAddRole(options: AddRoleOptions): Promise { - // checks - if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ManageRoles)) return addRoleResponse.MISSING_PERMISSIONS; - const ifShouldAddRole = this.#checkIfShouldAddRole(options.role, options.moderator); - if (ifShouldAddRole !== true) return ifShouldAddRole; - - let caseID: string | undefined = undefined; - const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); - if (!moderator) return addRoleResponse.CANNOT_RESOLVE_USER; - - const ret = await (async () => { - if (options.addToModlog || options.duration) { - const { log: modlog } = await Moderation.createModLogEntry({ - client: this.client, - type: options.duration ? ModLogType.TEMP_PUNISHMENT_ROLE : ModLogType.PERM_PUNISHMENT_ROLE, - guild: this.guild, - moderator: moderator.id, - user: this, - reason: 'N/A', - pseudo: !options.addToModlog, - evidence: options.evidence, - hidden: options.silent ?? false - }); - - if (!modlog) return addRoleResponse.MODLOG_ERROR; - caseID = modlog.id; - - if (options.addToModlog || options.duration) { - const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ - client: this.client, - type: 'role', - user: this, - guild: this.guild, - modlog: modlog.id, - duration: options.duration, - extraInfo: options.role.id - }); - if (!punishmentEntrySuccess) return addRoleResponse.PUNISHMENT_ENTRY_ADD_ERROR; - } - } - - const removeRoleSuccess = await this.roles.add(options.role, `${moderator.tag}`); - if (!removeRoleSuccess) return addRoleResponse.ACTION_ERROR; - - return addRoleResponse.SUCCESS; - })(); - if ( - !( - [addRoleResponse.ACTION_ERROR, addRoleResponse.MODLOG_ERROR, addRoleResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const - ).includes(ret) && - options.addToModlog && - !options.silent - ) - this.client.emit( - 'bushPunishRole', - this, - moderator, - this.guild, - options.reason ?? undefined, - caseID!, - options.duration ?? 0, - options.role, - options.evidence - ); - return ret; - } - - /** - * Remove a role from the user, if it is a punishment create a modlog entry, and destroy a punishment entry if it was temporary or a punishment. - * @param options Options for removing a role from the user. - * @returns A status message for removing the role. - * @emits {@link BushClientEvents.bushPunishRoleRemove} - */ - public override async bushRemoveRole(options: RemoveRoleOptions): Promise { - // checks - if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ManageRoles)) return removeRoleResponse.MISSING_PERMISSIONS; - const ifShouldAddRole = this.#checkIfShouldAddRole(options.role, options.moderator); - if (ifShouldAddRole !== true) return ifShouldAddRole; - - let caseID: string | undefined = undefined; - const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); - if (!moderator) return removeRoleResponse.CANNOT_RESOLVE_USER; - - const ret = await (async () => { - if (options.addToModlog) { - const { log: modlog } = await Moderation.createModLogEntry({ - client: this.client, - type: ModLogType.REMOVE_PUNISHMENT_ROLE, - guild: this.guild, - moderator: moderator.id, - user: this, - reason: 'N/A', - evidence: options.evidence, - hidden: options.silent ?? false - }); - - if (!modlog) return removeRoleResponse.MODLOG_ERROR; - caseID = modlog.id; - - const punishmentEntrySuccess = await Moderation.removePunishmentEntry({ - client: this.client, - type: 'role', - user: this, - guild: this.guild, - extraInfo: options.role.id - }); - - if (!punishmentEntrySuccess) return removeRoleResponse.PUNISHMENT_ENTRY_REMOVE_ERROR; - } - - const removeRoleSuccess = await this.roles.remove(options.role, `${moderator.tag}`); - if (!removeRoleSuccess) return removeRoleResponse.ACTION_ERROR; - - return removeRoleResponse.SUCCESS; - })(); - - if ( - !( - [ - removeRoleResponse.ACTION_ERROR, - removeRoleResponse.MODLOG_ERROR, - removeRoleResponse.PUNISHMENT_ENTRY_REMOVE_ERROR - ] as const - ).includes(ret) && - options.addToModlog && - !options.silent - ) - this.client.emit( - 'bushPunishRoleRemove', - this, - moderator, - this.guild, - options.reason ?? undefined, - caseID!, - options.role, - options.evidence - ); - return ret; - } - - /** - * Check whether or not a role should be added/removed from the user based on hierarchy. - * @param role The role to check if can be modified. - * @param moderator The moderator that is trying to add/remove the role. - * @returns `true` if the role should be added/removed or a string for the reason why it shouldn't. - */ - #checkIfShouldAddRole( - role: Role | Role, - moderator?: GuildMember - ): true | 'user hierarchy' | 'role managed' | 'client hierarchy' { - if (moderator && moderator.roles.highest.position <= role.position && this.guild.ownerId !== this.user.id) { - return shouldAddRoleResponse.USER_HIERARCHY; - } else if (role.managed) { - return shouldAddRoleResponse.ROLE_MANAGED; - } else if (this.guild.members.me!.roles.highest.position <= role.position) { - return shouldAddRoleResponse.CLIENT_HIERARCHY; - } - return true; - } - - /** - * Mute the user, create a modlog entry, creates a punishment entry, and dms the user. - * @param options Options for muting the user. - * @returns A status message for muting the user. - * @emits {@link BushClientEvents.bushMute} - */ - public override async bushMute(options: BushTimedPunishmentOptions): Promise { - // checks - const checks = await Moderation.checkMutePermissions(this.guild); - if (checks !== true) return checks; - - const muteRoleID = (await this.guild.getSetting('muteRole'))!; - const muteRole = this.guild.roles.cache.get(muteRoleID)!; - - let caseID: string | undefined = undefined; - let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); - if (!moderator) return muteResponse.CANNOT_RESOLVE_USER; - - const ret = await (async () => { - // add role - const muteSuccess = await this.roles - .add(muteRole, `[Mute] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) - .catch(async (e) => { - await this.client.console.warn('muteRoleAddError', e); - this.client.console.debug(e); - return false; - }); - if (!muteSuccess) return muteResponse.ACTION_ERROR; - - // add modlog entry - const { log: modlog } = await Moderation.createModLogEntry({ - client: this.client, - type: options.duration ? ModLogType.TEMP_MUTE : ModLogType.PERM_MUTE, - user: this, - moderator: moderator.id, - reason: options.reason, - duration: options.duration, - guild: this.guild, - evidence: options.evidence, - hidden: options.silent ?? false - }); - - if (!modlog) return muteResponse.MODLOG_ERROR; - caseID = modlog.id; - - // add punishment entry so they can be unmuted later - const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ - client: this.client, - type: 'mute', - user: this, - guild: this.guild, - duration: options.duration, - modlog: modlog.id - }); - - if (!punishmentEntrySuccess) return muteResponse.PUNISHMENT_ENTRY_ADD_ERROR; - - if (!options.silent) { - // dm user - const dmSuccess = await this.bushPunishDM('muted', options.reason, options.duration ?? 0, modlog.id); - dmSuccessEvent = dmSuccess; - if (!dmSuccess) return muteResponse.DM_ERROR; - } - - return muteResponse.SUCCESS; - })(); - - if ( - !([muteResponse.ACTION_ERROR, muteResponse.MODLOG_ERROR, muteResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes(ret) && - !options.silent - ) - this.client.emit( - 'bushMute', - this, - moderator, - this.guild, - options.reason ?? undefined, - caseID!, - options.duration ?? 0, - dmSuccessEvent!, - options.evidence - ); - return ret; - } - - /** - * Unmute the user, create a modlog entry, remove the punishment entry, and dm the user. - * @param options Options for unmuting the user. - * @returns A status message for unmuting the user. - * @emits {@link BushClientEvents.bushUnmute} - */ - public override async bushUnmute(options: BushPunishmentOptions): Promise { - // checks - const checks = await Moderation.checkMutePermissions(this.guild); - if (checks !== true) return checks; - - const muteRoleID = (await this.guild.getSetting('muteRole'))!; - const muteRole = this.guild.roles.cache.get(muteRoleID)!; - - let caseID: string | undefined = undefined; - let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); - if (!moderator) return unmuteResponse.CANNOT_RESOLVE_USER; - - const ret = await (async () => { - // remove role - const muteSuccess = await this.roles - .remove(muteRole, `[Unmute] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) - .catch(async (e) => { - await this.client.console.warn('muteRoleAddError', formatError(e, true)); - return false; - }); - if (!muteSuccess) return unmuteResponse.ACTION_ERROR; - - // add modlog entry - const { log: modlog } = await Moderation.createModLogEntry({ - client: this.client, - type: ModLogType.UNMUTE, - user: this, - moderator: moderator.id, - reason: options.reason, - guild: this.guild, - evidence: options.evidence, - hidden: options.silent ?? false - }); - - if (!modlog) return unmuteResponse.MODLOG_ERROR; - caseID = modlog.id; - - // remove mute entry - const removePunishmentEntrySuccess = await Moderation.removePunishmentEntry({ - client: this.client, - type: 'mute', - user: this, - guild: this.guild - }); - - if (!removePunishmentEntrySuccess) return unmuteResponse.PUNISHMENT_ENTRY_REMOVE_ERROR; - - if (!options.silent) { - // dm user - const dmSuccess = await this.bushPunishDM('unmuted', options.reason, undefined, '', false); - dmSuccessEvent = dmSuccess; - if (!dmSuccess) return unmuteResponse.DM_ERROR; - } - - return unmuteResponse.SUCCESS; - })(); - - if ( - !( - [unmuteResponse.ACTION_ERROR, unmuteResponse.MODLOG_ERROR, unmuteResponse.PUNISHMENT_ENTRY_REMOVE_ERROR] as const - ).includes(ret) && - !options.silent - ) - this.client.emit( - 'bushUnmute', - this, - moderator, - this.guild, - options.reason ?? undefined, - caseID!, - dmSuccessEvent!, - options.evidence - ); - return ret; - } - - /** - * Kick the user, create a modlog entry, and dm the user. - * @param options Options for kicking the user. - * @returns A status message for kicking the user. - * @emits {@link BushClientEvents.bushKick} - */ - public override async bushKick(options: BushPunishmentOptions): Promise { - // checks - if (!this.guild.members.me?.permissions.has(PermissionFlagsBits.KickMembers) || !this.kickable) - return kickResponse.MISSING_PERMISSIONS; - - let caseID: string | undefined = undefined; - let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); - if (!moderator) return kickResponse.CANNOT_RESOLVE_USER; - const ret = await (async () => { - // add modlog entry - const { log: modlog } = await Moderation.createModLogEntry({ - client: this.client, - type: ModLogType.KICK, - user: this, - moderator: moderator.id, - reason: options.reason, - guild: this.guild, - evidence: options.evidence, - hidden: options.silent ?? false - }); - if (!modlog) return kickResponse.MODLOG_ERROR; - caseID = modlog.id; - - // dm user - const dmSuccess = options.silent ? null : await this.bushPunishDM('kicked', options.reason, undefined, modlog.id); - dmSuccessEvent = dmSuccess ?? undefined; - - // kick - const kickSuccess = await this.kick(`${moderator?.tag} | ${options.reason ?? 'No reason provided.'}`).catch(() => false); - if (!kickSuccess) return kickResponse.ACTION_ERROR; - - if (dmSuccess === false) return kickResponse.DM_ERROR; - return kickResponse.SUCCESS; - })(); - if (!([kickResponse.ACTION_ERROR, kickResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent) - this.client.emit( - 'bushKick', - this, - moderator, - this.guild, - options.reason ?? undefined, - caseID!, - dmSuccessEvent!, - options.evidence - ); - return ret; - } - - /** - * Ban the user, create a modlog entry, create a punishment entry, and dm the user. - * @param options Options for banning the user. - * @returns A status message for banning the user. - * @emits {@link BushClientEvents.bushBan} - */ - public override async bushBan(options: BushBanOptions): Promise> { - // checks - if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.BanMembers) || !this.bannable) - return banResponse.MISSING_PERMISSIONS; - - let caseID: string | undefined = undefined; - let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); - if (!moderator) return banResponse.CANNOT_RESOLVE_USER; - - // ignore result, they should still be banned even if their mute cannot be removed - await this.bushUnmute({ - reason: 'User is about to be banned, a mute is no longer necessary.', - moderator: this.guild.members.me!, - silent: true - }); - - const ret = await (async () => { - // add modlog entry - const { log: modlog } = await Moderation.createModLogEntry({ - client: this.client, - type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, - user: this, - moderator: moderator.id, - reason: options.reason, - duration: options.duration, - guild: this.guild, - evidence: options.evidence, - hidden: options.silent ?? false - }); - if (!modlog) return banResponse.MODLOG_ERROR; - caseID = modlog.id; - - // dm user - const dmSuccess = options.silent - ? null - : await this.bushPunishDM('banned', options.reason, options.duration ?? 0, modlog.id); - dmSuccessEvent = dmSuccess ?? undefined; - - // ban - const banSuccess = await this.ban({ - reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, - deleteMessageDays: options.deleteDays - }).catch(() => false); - if (!banSuccess) return banResponse.ACTION_ERROR; - - // add punishment entry so they can be unbanned later - const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ - client: this.client, - type: 'ban', - user: this, - guild: this.guild, - duration: options.duration, - modlog: modlog.id - }); - if (!punishmentEntrySuccess) return banResponse.PUNISHMENT_ENTRY_ADD_ERROR; - - if (!dmSuccess) return banResponse.DM_ERROR; - return banResponse.SUCCESS; - })(); - if ( - !([banResponse.ACTION_ERROR, banResponse.MODLOG_ERROR, banResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes(ret) && - !options.silent - ) - this.client.emit( - 'bushBan', - this, - moderator, - this.guild, - options.reason ?? undefined, - caseID!, - options.duration ?? 0, - dmSuccessEvent!, - options.evidence - ); - return ret; - } - - /** - * Prevents a user from speaking in a channel. - * @param options Options for blocking the user. - */ - public override async bushBlock(options: BlockOptions): Promise { - const channel = this.guild.channels.resolve(options.channel); - if (!channel || (!channel.isTextBased() && !channel.isThread())) return blockResponse.INVALID_CHANNEL; - - // checks - if (!channel.permissionsFor(this.guild.members.me!)!.has(PermissionFlagsBits.ManageChannels)) - return blockResponse.MISSING_PERMISSIONS; - - let caseID: string | undefined = undefined; - let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); - if (!moderator) return blockResponse.CANNOT_RESOLVE_USER; - - const ret = await (async () => { - // change channel permissions - const channelToUse = channel.isThread() ? channel.parent! : channel; - const perm = channel.isThread() ? { SendMessagesInThreads: false } : { SendMessages: false }; - const blockSuccess = await channelToUse.permissionOverwrites - .edit(this, perm, { reason: `[Block] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}` }) - .catch(() => false); - if (!blockSuccess) return blockResponse.ACTION_ERROR; - - // add modlog entry - const { log: modlog } = await Moderation.createModLogEntry({ - client: this.client, - type: options.duration ? ModLogType.TEMP_CHANNEL_BLOCK : ModLogType.PERM_CHANNEL_BLOCK, - user: this, - moderator: moderator.id, - reason: options.reason, - guild: this.guild, - evidence: options.evidence, - hidden: options.silent ?? false - }); - if (!modlog) return blockResponse.MODLOG_ERROR; - caseID = modlog.id; - - // add punishment entry so they can be unblocked later - const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ - client: this.client, - type: 'block', - user: this, - guild: this.guild, - duration: options.duration, - modlog: modlog.id, - extraInfo: channel.id - }); - if (!punishmentEntrySuccess) return blockResponse.PUNISHMENT_ENTRY_ADD_ERROR; - - // dm user - const dmSuccess = options.silent - ? null - : await Moderation.punishDM({ - client: this.client, - punishment: 'blocked', - reason: options.reason ?? undefined, - duration: options.duration ?? 0, - modlog: modlog.id, - guild: this.guild, - user: this, - sendFooter: true, - channel: channel.id - }); - dmSuccessEvent = !!dmSuccess; - if (!dmSuccess) return blockResponse.DM_ERROR; - - return blockResponse.SUCCESS; - })(); - - if ( - !([blockResponse.ACTION_ERROR, blockResponse.MODLOG_ERROR, blockResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes( - ret - ) && - !options.silent - ) - this.client.emit( - 'bushBlock', - this, - moderator, - this.guild, - options.reason ?? undefined, - caseID!, - options.duration ?? 0, - dmSuccessEvent!, - channel, - options.evidence - ); - return ret; - } - - /** - * Allows a user to speak in a channel. - * @param options Options for unblocking the user. - */ - public override async bushUnblock(options: UnblockOptions): Promise { - const _channel = this.guild.channels.resolve(options.channel); - if (!_channel || (_channel.type !== ChannelType.GuildText && !_channel.isThread())) return unblockResponse.INVALID_CHANNEL; - const channel = _channel as GuildTextBasedChannel; - - // checks - if (!channel.permissionsFor(this.guild.members.me!)!.has(PermissionFlagsBits.ManageChannels)) - return unblockResponse.MISSING_PERMISSIONS; - - let caseID: string | undefined = undefined; - let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); - if (!moderator) return unblockResponse.CANNOT_RESOLVE_USER; - - const ret = await (async () => { - // change channel permissions - const channelToUse = channel.isThread() ? channel.parent! : channel; - const perm = channel.isThread() ? { SendMessagesInThreads: null } : { SendMessages: null }; - const blockSuccess = await channelToUse.permissionOverwrites - .edit(this, perm, { reason: `[Unblock] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}` }) - .catch(() => false); - if (!blockSuccess) return unblockResponse.ACTION_ERROR; - - // add modlog entry - const { log: modlog } = await Moderation.createModLogEntry({ - client: this.client, - type: ModLogType.CHANNEL_UNBLOCK, - user: this, - moderator: moderator.id, - reason: options.reason, - guild: this.guild, - evidence: options.evidence, - hidden: options.silent ?? false - }); - if (!modlog) return unblockResponse.MODLOG_ERROR; - caseID = modlog.id; - - // remove punishment entry - const punishmentEntrySuccess = await Moderation.removePunishmentEntry({ - client: this.client, - type: 'block', - user: this, - guild: this.guild, - extraInfo: channel.id - }); - if (!punishmentEntrySuccess) return unblockResponse.ACTION_ERROR; - - // dm user - const dmSuccess = options.silent - ? null - : await Moderation.punishDM({ - client: this.client, - punishment: 'unblocked', - reason: options.reason ?? undefined, - guild: this.guild, - user: this, - sendFooter: false, - channel: channel.id - }); - dmSuccessEvent = !!dmSuccess; - if (!dmSuccess) return blockResponse.DM_ERROR; - - dmSuccessEvent = !!dmSuccess; - if (!dmSuccess) return unblockResponse.DM_ERROR; - - return unblockResponse.SUCCESS; - })(); - - if ( - !([unblockResponse.ACTION_ERROR, unblockResponse.MODLOG_ERROR, unblockResponse.ACTION_ERROR] as const).includes(ret) && - !options.silent - ) - this.client.emit( - 'bushUnblock', - this, - moderator, - this.guild, - options.reason ?? undefined, - caseID!, - dmSuccessEvent!, - channel, - options.evidence - ); - return ret; - } - - /** - * Mutes a user using discord's timeout feature. - * @param options Options for timing out the user. - */ - public override async bushTimeout(options: BushTimeoutOptions): Promise { - // checks - if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ModerateMembers)) return timeoutResponse.MISSING_PERMISSIONS; - - const twentyEightDays = Time.Day * 28; - if (options.duration > twentyEightDays) return timeoutResponse.INVALID_DURATION; - - let caseID: string | undefined = undefined; - let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); - if (!moderator) return timeoutResponse.CANNOT_RESOLVE_USER; - - const ret = await (async () => { - // timeout - const timeoutSuccess = await this.timeout( - options.duration, - `${moderator.tag} | ${options.reason ?? 'No reason provided.'}` - ).catch(() => false); - if (!timeoutSuccess) return timeoutResponse.ACTION_ERROR; - - // add modlog entry - const { log: modlog } = await Moderation.createModLogEntry({ - client: this.client, - type: ModLogType.TIMEOUT, - user: this, - moderator: moderator.id, - reason: options.reason, - duration: options.duration, - guild: this.guild, - evidence: options.evidence, - hidden: options.silent ?? false - }); - - if (!modlog) return timeoutResponse.MODLOG_ERROR; - caseID = modlog.id; - - if (!options.silent) { - // dm user - const dmSuccess = await this.bushPunishDM('timedout', options.reason, options.duration, modlog.id); - dmSuccessEvent = dmSuccess; - if (!dmSuccess) return timeoutResponse.DM_ERROR; - } - - return timeoutResponse.SUCCESS; - })(); - - if (!([timeoutResponse.ACTION_ERROR, timeoutResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent) - this.client.emit( - 'bushTimeout', - this, - moderator, - this.guild, - options.reason ?? undefined, - caseID!, - options.duration ?? 0, - dmSuccessEvent!, - options.evidence - ); - return ret; - } - - /** - * Removes a timeout from a user. - * @param options Options for removing the timeout. - */ - public override async bushRemoveTimeout(options: BushPunishmentOptions): Promise { - // checks - if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ModerateMembers)) - return removeTimeoutResponse.MISSING_PERMISSIONS; - - let caseID: string | undefined = undefined; - let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); - if (!moderator) return removeTimeoutResponse.CANNOT_RESOLVE_USER; - - const ret = await (async () => { - // remove timeout - const timeoutSuccess = await this.timeout(null, `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`).catch( - () => false - ); - if (!timeoutSuccess) return removeTimeoutResponse.ACTION_ERROR; - - // add modlog entry - const { log: modlog } = await Moderation.createModLogEntry({ - client: this.client, - type: ModLogType.REMOVE_TIMEOUT, - user: this, - moderator: moderator.id, - reason: options.reason, - guild: this.guild, - evidence: options.evidence, - hidden: options.silent ?? false - }); - - if (!modlog) return removeTimeoutResponse.MODLOG_ERROR; - caseID = modlog.id; - - if (!options.silent) { - // dm user - const dmSuccess = await this.bushPunishDM('untimedout', options.reason, undefined, '', false); - dmSuccessEvent = dmSuccess; - if (!dmSuccess) return removeTimeoutResponse.DM_ERROR; - } - - return removeTimeoutResponse.SUCCESS; - })(); - - if (!([removeTimeoutResponse.ACTION_ERROR, removeTimeoutResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent) - this.client.emit( - 'bushRemoveTimeout', - this, - moderator, - this.guild, - options.reason ?? undefined, - caseID!, - dmSuccessEvent!, - options.evidence - ); - return ret; - } - - /** - * Whether or not the user is an owner of the bot. - */ - public override isOwner(): boolean { - return this.client.isOwner(this); - } - - /** - * Whether or not the user is a super user of the bot. - */ - public override isSuperUser(): boolean { - return this.client.isSuperUser(this); - } -} - -/** - * Options for punishing a user. - */ -export interface BushPunishmentOptions { - /** - * The reason for the punishment. - */ - reason?: string | null; - - /** - * The moderator who punished the user. - */ - moderator?: GuildMember; - - /** - * Evidence for the punishment. - */ - evidence?: string; - - /** - * Makes the punishment silent by not sending the user a punishment dm and not broadcasting the event to be logged. - */ - silent?: boolean; -} - -/** - * Punishment options for punishments that can be temporary. - */ -export interface BushTimedPunishmentOptions extends BushPunishmentOptions { - /** - * The duration of the punishment. - */ - duration?: number; -} - -/** - * Options for a role add punishment. - */ -export interface AddRoleOptions extends BushTimedPunishmentOptions { - /** - * The role to add to the user. - */ - role: Role; - - /** - * Whether to create a modlog entry for this punishment. - */ - addToModlog: boolean; -} - -/** - * Options for a role remove punishment. - */ -export interface RemoveRoleOptions extends BushTimedPunishmentOptions { - /** - * The role to remove from the user. - */ - role: Role; - - /** - * Whether to create a modlog entry for this punishment. - */ - addToModlog: boolean; -} - -/** - * Options for banning a user. - */ -export interface BushBanOptions extends BushTimedPunishmentOptions { - /** - * The number of days to delete the user's messages for. - */ - deleteDays?: number; -} - -/** - * Options for blocking a user from a channel. - */ -export interface BlockOptions extends BushTimedPunishmentOptions { - /** - * The channel to block the user from. - */ - channel: GuildChannelResolvable; -} - -/** - * Options for unblocking a user from a channel. - */ -export interface UnblockOptions extends BushPunishmentOptions { - /** - * The channel to unblock the user from. - */ - channel: GuildChannelResolvable; -} - -/** - * Punishment options for punishments that can be temporary. - */ -export interface BushTimeoutOptions extends BushPunishmentOptions { - /** - * The duration of the punishment. - */ - duration: number; -} - -export const basePunishmentResponse = Object.freeze({ - SUCCESS: 'success', - MODLOG_ERROR: 'error creating modlog entry', - ACTION_ERROR: 'error performing action', - CANNOT_RESOLVE_USER: 'cannot resolve user' -} as const); - -export const dmResponse = Object.freeze({ - ...basePunishmentResponse, - DM_ERROR: 'failed to dm' -} as const); - -export const permissionsResponse = Object.freeze({ - MISSING_PERMISSIONS: 'missing permissions' -} as const); - -export const punishmentEntryAdd = Object.freeze({ - PUNISHMENT_ENTRY_ADD_ERROR: 'error creating punishment entry' -} as const); - -export const punishmentEntryRemove = Object.freeze({ - PUNISHMENT_ENTRY_REMOVE_ERROR: 'error removing punishment entry' -} as const); - -export const shouldAddRoleResponse = Object.freeze({ - USER_HIERARCHY: 'user hierarchy', - CLIENT_HIERARCHY: 'client hierarchy', - ROLE_MANAGED: 'role managed' -} as const); - -export const baseBlockResponse = Object.freeze({ - INVALID_CHANNEL: 'invalid channel' -} as const); - -export const baseMuteResponse = Object.freeze({ - NO_MUTE_ROLE: 'no mute role', - MUTE_ROLE_INVALID: 'invalid mute role', - MUTE_ROLE_NOT_MANAGEABLE: 'mute role not manageable' -} as const); - -export const warnResponse = Object.freeze({ - ...dmResponse -} as const); - -export const addRoleResponse = Object.freeze({ - ...basePunishmentResponse, - ...permissionsResponse, - ...shouldAddRoleResponse, - ...punishmentEntryAdd -} as const); - -export const removeRoleResponse = Object.freeze({ - ...basePunishmentResponse, - ...permissionsResponse, - ...shouldAddRoleResponse, - ...punishmentEntryRemove -} as const); - -export const muteResponse = Object.freeze({ - ...dmResponse, - ...permissionsResponse, - ...baseMuteResponse, - ...punishmentEntryAdd -} as const); - -export const unmuteResponse = Object.freeze({ - ...dmResponse, - ...permissionsResponse, - ...baseMuteResponse, - ...punishmentEntryRemove -} as const); - -export const kickResponse = Object.freeze({ - ...dmResponse, - ...permissionsResponse -} as const); - -export const banResponse = Object.freeze({ - ...dmResponse, - ...permissionsResponse, - ...punishmentEntryAdd, - ALREADY_BANNED: 'already banned' -} as const); - -export const blockResponse = Object.freeze({ - ...dmResponse, - ...permissionsResponse, - ...baseBlockResponse, - ...punishmentEntryAdd -}); - -export const unblockResponse = Object.freeze({ - ...dmResponse, - ...permissionsResponse, - ...baseBlockResponse, - ...punishmentEntryRemove -}); - -export const timeoutResponse = Object.freeze({ - ...dmResponse, - ...permissionsResponse, - INVALID_DURATION: 'duration too long' -} as const); - -export const removeTimeoutResponse = Object.freeze({ - ...dmResponse, - ...permissionsResponse -} as const); - -/** - * Response returned when warning a user. - */ -export type WarnResponse = ValueOf; - -/** - * Response returned when adding a role to a user. - */ -export type AddRoleResponse = ValueOf; - -/** - * Response returned when removing a role from a user. - */ -export type RemoveRoleResponse = ValueOf; - -/** - * Response returned when muting a user. - */ -export type MuteResponse = ValueOf; - -/** - * Response returned when unmuting a user. - */ -export type UnmuteResponse = ValueOf; - -/** - * Response returned when kicking a user. - */ -export type KickResponse = ValueOf; - -/** - * Response returned when banning a user. - */ -export type BanResponse = ValueOf; - -/** - * Response returned when blocking a user. - */ -export type BlockResponse = ValueOf; - -/** - * Response returned when unblocking a user. - */ -export type UnblockResponse = ValueOf; - -/** - * Response returned when timing out a user. - */ -export type TimeoutResponse = ValueOf; - -/** - * Response returned when removing a timeout from a user. - */ -export type RemoveTimeoutResponse = ValueOf; - -/** - * @typedef {BushClientEvents} VSCodePleaseDontRemove - */ diff --git a/src/lib/extensions/discord.js/ExtendedMessage.ts b/src/lib/extensions/discord.js/ExtendedMessage.ts deleted file mode 100644 index 4748803..0000000 --- a/src/lib/extensions/discord.js/ExtendedMessage.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { CommandUtil } from 'discord-akairo'; -import { Message, type Client } from 'discord.js'; -import { type RawMessageData } from 'discord.js/typings/rawDataTypes.js'; - -export class ExtendedMessage extends Message { - public declare util: CommandUtil; - - public constructor(client: Client, data: RawMessageData) { - super(client, data); - this.util = new CommandUtil(client.commandHandler, this); - } -} diff --git a/src/lib/extensions/discord.js/ExtendedUser.ts b/src/lib/extensions/discord.js/ExtendedUser.ts deleted file mode 100644 index 23de523..0000000 --- a/src/lib/extensions/discord.js/ExtendedUser.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { User, type Partialize } from 'discord.js'; - -declare module 'discord.js' { - export interface User { - /** - * Indicates whether the user is an owner of the bot. - */ - isOwner(): boolean; - /** - * Indicates whether the user is a superuser of the bot. - */ - isSuperUser(): boolean; - } -} - -export type PartialBushUser = Partialize; - -/** - * Represents a user on Discord. - */ -export class ExtendedUser extends User { - /** - * Indicates whether the user is an owner of the bot. - */ - public override isOwner(): boolean { - return this.client.isOwner(this); - } - - /** - * Indicates whether the user is a superuser of the bot. - */ - public override isSuperUser(): boolean { - return this.client.isSuperUser(this); - } -} diff --git a/src/lib/extensions/global.ts b/src/lib/extensions/global.ts deleted file mode 100644 index a9020d7..0000000 --- a/src/lib/extensions/global.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable no-var */ -declare global { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface ReadonlyArray { - includes}`>( - this: ReadonlyArray, - searchElement: S, - fromIndex?: number - ): searchElement is R & S; - } -} - -export {}; diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index 3e57f9e..0000000 --- a/src/lib/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -export * from './common/AutoMod.js'; -export * from './common/ButtonPaginator.js'; -export * from './common/ConfirmationPrompt.js'; -export * from './common/DeleteButton.js'; -export type { BushInspectOptions } from './common/typings/BushInspectOptions.js'; -export type { CodeBlockLang } from './common/typings/CodeBlockLang.js'; -export * as Arg from './common/util/Arg.js'; -export * as Format from './common/util/Format.js'; -export * as Moderation from './common/util/Moderation.js'; -export type { - AppealButtonId, - CreateModLogEntryOptions, - CreatePunishmentEntryOptions, - PunishDMOptions, - PunishmentTypeDM, - PunishmentTypePresent, - RemovePunishmentEntryOptions, - SimpleCreateModLogEntryOptions -} from './common/util/Moderation.js'; -export * from './extensions/discord-akairo/BushArgumentTypeCaster.js'; -export * from './extensions/discord-akairo/BushClient.js'; -export * from './extensions/discord-akairo/BushCommand.js'; -export * from './extensions/discord-akairo/BushCommandHandler.js'; -export * from './extensions/discord-akairo/BushInhibitor.js'; -export * from './extensions/discord-akairo/BushInhibitorHandler.js'; -export * from './extensions/discord-akairo/BushListener.js'; -export * from './extensions/discord-akairo/BushListenerHandler.js'; -export * from './extensions/discord-akairo/BushTask.js'; -export * from './extensions/discord-akairo/BushTaskHandler.js'; -export * from './extensions/discord-akairo/SlashMessage.js'; -export type { BushClientEvents } from './extensions/discord.js/BushClientEvents.js'; -export * from './extensions/discord.js/ExtendedGuild.js'; -export * from './extensions/discord.js/ExtendedGuildMember.js'; -export * from './extensions/discord.js/ExtendedMessage.js'; -export * from './extensions/discord.js/ExtendedUser.js'; -export * from './models/BaseModel.js'; -export * from './models/instance/ActivePunishment.js'; -export * from './models/instance/Guild.js'; -export * from './models/instance/Highlight.js'; -export * from './models/instance/Level.js'; -export * from './models/instance/ModLog.js'; -export * from './models/instance/Reminder.js'; -export * from './models/instance/StickyRole.js'; -export * from './models/shared/Global.js'; -export * from './models/shared/MemberCount.js'; -export * from './models/shared/Shared.js'; -export * from './models/shared/Stat.js'; -export * from './utils/AllowedMentions.js'; -export * from './utils/BushCache.js'; -export * from './utils/BushConstants.js'; -export * from './utils/BushLogger.js'; -export * from './utils/BushUtils.js'; -export * from './utils/CanvasProgressBar.js'; diff --git a/src/lib/models/BaseModel.ts b/src/lib/models/BaseModel.ts deleted file mode 100644 index e503317..0000000 --- a/src/lib/models/BaseModel.ts +++ /dev/null @@ -1,13 +0,0 @@ -const { Model } = (await import('sequelize')).default; - -export abstract class BaseModel extends Model { - /** - * The date when the row was created. - */ - public declare readonly createdAt: Date; - - /** - * The date when the row was last updated. - */ - public declare readonly updatedAt: Date; -} diff --git a/src/lib/models/instance/ActivePunishment.ts b/src/lib/models/instance/ActivePunishment.ts deleted file mode 100644 index 38012ca..0000000 --- a/src/lib/models/instance/ActivePunishment.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { type Snowflake } from 'discord.js'; -import { nanoid } from 'nanoid'; -import { type Sequelize } from 'sequelize'; -import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; - -export enum ActivePunishmentType { - BAN = 'BAN', - MUTE = 'MUTE', - ROLE = 'ROLE', - BLOCK = 'BLOCK' -} - -export interface ActivePunishmentModel { - id: string; - type: ActivePunishmentType; - user: Snowflake; - guild: Snowflake; - extraInfo: Snowflake; - expires: Date | null; - modlog: string; -} - -export interface ActivePunishmentModelCreationAttributes { - id?: string; - type: ActivePunishmentType; - user: Snowflake; - guild: Snowflake; - extraInfo?: Snowflake; - expires?: Date; - modlog: string; -} - -/** - * Keeps track of active punishments so they can be removed later. - */ -export class ActivePunishment - extends BaseModel - implements ActivePunishmentModel -{ - /** - * The ID of this punishment (no real use just for a primary key) - */ - public declare id: string; - - /** - * The type of punishment. - */ - public declare type: ActivePunishmentType; - - /** - * The user who is punished. - */ - public declare user: Snowflake; - - /** - * The guild they are punished in. - */ - public declare guild: Snowflake; - - /** - * Additional info about the punishment if applicable. The channel id for channel blocks and role for punishment roles. - */ - public declare extraInfo: Snowflake; - - /** - * The date when this punishment expires (optional). - */ - public declare expires: Date | null; - - /** - * The reference to the modlog entry. - */ - public declare modlog: string; - - /** - * Initializes the model. - * @param sequelize The sequelize instance. - */ - public static initModel(sequelize: Sequelize): void { - ActivePunishment.init( - { - id: { type: DataTypes.STRING, primaryKey: true, defaultValue: nanoid }, - type: { type: DataTypes.STRING, allowNull: false }, - user: { type: DataTypes.STRING, allowNull: false }, - guild: { type: DataTypes.STRING, allowNull: false, references: { model: 'Guilds', key: 'id' } }, - extraInfo: { type: DataTypes.STRING, allowNull: true }, - expires: { type: DataTypes.DATE, allowNull: true }, - modlog: { type: DataTypes.STRING, allowNull: true, references: { model: 'ModLogs', key: 'id' } } - }, - { sequelize } - ); - } -} diff --git a/src/lib/models/instance/Guild.ts b/src/lib/models/instance/Guild.ts deleted file mode 100644 index f0cac74..0000000 --- a/src/lib/models/instance/Guild.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { ChannelType, Constants, type Snowflake } from 'discord.js'; -import { type Sequelize } from 'sequelize'; -import { BadWordDetails } from '../../common/AutoMod.js'; -import { type BushClient } from '../../extensions/discord-akairo/BushClient.js'; -import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; - -export interface GuildModel { - id: Snowflake; - prefix: string; - autoPublishChannels: Snowflake[]; - blacklistedChannels: Snowflake[]; - blacklistedUsers: Snowflake[]; - welcomeChannel: Snowflake | null; - muteRole: Snowflake | null; - punishmentEnding: string | null; - disabledCommands: string[]; - lockdownChannels: Snowflake[]; - autoModPhases: BadWordDetails[]; - enabledFeatures: GuildFeatures[]; - joinRoles: Snowflake[]; - logChannels: LogChannelDB; - bypassChannelBlacklist: Snowflake[]; - noXpChannels: Snowflake[]; - levelRoles: { [level: number]: Snowflake }; - levelUpChannel: Snowflake | null; -} - -export interface GuildModelCreationAttributes { - id: Snowflake; - prefix?: string; - autoPublishChannels?: Snowflake[]; - blacklistedChannels?: Snowflake[]; - blacklistedUsers?: Snowflake[]; - welcomeChannel?: Snowflake; - muteRole?: Snowflake; - punishmentEnding?: string; - disabledCommands?: string[]; - lockdownChannels?: Snowflake[]; - autoModPhases?: BadWordDetails[]; - enabledFeatures?: GuildFeatures[]; - joinRoles?: Snowflake[]; - logChannels?: LogChannelDB; - bypassChannelBlacklist?: Snowflake[]; - noXpChannels?: Snowflake[]; - levelRoles?: { [level: number]: Snowflake }; - levelUpChannel?: Snowflake; -} - -/** - * Settings for a guild. - */ -export class Guild extends BaseModel implements GuildModel { - /** - * The ID of the guild - */ - public declare id: Snowflake; - - /** - * The bot's prefix for the guild - */ - public declare prefix: string; - - /** - * Channels that will have their messages automatically published - */ - public declare autoPublishChannels: Snowflake[]; - - /** - * Channels where the bot won't respond in. - */ - public declare blacklistedChannels: Snowflake[]; - - /** - * Users that the bot ignores in this guild - */ - public declare blacklistedUsers: Snowflake[]; - - /** - * The channels where the welcome messages are sent - */ - public declare welcomeChannel: Snowflake | null; - - /** - * The role given out when muting someone - */ - public declare muteRole: Snowflake | null; - - /** - * The message that gets sent after someone gets a punishment dm - */ - public declare punishmentEnding: string | null; - - /** - * Guild specific disabled commands - */ - public declare disabledCommands: string[]; - - /** - * Channels that should get locked down when the lockdown command gets used. - */ - public declare lockdownChannels: Snowflake[]; - - /** - * Custom automod phases - */ - public declare autoModPhases: BadWordDetails[]; - - /** - * The features enabled in a guild - */ - public declare enabledFeatures: GuildFeatures[]; - - /** - * The roles to assign to a user if they are not assigned sticky roles - */ - public declare joinRoles: Snowflake[]; - - /** - * The channels where logging messages will be sent. - */ - public declare logChannels: LogChannelDB; - - /** - * These users will be able to use commands in channels blacklisted - */ - public declare bypassChannelBlacklist: Snowflake[]; - - /** - * Channels where users will not earn xp for leveling. - */ - public declare noXpChannels: Snowflake[]; - - /** - * What roles get given to users when they reach certain levels. - */ - public declare levelRoles: { [level: number]: Snowflake }; - - /** - * The channel to send level up messages in instead of last channel. - */ - public declare levelUpChannel: Snowflake | null; - - /** - * Initializes the model. - * @param sequelize The sequelize instance. - */ - public static initModel(sequelize: Sequelize, client: BushClient): void { - Guild.init( - { - id: { type: DataTypes.STRING, primaryKey: true }, - prefix: { type: DataTypes.TEXT, allowNull: false, defaultValue: client.config.prefix }, - autoPublishChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - blacklistedChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - blacklistedUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - welcomeChannel: { type: DataTypes.STRING, allowNull: true }, - muteRole: { type: DataTypes.STRING, allowNull: true }, - punishmentEnding: { type: DataTypes.TEXT, allowNull: true }, - disabledCommands: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - lockdownChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - autoModPhases: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - enabledFeatures: { - type: DataTypes.JSONB, - allowNull: false, - defaultValue: Object.keys(guildFeaturesObj).filter( - (key) => guildFeaturesObj[key as keyof typeof guildFeaturesObj].default - ) - }, - joinRoles: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - logChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: {} }, - bypassChannelBlacklist: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - noXpChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - levelRoles: { type: DataTypes.JSONB, allowNull: false, defaultValue: {} }, - levelUpChannel: { type: DataTypes.STRING, allowNull: true } - }, - { sequelize } - ); - } -} - -export type BaseGuildSetting = 'channel' | 'role' | 'user'; -export type GuildNoArraySetting = 'string' | 'custom' | BaseGuildSetting; -export type GuildSettingType = GuildNoArraySetting | `${BaseGuildSetting}-array`; - -export interface GuildSetting { - name: string; - description: string; - type: GuildSettingType; - subType: ChannelType[] | undefined; - configurable: boolean; - replaceNullWith: () => string | null; -} -const asGuildSetting = (et: { [K in keyof T]: PartialBy }) => { - for (const key in et) { - et[key].subType ??= undefined; - et[key].configurable ??= true; - et[key].replaceNullWith ??= () => null; - } - return et as { [K in keyof T]: GuildSetting }; -}; - -const { default: config } = await import('../../../../config/options.js'); - -export const guildSettingsObj = asGuildSetting({ - prefix: { - name: 'Prefix', - description: 'The phrase required to trigger text commands in this server.', - type: 'string', - replaceNullWith: () => config.prefix - }, - autoPublishChannels: { - name: 'Auto Publish Channels', - description: 'Channels were every message is automatically published.', - type: 'channel-array', - subType: [ChannelType.GuildNews] - }, - welcomeChannel: { - name: 'Welcome Channel', - description: 'The channel where the bot will send join and leave message.', - type: 'channel', - subType: [ - ChannelType.GuildText, - ChannelType.GuildNews, - ChannelType.GuildNewsThread, - ChannelType.GuildPublicThread, - ChannelType.GuildPrivateThread - ] - }, - muteRole: { - name: 'Mute Role', - description: 'The role assigned when muting someone.', - type: 'role' - }, - punishmentEnding: { - name: 'Punishment Ending', - description: 'The message after punishment information to a user in a dm.', - type: 'string' - }, - lockdownChannels: { - name: 'Lockdown Channels', - description: 'Channels that are locked down when a mass lockdown is specified.', - type: 'channel-array', - subType: [ChannelType.GuildText] - }, - joinRoles: { - name: 'Join Roles', - description: 'Roles assigned to users on join who do not have sticky role information.', - type: 'role-array' - }, - bypassChannelBlacklist: { - name: 'Bypass Channel Blacklist', - description: 'These users will be able to use commands in channels blacklisted.', - type: 'user-array' - }, - logChannels: { - name: 'Log Channels', - description: 'The channel were logs are sent.', - type: 'custom', - subType: [ChannelType.GuildText], - configurable: false - }, - autoModPhases: { - name: 'Automod Phases', - description: 'Custom phrases to be detected by automod.', - type: 'custom', - configurable: false - }, - noXpChannels: { - name: 'No Xp Channels', - description: 'Channels where users will not earn xp for leveling.', - type: 'channel-array', - subType: Constants.TextBasedChannelTypes.filter((type) => type !== ChannelType.DM) - }, - levelRoles: { - name: 'Level Roles', - description: 'What roles get given to users when they reach certain levels.', - type: 'custom', - configurable: false - }, - levelUpChannel: { - name: 'Level Up Channel', - description: 'The channel to send level up messages in instead of last channel.', - type: 'channel', - subType: Constants.TextBasedChannelTypes.filter((type) => type !== ChannelType.DM) - } -}); - -export type GuildSettings = keyof typeof guildSettingsObj; -export const settingsArr = Object.keys(guildSettingsObj).filter( - (s) => guildSettingsObj[s as GuildSettings].configurable -) as GuildSettings[]; - -interface GuildFeature { - name: string; - description: string; - default: boolean; - hidden: boolean; -} - -type PartialBy = Omit & Partial>; - -const asGuildFeature = (gf: { [K in keyof T]: PartialBy }): { - [K in keyof T]: GuildFeature; -} => { - for (const key in gf) { - gf[key].hidden ??= false; - gf[key].default ??= false; - } - return gf as { [K in keyof T]: GuildFeature }; -}; - -export const guildFeaturesObj = asGuildFeature({ - automod: { - name: 'Automod', - description: 'Deletes offensive content as well as phishing links.' - }, - excludeDefaultAutomod: { - name: 'Exclude Default Automod', - description: 'Opt out of using the default automod options.' - }, - excludeAutomodScamLinks: { - name: 'Exclude Automod Scam Links', - description: 'Opt out of having automod delete scam links.' - }, - delScamMentions: { - name: 'Delete Scam Mentions', - description: 'Deletes messages with @everyone and @here mentions that have common scam phrases.' - }, - blacklistedFile: { - name: 'Blacklisted File', - description: 'Automatically deletes malicious files.' - }, - autoPublish: { - name: 'Auto Publish', - description: 'Publishes messages in configured announcement channels.' - }, - // todo implement a better auto thread system - autoThread: { - name: 'Auto Thread', - description: 'Creates a new thread for messages in configured channels.', - hidden: true - }, - perspectiveApi: { - name: 'Perspective API', - description: 'Use the Perspective API to detect toxicity.', - hidden: true - }, - boosterMessageReact: { - name: 'Booster Message React', - description: 'Reacts to booster messages with the boost emoji.' - }, - leveling: { - name: 'Leveling', - description: "Tracks users' messages and assigns them xp." - }, - sendLevelUpMessages: { - name: 'Send Level Up Messages', - description: 'Send a message when a user levels up.', - default: true - }, - stickyRoles: { - name: 'Sticky Roles', - description: 'Restores past roles to a user when they rejoin.' - }, - reporting: { - name: 'Reporting', - description: 'Allow users to make reports.' - }, - modsCanPunishMods: { - name: 'Mods Can Punish Mods', - description: 'Allow moderators to punish other moderators.' - }, - logManualPunishments: { - name: 'Log Manual Punishments', - description: "Adds manual punishment to the user's modlogs and the logging channels.", - default: true - }, - punishmentAppeals: { - name: 'Punishment Appeals', - description: 'Allow users to appeal their punishments and send the appeal to the configured channel.', - hidden: true - }, - highlight: { - name: 'Highlight', - description: 'Allows the highlight command to be used.', - default: true - } -}); - -export const guildLogsObj = { - automod: { - description: 'Sends a message in this channel every time automod is activated.', - configurable: true - }, - moderation: { - description: 'Sends a message in this channel every time a moderation action is performed.', - configurable: true - }, - report: { - description: 'Logs user reports.', - configurable: true - }, - error: { - description: 'Logs errors that occur with the bot.', - configurable: true - }, - appeals: { - description: 'Where punishment appeals are sent.', - configurable: false - } -}; - -export type GuildLogType = keyof typeof guildLogsObj; -export const guildLogsArr = Object.keys(guildLogsObj).filter( - (s) => guildLogsObj[s as GuildLogType].configurable -) as GuildLogType[]; -type LogChannelDB = { [x in keyof typeof guildLogsObj]?: Snowflake }; - -export type GuildFeatures = keyof typeof guildFeaturesObj; -export const guildFeaturesArr: GuildFeatures[] = Object.keys(guildFeaturesObj).filter( - (f) => !guildFeaturesObj[f as keyof typeof guildFeaturesObj].hidden -) as GuildFeatures[]; diff --git a/src/lib/models/instance/Highlight.ts b/src/lib/models/instance/Highlight.ts deleted file mode 100644 index 5889fad..0000000 --- a/src/lib/models/instance/Highlight.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { type Snowflake } from 'discord.js'; -import { nanoid } from 'nanoid'; -import { type Sequelize } from 'sequelize'; -import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; - -export interface HighlightModel { - pk: string; - user: Snowflake; - guild: Snowflake; - words: HighlightWord[]; - blacklistedChannels: Snowflake[]; - blacklistedUsers: Snowflake[]; -} - -export interface HighLightCreationAttributes { - pk?: string; - user: Snowflake; - guild: Snowflake; - words?: HighlightWord[]; - blacklistedChannels?: Snowflake[]; - blacklistedUsers?: Snowflake[]; -} - -export interface HighlightWord { - word: string; - regex: boolean; -} - -/** - * List of words that should cause the user to be notified for if found in the specified guild. - */ -export class Highlight extends BaseModel implements HighlightModel { - /** - * The primary key of the highlight. - */ - public declare pk: string; - - /** - * The user that the highlight is for. - */ - public declare user: Snowflake; - - /** - * The guild to look for highlights in. - */ - public declare guild: Snowflake; - - /** - * The words to look for. - */ - public declare words: HighlightWord[]; - - /** - * Channels that the user choose to ignore highlights in. - */ - public declare blacklistedChannels: Snowflake[]; - - /** - * Users that the user choose to ignore highlights from. - */ - public declare blacklistedUsers: Snowflake[]; - - /** - * Initializes the model. - * @param sequelize The sequelize instance. - */ - public static initModel(sequelize: Sequelize): void { - Highlight.init( - { - pk: { type: DataTypes.STRING, primaryKey: true, defaultValue: nanoid }, - user: { type: DataTypes.STRING, allowNull: false }, - guild: { type: DataTypes.STRING, allowNull: false }, - words: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - blacklistedChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - blacklistedUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] } - }, - { sequelize } - ); - } -} diff --git a/src/lib/models/instance/Level.ts b/src/lib/models/instance/Level.ts deleted file mode 100644 index d8d16f0..0000000 --- a/src/lib/models/instance/Level.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { type Snowflake } from 'discord.js'; -import { type Sequelize } from 'sequelize'; -import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; - -export interface LevelModel { - user: Snowflake; - guild: Snowflake; - xp: number; -} - -export interface LevelModelCreationAttributes { - user: Snowflake; - guild: Snowflake; - xp?: number; -} - -/** - * Leveling information for a user in a guild. - */ -export class Level extends BaseModel implements LevelModel { - /** - * The user's id. - */ - public declare user: Snowflake; - - /** - * The guild where the user is gaining xp. - */ - public declare guild: Snowflake; - - /** - * The user's xp. - */ - public declare xp: number; - - /** - * The user's level. - */ - public get level(): number { - return Level.convertXpToLevel(this.xp); - } - - /** - * Initializes the model. - * @param sequelize The sequelize instance. - */ - public static initModel(sequelize: Sequelize): void { - Level.init( - { - user: { type: DataTypes.STRING, allowNull: false }, - guild: { type: DataTypes.STRING, allowNull: false }, - xp: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 } - }, - { sequelize } - ); - } - - public static convertXpToLevel(xp: number): number { - return Math.floor((-25 + Math.sqrt(625 + 200 * xp)) / 100); - } - - public static convertLevelToXp(level: number): number { - return 50 * level * level + 25 * level; // 50x² + 25x - } - - public static genRandomizedXp(): number { - return Math.floor(Math.random() * (40 - 15 + 1)) + 15; - } -} diff --git a/src/lib/models/instance/ModLog.ts b/src/lib/models/instance/ModLog.ts deleted file mode 100644 index c25f043..0000000 --- a/src/lib/models/instance/ModLog.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { type Snowflake } from 'discord.js'; -import { nanoid } from 'nanoid'; -import { type Sequelize } from 'sequelize'; -import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; - -export enum ModLogType { - PERM_BAN = 'PERM_BAN', - TEMP_BAN = 'TEMP_BAN', - UNBAN = 'UNBAN', - KICK = 'KICK', - PERM_MUTE = 'PERM_MUTE', - TEMP_MUTE = 'TEMP_MUTE', - UNMUTE = 'UNMUTE', - WARN = 'WARN', - PERM_PUNISHMENT_ROLE = 'PERM_PUNISHMENT_ROLE', - TEMP_PUNISHMENT_ROLE = 'TEMP_PUNISHMENT_ROLE', - REMOVE_PUNISHMENT_ROLE = 'REMOVE_PUNISHMENT_ROLE', - PERM_CHANNEL_BLOCK = 'PERM_CHANNEL_BLOCK', - TEMP_CHANNEL_BLOCK = 'TEMP_CHANNEL_BLOCK', - CHANNEL_UNBLOCK = 'CHANNEL_UNBLOCK', - TIMEOUT = 'TIMEOUT', - REMOVE_TIMEOUT = 'REMOVE_TIMEOUT' -} - -export interface ModLogModel { - id: string; - type: ModLogType; - user: Snowflake; - moderator: Snowflake; - reason: string | null; - duration: number | null; - guild: Snowflake; - evidence: string; - pseudo: boolean; - hidden: boolean; -} - -export interface ModLogModelCreationAttributes { - id?: string; - type: ModLogType; - user: Snowflake; - moderator: Snowflake; - reason?: string | null; - duration?: number; - guild: Snowflake; - evidence?: string; - pseudo?: boolean; - hidden?: boolean; -} - -/** - * A mod log case. - */ -export class ModLog extends BaseModel implements ModLogModel { - /** - * The primary key of the modlog entry. - */ - public declare id: string; - - /** - * The type of punishment. - */ - public declare type: ModLogType; - - /** - * The user being punished. - */ - public declare user: Snowflake; - - /** - * The user carrying out the punishment. - */ - public declare moderator: Snowflake; - - /** - * The reason the user is getting punished. - */ - public declare reason: string | null; - - /** - * The amount of time the user is getting punished for. - */ - public declare duration: number | null; - - /** - * The guild the user is getting punished in. - */ - public declare guild: Snowflake; - - /** - * Evidence of what the user is getting punished for. - */ - public declare evidence: string; - - /** - * Not an actual modlog just used so a punishment entry can be made. - */ - public declare pseudo: boolean; - - /** - * Hides from the modlog command unless show hidden is specified. - */ - public declare hidden: boolean; - - /** - * Initializes the model. - * @param sequelize The sequelize instance. - */ - public static initModel(sequelize: Sequelize): void { - ModLog.init( - { - id: { type: DataTypes.STRING, primaryKey: true, allowNull: false, defaultValue: nanoid }, - type: { type: DataTypes.STRING, allowNull: false }, //? This is not an enum because of a sequelize issue: https://github.com/sequelize/sequelize/issues/2554 - user: { type: DataTypes.STRING, allowNull: false }, - moderator: { type: DataTypes.STRING, allowNull: false }, - duration: { type: DataTypes.STRING, allowNull: true }, - reason: { type: DataTypes.TEXT, allowNull: true }, - guild: { type: DataTypes.STRING, references: { model: 'Guilds', key: 'id' } }, - evidence: { type: DataTypes.TEXT, allowNull: true }, - pseudo: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }, - hidden: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false } - }, - { sequelize } - ); - } -} diff --git a/src/lib/models/instance/Reminder.ts b/src/lib/models/instance/Reminder.ts deleted file mode 100644 index 964ea63..0000000 --- a/src/lib/models/instance/Reminder.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Snowflake } from 'discord.js'; -import { nanoid } from 'nanoid'; -import { type Sequelize } from 'sequelize'; -import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; - -export interface ReminderModel { - id: string; - user: Snowflake; - messageUrl: string; - content: string; - created: Date; - expires: Date; - notified: boolean; -} - -export interface ReminderModelCreationAttributes { - id?: string; - user: Snowflake; - messageUrl: string; - content: string; - created: Date; - expires: Date; - notified?: boolean; -} - -/** - * Represents a reminder the a user has set. - */ -export class Reminder extends BaseModel implements ReminderModel { - /** - * The id of the reminder. - */ - public declare id: string; - - /** - * The user that the reminder is for. - */ - public declare user: Snowflake; - - /** - * The url of the message where the reminder was created. - */ - public declare messageUrl: string; - - /** - * The content of the reminder. - */ - public declare content: string; - - /** - * The date the reminder was created. - */ - public declare created: Date; - - /** - * The date when the reminder expires. - */ - public declare expires: Date; - - /** - * Whether the user has been notified about the reminder. - */ - public declare notified: boolean; - - /** - * Initializes the model. - * @param sequelize The sequelize instance. - */ - public static initModel(sequelize: Sequelize): void { - Reminder.init( - { - id: { type: DataTypes.STRING, primaryKey: true, defaultValue: nanoid }, - user: { type: DataTypes.STRING, allowNull: false }, - messageUrl: { type: DataTypes.STRING, allowNull: false }, - content: { type: DataTypes.TEXT, allowNull: false }, - created: { type: DataTypes.DATE, allowNull: false }, - expires: { type: DataTypes.DATE, allowNull: false }, - notified: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false } - }, - { sequelize } - ); - } -} diff --git a/src/lib/models/instance/StickyRole.ts b/src/lib/models/instance/StickyRole.ts deleted file mode 100644 index 00e98ce..0000000 --- a/src/lib/models/instance/StickyRole.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { type Snowflake } from 'discord.js'; -import { type Sequelize } from 'sequelize'; -import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; - -export interface StickyRoleModel { - user: Snowflake; - guild: Snowflake; - roles: Snowflake[]; - nickname: string; -} -export interface StickyRoleModelCreationAttributes { - user: Snowflake; - guild: Snowflake; - roles: Snowflake[]; - nickname?: string; -} - -/** - * Information about a user's roles and nickname when they leave a guild. - */ -export class StickyRole extends BaseModel implements StickyRoleModel { - /** - * The id of the user the roles belongs to. - */ - public declare user: Snowflake; - - /** - * The guild where this should happen. - */ - public declare guild: Snowflake; - - /** - * The roles that the user should have returned - */ - public declare roles: Snowflake[]; - - /** - * The user's previous nickname - */ - public declare nickname: string; - - /** - * Initializes the model. - * @param sequelize The sequelize instance. - */ - public static initModel(sequelize: Sequelize): void { - StickyRole.init( - { - user: { type: DataTypes.STRING, allowNull: false }, - guild: { type: DataTypes.STRING, allowNull: false }, - roles: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - nickname: { type: DataTypes.STRING, allowNull: true } - }, - { sequelize } - ); - } -} diff --git a/src/lib/models/shared/Global.ts b/src/lib/models/shared/Global.ts deleted file mode 100644 index b1aa0cc..0000000 --- a/src/lib/models/shared/Global.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { type Snowflake } from 'discord.js'; -import { type Sequelize } from 'sequelize'; -import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; - -export interface GlobalModel { - environment: 'production' | 'development' | 'beta'; - disabledCommands: string[]; - blacklistedUsers: Snowflake[]; - blacklistedGuilds: Snowflake[]; - blacklistedChannels: Snowflake[]; -} - -export interface GlobalModelCreationAttributes { - environment: 'production' | 'development' | 'beta'; - disabledCommands?: string[]; - blacklistedUsers?: Snowflake[]; - blacklistedGuilds?: Snowflake[]; - blacklistedChannels?: Snowflake[]; -} - -/** - * Data specific to a certain instance of the bot. - */ -export class Global extends BaseModel implements GlobalModel { - /** - * The bot's environment. - */ - public declare environment: 'production' | 'development' | 'beta'; - - /** - * Globally disabled commands. - */ - public declare disabledCommands: string[]; - - /** - * Globally blacklisted users. - */ - public declare blacklistedUsers: Snowflake[]; - - /** - * Guilds blacklisted from using the bot. - */ - public declare blacklistedGuilds: Snowflake[]; - - /** - * Channels where the bot is prevented from running commands in. - */ - public declare blacklistedChannels: Snowflake[]; - - /** - * Initializes the model. - * @param sequelize The sequelize instance. - */ - public static initModel(sequelize: Sequelize): void { - Global.init( - { - environment: { type: DataTypes.STRING, primaryKey: true }, - disabledCommands: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - blacklistedUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - blacklistedGuilds: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - blacklistedChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] } - }, - { sequelize } - ); - } -} diff --git a/src/lib/models/shared/GuildCount.ts b/src/lib/models/shared/GuildCount.ts deleted file mode 100644 index 51e571a..0000000 --- a/src/lib/models/shared/GuildCount.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { type Sequelize } from 'sequelize'; -import { Environment } from '../../../../config/Config.js'; -const { DataTypes, Model } = (await import('sequelize')).default; - -export interface GuildCountModel { - timestamp: Date; - environment: Environment; - guildCount: number; -} - -export interface GuildCountCreationAttributes { - timestamp?: Date; - environment: Environment; - guildCount: number; -} - -/** - * The number of guilds that the bot is in for each environment. - */ -export class GuildCount extends Model implements GuildCountModel { - public declare timestamp: Date; - public declare environment: Environment; - public declare guildCount: number; - - /** - * Initializes the model. - * @param sequelize The sequelize instance. - */ - public static initModel(sequelize: Sequelize): void { - GuildCount.init( - { - timestamp: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW }, - environment: { type: DataTypes.STRING, allowNull: false }, - guildCount: { type: DataTypes.BIGINT, allowNull: false } - }, - { sequelize, timestamps: false } - ); - } -} diff --git a/src/lib/models/shared/MemberCount.ts b/src/lib/models/shared/MemberCount.ts deleted file mode 100644 index ea8795c..0000000 --- a/src/lib/models/shared/MemberCount.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { type Sequelize } from 'sequelize'; -const { DataTypes, Model } = (await import('sequelize')).default; - -export interface MemberCountModel { - timestamp: Date; - guildId: string; - memberCount: number; -} - -export interface MemberCountCreationAttributes { - timestamp?: Date; - guildId: string; - memberCount: number; -} - -/** - * The member count of each guild that the bot is in that have over 100 members. - */ -export class MemberCount extends Model implements MemberCountModel { - public declare timestamp: Date; - public declare guildId: string; - public declare memberCount: number; - - /** - * Initializes the model. - * @param sequelize The sequelize instance. - */ - public static initModel(sequelize: Sequelize): void { - MemberCount.init( - { - timestamp: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW }, - guildId: { type: DataTypes.STRING, allowNull: false }, - memberCount: { type: DataTypes.BIGINT, allowNull: false } - }, - { sequelize, timestamps: false } - ); - } -} diff --git a/src/lib/models/shared/Shared.ts b/src/lib/models/shared/Shared.ts deleted file mode 100644 index 4d13011..0000000 --- a/src/lib/models/shared/Shared.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Snowflake } from 'discord.js'; -import type { Sequelize } from 'sequelize'; -import type { BadWords } from '../../common/AutoMod.js'; -import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; - -export interface SharedModel { - primaryKey: 0; - superUsers: Snowflake[]; - privilegedUsers: Snowflake[]; - badLinksSecret: string[]; - badLinks: string[]; - badWords: BadWords; - autoBanCode: string | null; -} - -export interface SharedModelCreationAttributes { - primaryKey?: 0; - superUsers?: Snowflake[]; - privilegedUsers?: Snowflake[]; - badLinksSecret?: string[]; - badLinks?: string[]; - badWords?: BadWords; - autoBanCode?: string; -} - -/** - * Data shared between all bot instances. - */ -export class Shared extends BaseModel implements SharedModel { - /** - * The primary key of the shared model. - */ - public declare primaryKey: 0; - - /** - * Trusted users. - */ - public declare superUsers: Snowflake[]; - - /** - * Users that have all permissions that devs have except eval. - */ - public declare privilegedUsers: Snowflake[]; - - /** - * Non-public bad links. - */ - public declare badLinksSecret: string[]; - - /** - * Public Bad links. - */ - public declare badLinks: string[]; - - /** - * Bad words. - */ - public declare badWords: BadWords; - - /** - * Code that is used to match for auto banning users in moulberry's bush - */ - public declare autoBanCode: string; - - /** - * Initializes the model. - * @param sequelize The sequelize instance. - */ - public static initModel(sequelize: Sequelize): void { - Shared.init( - { - primaryKey: { type: DataTypes.INTEGER, primaryKey: true, validate: { min: 0, max: 0 } }, - superUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - privilegedUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - badLinksSecret: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - badLinks: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }, - badWords: { type: DataTypes.JSONB, allowNull: false, defaultValue: {} }, - autoBanCode: { type: DataTypes.TEXT } - }, - { sequelize, freezeTableName: true } - ); - } -} diff --git a/src/lib/models/shared/Stat.ts b/src/lib/models/shared/Stat.ts deleted file mode 100644 index 8e2e0b3..0000000 --- a/src/lib/models/shared/Stat.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { type Sequelize } from 'sequelize'; -import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; - -type Environment = 'production' | 'development' | 'beta'; - -export interface StatModel { - environment: Environment; - commandsUsed: bigint; - slashCommandsUsed: bigint; -} - -export interface StatModelCreationAttributes { - environment: Environment; - commandsUsed?: bigint; - slashCommandsUsed?: bigint; -} - -/** - * Statistics for each instance of the bot. - */ -export class Stat extends BaseModel implements StatModel { - /** - * The bot's environment. - */ - public declare environment: Environment; - - /** - * The number of commands used - */ - public declare commandsUsed: bigint; - - /** - * The number of slash commands used - */ - public declare slashCommandsUsed: bigint; - - /** - * Initializes the model. - * @param sequelize The sequelize instance. - */ - public static initModel(sequelize: Sequelize): void { - Stat.init( - { - environment: { type: DataTypes.STRING, primaryKey: true }, - commandsUsed: { - type: DataTypes.TEXT, - get: function (): bigint { - return BigInt(this.getDataValue('commandsUsed')); - }, - set: function (val: bigint) { - return this.setDataValue('commandsUsed', `${val}`); - }, - allowNull: false, - defaultValue: `${0n}` - }, - slashCommandsUsed: { - type: DataTypes.TEXT, - get: function (): bigint { - return BigInt(this.getDataValue('slashCommandsUsed')); - }, - set: function (val: bigint) { - return this.setDataValue('slashCommandsUsed', `${val}`); - }, - allowNull: false, - defaultValue: `${0n}` - } - }, - { sequelize } - ); - } -} diff --git a/src/lib/utils/AllowedMentions.ts b/src/lib/utils/AllowedMentions.ts deleted file mode 100644 index d2eb030..0000000 --- a/src/lib/utils/AllowedMentions.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { type MessageMentionOptions, type MessageMentionTypes } from 'discord.js'; - -/** - * A utility class for creating allowed mentions. - */ -export class AllowedMentions { - /** - * @param everyone Whether everyone and here should be mentioned. - * @param roles Whether roles should be mentioned. - * @param users Whether users should be mentioned. - * @param repliedUser Whether the author of the Message being replied to should be mentioned. - */ - public constructor(public everyone = false, public roles = false, public users = true, public repliedUser = true) {} - - /** - * Don't mention anyone. - * @param repliedUser Whether the author of the Message being replied to should be mentioned. - */ - public static none(repliedUser = true): MessageMentionOptions { - return { parse: [], repliedUser }; - } - - /** - * Mention @everyone and @here, roles, and users. - * @param repliedUser Whether the author of the Message being replied to should be mentioned. - */ - public static all(repliedUser = true): MessageMentionOptions { - return { parse: ['everyone', 'roles', 'users'], repliedUser }; - } - - /** - * Mention users. - * @param repliedUser Whether the author of the Message being replied to should be mentioned. - */ - public static users(repliedUser = true): MessageMentionOptions { - return { parse: ['users'], repliedUser }; - } - - /** - * Mention everyone and here. - * @param repliedUser Whether the author of the Message being replied to should be mentioned. - */ - public static everyone(repliedUser = true): MessageMentionOptions { - return { parse: ['everyone'], repliedUser }; - } - - /** - * Mention roles. - * @param repliedUser Whether the author of the Message being replied to should be mentioned. - */ - public static roles(repliedUser = true): MessageMentionOptions { - return { parse: ['roles'], repliedUser }; - } - - /** - * Converts this into a MessageMentionOptions object. - */ - public toObject(): MessageMentionOptions { - return { - parse: [ - ...(this.users ? ['users'] : []), - ...(this.roles ? ['roles'] : []), - ...(this.everyone ? ['everyone'] : []) - ] as MessageMentionTypes[], - repliedUser: this.repliedUser - }; - } -} diff --git a/src/lib/utils/BushCache.ts b/src/lib/utils/BushCache.ts deleted file mode 100644 index 22a13ef..0000000 --- a/src/lib/utils/BushCache.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { BadWords, GlobalModel, SharedModel, type Guild } from '#lib'; -import { Collection, type Snowflake } from 'discord.js'; - -export class BushCache { - public global = new GlobalCache(); - public shared = new SharedCache(); - public guilds = new GuildCache(); -} - -export class GlobalCache implements Omit { - public disabledCommands: string[] = []; - public blacklistedChannels: Snowflake[] = []; - public blacklistedGuilds: Snowflake[] = []; - public blacklistedUsers: Snowflake[] = []; -} - -export class SharedCache implements Omit { - public superUsers: Snowflake[] = []; - public privilegedUsers: Snowflake[] = []; - public badLinksSecret: string[] = []; - public badLinks: string[] = []; - public badWords: BadWords = {}; - public autoBanCode: string | null = null; -} - -export class GuildCache extends Collection {} diff --git a/src/lib/utils/BushClientUtils.ts b/src/lib/utils/BushClientUtils.ts deleted file mode 100644 index 920ff40..0000000 --- a/src/lib/utils/BushClientUtils.ts +++ /dev/null @@ -1,498 +0,0 @@ -import assert from 'assert/strict'; -import { - cleanCodeBlockContent, - DMChannel, - escapeCodeBlock, - GuildMember, - Message, - PartialDMChannel, - Routes, - TextBasedChannel, - ThreadMember, - User, - type APIMessage, - type Client, - type Snowflake, - type UserResolvable -} from 'discord.js'; -import got from 'got'; -import _ from 'lodash'; -import { ConfigChannelKey } from '../../../config/Config.js'; -import CommandErrorListener from '../../listeners/commands/commandError.js'; -import { BushInspectOptions } from '../common/typings/BushInspectOptions.js'; -import { CodeBlockLang } from '../common/typings/CodeBlockLang.js'; -import { CommandMessage } from '../extensions/discord-akairo/BushCommand.js'; -import { SlashMessage } from '../extensions/discord-akairo/SlashMessage.js'; -import { Global } from '../models/shared/Global.js'; -import { Shared } from '../models/shared/Shared.js'; -import { GlobalCache, SharedCache } from './BushCache.js'; -import { emojis, Pronoun, PronounCode, pronounMapping, regex } from './BushConstants.js'; -import { addOrRemoveFromArray, formatError, inspect } from './BushUtils.js'; - -/** - * Utilities that require access to the client. - */ -export class BushClientUtils { - /** - * The hastebin urls used to post to hastebin, attempts to post in order - */ - #hasteURLs: string[] = [ - 'https://hst.sh', - // 'https://hasteb.in', - 'https://hastebin.com', - 'https://mystb.in', - 'https://haste.clicksminuteper.net', - 'https://paste.pythondiscord.com', - 'https://haste.unbelievaboat.com' - // 'https://haste.tyman.tech' - ]; - - public constructor(private readonly client: Client) {} - - /** - * Maps an array of user ids to user objects. - * @param ids The list of IDs to map - * @returns The list of users mapped - */ - public async mapIDs(ids: Snowflake[]): Promise { - return await Promise.all(ids.map((id) => this.client.users.fetch(id))); - } - - /** - * Posts text to hastebin - * @param content The text to post - * @returns The url of the posted text - */ - public async haste(content: string, substr = false): Promise { - let isSubstr = false; - if (content.length > 400_000 && !substr) { - void this.handleError('haste', new Error(`content over 400,000 characters (${content.length.toLocaleString()})`)); - return { error: 'content too long' }; - } else if (content.length > 400_000) { - content = content.substring(0, 400_000); - isSubstr = true; - } - for (const url of this.#hasteURLs) { - try { - const res: HastebinRes = await got.post(`${url}/documents`, { body: content }).json(); - return { url: `${url}/${res.key}`, error: isSubstr ? 'substr' : undefined }; - } catch { - void this.client.console.error('haste', `Unable to upload haste to ${url}`); - } - } - return { error: 'unable to post' }; - } - - /** - * Resolves a user-provided string into a user object, if possible - * @param text The text to try and resolve - * @returns The user resolved or null - */ - public async resolveUserAsync(text: string): Promise { - const idReg = /\d{17,19}/; - const idMatch = text.match(idReg); - if (idMatch) { - try { - return await this.client.users.fetch(text as Snowflake); - } catch {} - } - const mentionReg = /<@!?(?\d{17,19})>/; - const mentionMatch = text.match(mentionReg); - if (mentionMatch) { - try { - return await this.client.users.fetch(mentionMatch.groups!.id as Snowflake); - } catch {} - } - const user = this.client.users.cache.find((u) => u.username === text); - if (user) return user; - return null; - } - - /** - * Surrounds text in a code block with the specified language and puts it in a hastebin if its too long. - * * Embed Description Limit = 4096 characters - * * Embed Field Limit = 1024 characters - * @param code The content of the code block. - * @param length The maximum length of the code block. - * @param language The language of the code. - * @param substr Whether or not to substring the code if it is too long. - * @returns The generated code block - */ - public async codeblock(code: string, length: number, language: CodeBlockLang | '' = '', substr = false): Promise { - let hasteOut = ''; - code = escapeCodeBlock(code); - const prefix = `\`\`\`${language}\n`; - const suffix = '\n```'; - if (code.length + (prefix + suffix).length >= length) { - const haste_ = await this.haste(code, substr); - hasteOut = `Too large to display. ${ - haste_.url - ? `Hastebin: ${haste_.url}${language ? `.${language}` : ''}${haste_.error ? ` - ${haste_.error}` : ''}` - : `${emojis.error} Hastebin: ${haste_.error}` - }`; - } - - const FormattedHaste = hasteOut.length ? `\n${hasteOut}` : ''; - const shortenedCode = hasteOut ? code.substring(0, length - (prefix + FormattedHaste + suffix).length) : code; - const code3 = code.length ? prefix + shortenedCode + suffix + FormattedHaste : prefix + suffix; - if (code3.length > length) { - void this.client.console.warn(`codeblockError`, `Required Length: ${length}. Actual Length: ${code3.length}`, true); - void this.client.console.warn(`codeblockError`, code3, true); - throw new Error('code too long'); - } - return code3; - } - - /** - * Maps the key of a credential with a readable version when redacting. - * @param key The key of the credential. - * @returns The readable version of the key or the original key if there isn't a mapping. - */ - #mapCredential(key: string): string { - return ( - { - token: 'Main Token', - devToken: 'Dev Token', - betaToken: 'Beta Token', - hypixelApiKey: 'Hypixel Api Key', - wolframAlphaAppId: 'Wolfram|Alpha App ID', - dbPassword: 'Database Password' - }[key] ?? key - ); - } - - /** - * Redacts credentials from a string. - * @param text The text to redact credentials from. - * @returns The redacted text. - */ - public redact(text: string) { - for (const credentialName in { ...this.client.config.credentials, dbPassword: this.client.config.db.password }) { - const credential = { ...this.client.config.credentials, dbPassword: this.client.config.db.password }[ - credentialName as keyof typeof this.client.config.credentials - ]; - if (credential === null || credential === '') continue; - const replacement = this.#mapCredential(credentialName); - const escapeRegex = /[.*+?^${}()|[\]\\]/g; - text = text.replace(new RegExp(credential.toString().replace(escapeRegex, '\\$&'), 'g'), `[${replacement} Omitted]`); - text = text.replace( - new RegExp([...credential.toString()].reverse().join('').replace(escapeRegex, '\\$&'), 'g'), - `[${replacement} Omitted]` - ); - } - return text; - } - - /** - * Takes an any value, inspects it, redacts credentials, and puts it in a codeblock - * (and uploads to hast if the content is too long). - * @param input The object to be inspect, redacted, and put into a codeblock. - * @param language The language to make the codeblock. - * @param inspectOptions The options for {@link BushClientUtil.inspect}. - * @param length The maximum length that the codeblock can be. - * @returns The generated codeblock. - */ - public async inspectCleanRedactCodeblock( - input: any, - language?: CodeBlockLang | '', - inspectOptions?: BushInspectOptions, - length = 1024 - ) { - input = inspect(input, inspectOptions ?? undefined); - if (inspectOptions) inspectOptions.inspectStrings = undefined; - input = cleanCodeBlockContent(input); - input = this.redact(input); - return this.codeblock(input, length, language, true); - } - - /** - * Takes an any value, inspects it, redacts credentials, and uploads it to haste. - * @param input The object to be inspect, redacted, and upload. - * @param inspectOptions The options for {@link BushClientUtil.inspect}. - * @returns The {@link HasteResults}. - */ - public async inspectCleanRedactHaste(input: any, inspectOptions?: BushInspectOptions): Promise { - input = inspect(input, inspectOptions ?? undefined); - input = this.redact(input); - return this.haste(input, true); - } - - /** - * Takes an any value, inspects it and redacts credentials. - * @param input The object to be inspect and redacted. - * @param inspectOptions The options for {@link BushClientUtil.inspect}. - * @returns The redacted and inspected object. - */ - public inspectAndRedact(input: any, inspectOptions?: BushInspectOptions): string { - input = inspect(input, inspectOptions ?? undefined); - return this.redact(input); - } - - /** - * Get the global cache. - */ - public getGlobal(): GlobalCache; - /** - * Get a key from the global cache. - * @param key The key to get in the global cache. - */ - public getGlobal(key: K): GlobalCache[K]; - public getGlobal(key?: keyof GlobalCache) { - return key ? this.client.cache.global[key] : this.client.cache.global; - } - - /** - * Get the shared cache. - */ - public getShared(): SharedCache; - /** - * Get a key from the shared cache. - * @param key The key to get in the shared cache. - */ - public getShared(key: K): SharedCache[K]; - public getShared(key?: keyof SharedCache) { - return key ? this.client.cache.shared[key] : this.client.cache.shared; - } - - /** - * Add or remove an element from an array stored in the Globals database. - * @param action Either `add` or `remove` an element. - * @param key The key of the element in the global cache to update. - * @param value The value to add/remove from the array. - */ - public async insertOrRemoveFromGlobal( - action: 'add' | 'remove', - key: K, - value: Client['cache']['global'][K][0] - ): Promise { - const row = - (await Global.findByPk(this.client.config.environment)) ?? - (await Global.create({ environment: this.client.config.environment })); - const oldValue: any[] = row[key]; - const newValue = addOrRemoveFromArray(action, oldValue, value); - row[key] = newValue; - this.client.cache.global[key] = newValue; - return await row.save().catch((e) => this.handleError('insertOrRemoveFromGlobal', e)); - } - - /** - * Add or remove an element from an array stored in the Shared database. - * @param action Either `add` or `remove` an element. - * @param key The key of the element in the shared cache to update. - * @param value The value to add/remove from the array. - */ - public async insertOrRemoveFromShared>( - action: 'add' | 'remove', - key: K, - value: Client['cache']['shared'][K][0] - ): Promise { - const row = (await Shared.findByPk(0)) ?? (await Shared.create()); - const oldValue: any[] = row[key]; - const newValue = addOrRemoveFromArray(action, oldValue, value); - row[key] = newValue; - this.client.cache.shared[key] = newValue; - return await row.save().catch((e) => this.handleError('insertOrRemoveFromShared', e)); - } - - /** - * Updates an element in the Globals database. - * @param key The key in the global cache to update. - * @param value The value to set the key to. - */ - public async setGlobal( - key: K, - value: Client['cache']['global'][K] - ): Promise { - const row = - (await Global.findByPk(this.client.config.environment)) ?? - (await Global.create({ environment: this.client.config.environment })); - row[key] = value; - this.client.cache.global[key] = value; - return await row.save().catch((e) => this.handleError('setGlobal', e)); - } - - /** - * Updates an element in the Shared database. - * @param key The key in the shared cache to update. - * @param value The value to set the key to. - */ - public async setShared>( - key: K, - value: Client['cache']['shared'][K] - ): Promise { - const row = (await Shared.findByPk(0)) ?? (await Shared.create()); - row[key] = value; - this.client.cache.shared[key] = value; - return await row.save().catch((e) => this.handleError('setShared', e)); - } - - /** - * Send a message in the error logging channel and console for an error. - * @param context - * @param error - */ - public async handleError(context: string, error: Error) { - await this.client.console.error(_.camelCase(context), `An error occurred:\n${formatError(error, false)}`, false); - await this.client.console.channelError({ - embeds: await CommandErrorListener.generateErrorEmbed(this.client, { type: 'unhandledRejection', error: error, context }) - }); - } - - /** - * Fetches a user from discord. - * @param user The user to fetch - * @returns Undefined if the user is not found, otherwise the user. - */ - public async resolveNonCachedUser(user: UserResolvable | undefined | null): Promise { - if (user == null) return undefined; - const resolvedUser = - user instanceof User - ? user - : user instanceof GuildMember - ? user.user - : user instanceof ThreadMember - ? user.user - : user instanceof Message - ? user.author - : undefined; - - return resolvedUser ?? (await this.client.users.fetch(user as Snowflake).catch(() => undefined)); - } - - /** - * Get the pronouns of a discord user from pronoundb.org - * @param user The user to retrieve the promises of. - * @returns The human readable pronouns of the user, or undefined if they do not have any. - */ - public async getPronounsOf(user: User | Snowflake): Promise { - const _user = await this.resolveNonCachedUser(user); - if (!_user) throw new Error(`Cannot find user ${user}`); - const apiRes = (await got - .get(`https://pronoundb.org/api/v1/lookup?platform=discord&id=${_user.id}`) - .json() - .catch(() => undefined)) as { pronouns: PronounCode } | undefined; - - if (!apiRes) return undefined; - assert(apiRes.pronouns); - - return pronounMapping[apiRes.pronouns!]!; - } - - /** - * Uploads an image to imgur. - * @param image The image to upload. - * @returns The url of the imgur. - */ - public async uploadImageToImgur(image: string) { - const clientId = this.client.config.credentials.imgurClientId; - - const resp = (await got - .post('https://api.imgur.com/3/upload', { - headers: { - Authorization: `Client-ID ${clientId}`, - Accept: 'application/json' - }, - form: { - image: image, - type: 'base64' - }, - followRedirect: true - }) - .json()) as { data: { link: string } }; - - return resp.data.link; - } - - /** - * Gets the prefix based off of the message. - * @param message The message to get the prefix from. - * @returns The prefix. - */ - public prefix(message: CommandMessage | SlashMessage): string { - return message.util.isSlash - ? '/' - : this.client.config.isDevelopment - ? 'dev ' - : message.util.parsed?.prefix ?? this.client.config.prefix; - } - - public async resolveMessageLinks(content: string | null): Promise { - const res: MessageLinkParts[] = []; - - if (!content) return res; - - const regex_ = new RegExp(regex.messageLink); - let match: RegExpExecArray | null; - while (((match = regex_.exec(content)), match !== null)) { - const input = match.input; - if (!match.groups || !input) continue; - if (input.startsWith('<') && input.endsWith('>')) continue; - - const { guild_id, channel_id, message_id } = match.groups; - if (!guild_id || !channel_id || !message_id) continue; - - res.push({ guild_id, channel_id, message_id }); - } - - return res; - } - - public async resolveMessagesFromLinks(content: string): Promise { - const res: APIMessage[] = []; - - const links = await this.resolveMessageLinks(content); - if (!links.length) return []; - - for (const { guild_id, channel_id, message_id } of links) { - const guild = this.client.guilds.cache.get(guild_id); - if (!guild) continue; - const channel = guild.channels.cache.get(channel_id); - if (!channel || (!channel.isTextBased() && !channel.isThread())) continue; - - const message = (await this.client.rest - .get(Routes.channelMessage(channel_id, message_id)) - .catch(() => null)) as APIMessage | null; - if (!message) continue; - - res.push(message); - } - - return res; - } - - /** - * Resolves a channel from the config and ensures it is a non-dm-based-text-channel. - * @param channel The channel to retrieve. - */ - public async getConfigChannel( - channel: ConfigChannelKey - ): Promise | null> { - const channels = this.client.config.channels; - if (!(channel in channels)) - throw new TypeError(`Invalid channel provided (${channel}), must be one of ${Object.keys(channels).join(' ')}`); - - const channelId = channels[channel]; - if (channelId === '') return null; - - const res = await this.client.channels.fetch(channelId); - - if (!res?.isTextBased() || res.isDMBased()) return null; - - return res; - } -} - -interface HastebinRes { - key: string; -} - -export interface HasteResults { - url?: string; - error?: 'content too long' | 'substr' | 'unable to post'; -} - -export interface MessageLinkParts { - guild_id: Snowflake; - channel_id: Snowflake; - message_id: Snowflake; -} diff --git a/src/lib/utils/BushConstants.ts b/src/lib/utils/BushConstants.ts deleted file mode 100644 index 090616c..0000000 --- a/src/lib/utils/BushConstants.ts +++ /dev/null @@ -1,531 +0,0 @@ -import deepLock from 'deep-lock'; -import { - ArgumentMatches as AkairoArgumentMatches, - ArgumentTypes as AkairoArgumentTypes, - BuiltInReasons, - CommandHandlerEvents as AkairoCommandHandlerEvents -} from 'discord-akairo/dist/src/util/Constants.js'; -import { Colors, GuildFeature } from 'discord.js'; - -const rawCapeUrl = 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/'; - -/** - * Time units in milliseconds - */ -export const enum Time { - /** - * One millisecond (1 ms). - */ - Millisecond = 1, - - /** - * One second (1,000 ms). - */ - Second = Millisecond * 1000, - - /** - * One minute (60,000 ms). - */ - Minute = Second * 60, - - /** - * One hour (3,600,000 ms). - */ - Hour = Minute * 60, - - /** - * One day (86,400,000 ms). - */ - Day = Hour * 24, - - /** - * One week (604,800,000 ms). - */ - Week = Day * 7, - - /** - * One month (2,629,800,000 ms). - */ - Month = Day * 30.4375, // average of days in a month (including leap years) - - /** - * One year (31,557,600,000 ms). - */ - Year = Day * 365.25 // average with leap years -} - -export const emojis = Object.freeze({ - success: '<:success:837109864101707807>', - warn: '<:warn:848726900876247050>', - error: '<:error:837123021016924261>', - successFull: '<:success_full:850118767576088646>', - warnFull: '<:warn_full:850118767391539312>', - errorFull: '<:error_full:850118767295201350>', - mad: '<:mad:783046135392239626>', - join: '<:join:850198029809614858>', - leave: '<:leave:850198048205307919>', - loading: '', - offlineCircle: '<:offline:787550565382750239>', - dndCircle: '<:dnd:787550487633330176>', - idleCircle: '<:idle:787550520956551218>', - onlineCircle: '<:online:787550449435803658>', - cross: '<:cross:878319362539421777>', - check: '<:check:878320135297961995>' -} as const); - -export const emojisRaw = Object.freeze({ - success: '837109864101707807', - warn: '848726900876247050', - error: '837123021016924261', - successFull: '850118767576088646', - warnFull: '850118767391539312', - errorFull: '850118767295201350', - mad: '783046135392239626', - join: '850198029809614858', - leave: '850198048205307919', - loading: '853419254619963392', - offlineCircle: '787550565382750239', - dndCircle: '787550487633330176', - idleCircle: '787550520956551218', - onlineCircle: '787550449435803658', - cross: '878319362539421777', - check: '878320135297961995' -} as const); - -export const colors = Object.freeze({ - default: 0x1fd8f1, - error: 0xef4947, - warn: 0xfeba12, - success: 0x3bb681, - info: 0x3b78ff, - red: 0xff0000, - blue: 0x0055ff, - aqua: 0x00bbff, - purple: 0x8400ff, - blurple: 0x5440cd, - newBlurple: 0x5865f2, - pink: 0xff00e6, - green: 0x00ff1e, - darkGreen: 0x008f11, - gold: 0xb59400, - yellow: 0xffff00, - white: 0xffffff, - gray: 0xa6a6a6, - lightGray: 0xcfcfcf, - darkGray: 0x7a7a7a, - black: 0x000000, - orange: 0xe86100, - ...Colors -} as const); - -// Somewhat stolen from @Mzato0001 -export const timeUnits = deepLock({ - milliseconds: { - match: / (?:(?-?(?:\d+)?\.?\d+) *(?:milliseconds?|msecs?|ms))/im, - value: Time.Millisecond - }, - seconds: { - match: / (?:(?-?(?:\d+)?\.?\d+) *(?:seconds?|secs?|s))/im, - value: Time.Second - }, - minutes: { - match: / (?:(?-?(?:\d+)?\.?\d+) *(?:minutes?|mins?|m))/im, - value: Time.Minute - }, - hours: { - match: / (?:(?-?(?:\d+)?\.?\d+) *(?:hours?|hrs?|h))/im, - value: Time.Hour - }, - days: { - match: / (?:(?-?(?:\d+)?\.?\d+) *(?:days?|d))/im, - value: Time.Day - }, - weeks: { - match: / (?:(?-?(?:\d+)?\.?\d+) *(?:weeks?|w))/im, - value: Time.Week - }, - months: { - match: / (?:(?-?(?:\d+)?\.?\d+) *(?:months?|mon|mo))/im, - value: Time.Month - }, - years: { - match: / (?:(?-?(?:\d+)?\.?\d+) *(?:years?|y))/im, - value: Time.Year - } -} as const); - -export const regex = deepLock({ - snowflake: /^\d{15,21}$/im, - - discordEmoji: /[a-zA-Z0-9_]+):(?\d{15,21})>/im, - - /* - * Taken with permission from Geek: - * https://github.com/FireDiscordBot/bot/blob/5d1990e5f8b52fcc72261d786aa3c7c7c65ab5e8/lib/util/constants.ts#L276 - */ - /** **This has the global flag, make sure to handle it correctly.** */ - messageLink: - /\d{15,21})\/(?\d{15,21})\/(?\d{15,21})>?/gim -} as const); - -/** - * Maps the response from pronoundb.org to a readable format - */ -export const pronounMapping = Object.freeze({ - unspecified: 'Unspecified', - hh: 'He/Him', - hi: 'He/It', - hs: 'He/She', - ht: 'He/They', - ih: 'It/Him', - ii: 'It/Its', - is: 'It/She', - it: 'It/They', - shh: 'She/He', - sh: 'She/Her', - si: 'She/It', - st: 'She/They', - th: 'They/He', - ti: 'They/It', - ts: 'They/She', - tt: 'They/Them', - any: 'Any pronouns', - other: 'Other pronouns', - ask: 'Ask me my pronouns', - avoid: 'Avoid pronouns, use my name' -} as const); - -/** - * A bunch of mappings - */ -export const mappings = deepLock({ - guilds: { - "Moulberry's Bush": '516977525906341928', - "Moulberry's Tree": '767448775450820639', - 'MB Staff': '784597260465995796', - "IRONM00N's Space Ship": '717176538717749358' - }, - - channels: { - 'neu-support': '714332750156660756', - 'giveaways': '767782084981817344' - }, - - users: { - IRONM00N: '322862723090219008', - Moulberry: '211288288055525376', - nopo: '384620942577369088', - Bestower: '496409778822709251' - }, - - permissions: { - CreateInstantInvite: { name: 'Create Invite', important: false }, - KickMembers: { name: 'Kick Members', important: true }, - BanMembers: { name: 'Ban Members', important: true }, - Administrator: { name: 'Administrator', important: true }, - ManageChannels: { name: 'Manage Channels', important: true }, - ManageGuild: { name: 'Manage Server', important: true }, - AddReactions: { name: 'Add Reactions', important: false }, - ViewAuditLog: { name: 'View Audit Log', important: true }, - PrioritySpeaker: { name: 'Priority Speaker', important: true }, - Stream: { name: 'Video', important: false }, - ViewChannel: { name: 'View Channel', important: false }, - SendMessages: { name: 'Send Messages', important: false }, - SendTTSMessages: { name: 'Send Text-to-Speech Messages', important: true }, - ManageMessages: { name: 'Manage Messages', important: true }, - EmbedLinks: { name: 'Embed Links', important: false }, - AttachFiles: { name: 'Attach Files', important: false }, - ReadMessageHistory: { name: 'Read Message History', important: false }, - MentionEveryone: { name: 'Mention @\u200Beveryone, @\u200Bhere, and All Roles', important: true }, // name has a zero-width space to prevent accidents - UseExternalEmojis: { name: 'Use External Emoji', important: false }, - ViewGuildInsights: { name: 'View Server Insights', important: true }, - Connect: { name: 'Connect', important: false }, - Speak: { name: 'Speak', important: false }, - MuteMembers: { name: 'Mute Members', important: true }, - DeafenMembers: { name: 'Deafen Members', important: true }, - MoveMembers: { name: 'Move Members', important: true }, - UseVAD: { name: 'Use Voice Activity', important: false }, - ChangeNickname: { name: 'Change Nickname', important: false }, - ManageNicknames: { name: 'Change Nicknames', important: true }, - ManageRoles: { name: 'Manage Roles', important: true }, - ManageWebhooks: { name: 'Manage Webhooks', important: true }, - ManageEmojisAndStickers: { name: 'Manage Emojis and Stickers', important: true }, - UseApplicationCommands: { name: 'Use Slash Commands', important: false }, - RequestToSpeak: { name: 'Request to Speak', important: false }, - ManageEvents: { name: 'Manage Events', important: true }, - ManageThreads: { name: 'Manage Threads', important: true }, - CreatePublicThreads: { name: 'Create Public Threads', important: false }, - CreatePrivateThreads: { name: 'Create Private Threads', important: false }, - UseExternalStickers: { name: 'Use External Stickers', important: false }, - SendMessagesInThreads: { name: 'Send Messages In Threads', important: false }, - StartEmbeddedActivities: { name: 'Start Activities', important: false }, - ModerateMembers: { name: 'Timeout Members', important: true }, - UseEmbeddedActivities: { name: 'Use Activities', important: false } - }, - - // prettier-ignore - features: { - [GuildFeature.Verified]: { name: 'Verified', important: true, emoji: '<:verified:850795049817473066>', weight: 0 }, - [GuildFeature.Partnered]: { name: 'Partnered', important: true, emoji: '<:partneredServer:850794851955507240>', weight: 1 }, - [GuildFeature.MoreStickers]: { name: 'More Stickers', important: true, emoji: null, weight: 2 }, - MORE_EMOJIS: { name: 'More Emoji', important: true, emoji: '<:moreEmoji:850786853497602080>', weight: 3 }, - [GuildFeature.Featurable]: { name: 'Featurable', important: true, emoji: '<:featurable:850786776372084756>', weight: 4 }, - [GuildFeature.RelayEnabled]: { name: 'Relay Enabled', important: true, emoji: '<:relayEnabled:850790531441229834>', weight: 5 }, - [GuildFeature.Discoverable]: { name: 'Discoverable', important: true, emoji: '<:discoverable:850786735360966656>', weight: 6 }, - ENABLED_DISCOVERABLE_BEFORE: { name: 'Enabled Discovery Before', important: false, emoji: '<:enabledDiscoverableBefore:850786754670624828>', weight: 7 }, - [GuildFeature.MonetizationEnabled]: { name: 'Monetization Enabled', important: true, emoji: null, weight: 8 }, - [GuildFeature.TicketedEventsEnabled]: { name: 'Ticketed Events Enabled', important: true, emoji: null, weight: 9 }, - [GuildFeature.PreviewEnabled]: { name: 'Preview Enabled', important: true, emoji: '<:previewEnabled:850790508266913823>', weight: 10 }, - COMMERCE: { name: 'Store Channels', important: true, emoji: '<:storeChannels:850786692432396338>', weight: 11 }, - [GuildFeature.VanityURL]: { name: 'Vanity URL', important: false, emoji: '<:vanityURL:850790553079644160>', weight: 12 }, - [GuildFeature.VIPRegions]: { name: 'VIP Regions', important: false, emoji: '<:VIPRegions:850794697496854538>', weight: 13 }, - [GuildFeature.AnimatedIcon]: { name: 'Animated Icon', important: false, emoji: '<:animatedIcon:850774498071412746>', weight: 14 }, - [GuildFeature.Banner]: { name: 'Banner', important: false, emoji: '<:banner:850786673150787614>', weight: 15 }, - [GuildFeature.InviteSplash]: { name: 'Invite Splash', important: false, emoji: '<:inviteSplash:850786798246559754>', weight: 16 }, - [GuildFeature.PrivateThreads]: { name: 'Private Threads', important: false, emoji: '<:privateThreads:869763711894700093>', weight: 17 }, - THREE_DAY_THREAD_ARCHIVE: { name: 'Three Day Thread Archive', important: false, emoji: '<:threeDayThreadArchive:869767841652564008>', weight: 19 }, - SEVEN_DAY_THREAD_ARCHIVE: { name: 'Seven Day Thread Archive', important: false, emoji: '<:sevenDayThreadArchive:869767896123998288>', weight: 20 }, - [GuildFeature.RoleIcons]: { name: 'Role Icons', important: false, emoji: '<:roleIcons:876993381929222175>', weight: 21 }, - [GuildFeature.News]: { name: 'Announcement Channels', important: false, emoji: '<:announcementChannels:850790491796013067>', weight: 22 }, - [GuildFeature.MemberVerificationGateEnabled]: { name: 'Membership Verification Gate', important: false, emoji: '<:memberVerificationGateEnabled:850786829984858212>', weight: 23 }, - [GuildFeature.WelcomeScreenEnabled]: { name: 'Welcome Screen Enabled', important: false, emoji: '<:welcomeScreenEnabled:850790575875817504>', weight: 24 }, - [GuildFeature.Community]: { name: 'Community', important: false, emoji: '<:community:850786714271875094>', weight: 25 }, - THREADS_ENABLED: {name: 'Threads Enabled', important: false, emoji: '<:threadsEnabled:869756035345317919>', weight: 26 }, - THREADS_ENABLED_TESTING: {name: 'Threads Enabled Testing', important: false, emoji: null, weight: 27 }, - [GuildFeature.AnimatedBanner]: { name: 'Animated Banner', important: false, emoji: null, weight: 28 }, - [GuildFeature.HasDirectoryEntry]: { name: 'Has Directory Entry', important: true, emoji: null, weight: 29 }, - [GuildFeature.Hub]: { name: 'Hub', important: true, emoji: null, weight: 30 }, - [GuildFeature.LinkedToHub]: { name: 'Linked To Hub', important: true, emoji: null, weight: 31 }, - }, - - regions: { - 'automatic': ':united_nations: Automatic', - 'brazil': ':flag_br: Brazil', - 'europe': ':flag_eu: Europe', - 'hongkong': ':flag_hk: Hongkong', - 'india': ':flag_in: India', - 'japan': ':flag_jp: Japan', - 'russia': ':flag_ru: Russia', - 'singapore': ':flag_sg: Singapore', - 'southafrica': ':flag_za: South Africa', - 'sydney': ':flag_au: Sydney', - 'us-central': ':flag_us: US Central', - 'us-east': ':flag_us: US East', - 'us-south': ':flag_us: US South', - 'us-west': ':flag_us: US West' - }, - - otherEmojis: { - ServerBooster1: '<:serverBooster1:848740052091142145>', - ServerBooster2: '<:serverBooster2:848740090506510388>', - ServerBooster3: '<:serverBooster3:848740124992077835>', - ServerBooster6: '<:serverBooster6:848740155245461514>', - ServerBooster9: '<:serverBooster9:848740188846030889>', - ServerBooster12: '<:serverBooster12:848740304365551668>', - ServerBooster15: '<:serverBooster15:848740354890137680>', - ServerBooster18: '<:serverBooster18:848740402886606868>', - ServerBooster24: '<:serverBooster24:848740444628320256>', - Nitro: '<:nitro:848740498054971432>', - Booster: '<:booster:848747775020892200>', - Owner: '<:owner:848746439311753286>', - Admin: '<:admin:848963914628333598>', - Superuser: '<:superUser:848947986326224926>', - Developer: '<:developer:848954538111139871>', - Bot: '<:bot:1006929813203853427>', - BushVerified: '<:verfied:853360152090771497>', - BoostTier1: '<:boostitle:853363736679940127>', - BoostTier2: '<:boostitle:853363752728789075>', - BoostTier3: '<:boostitle:853363769132056627>', - ChannelText: '<:text:853375537791893524>', - ChannelNews: '<:announcements:853375553531674644>', - ChannelVoice: '<:voice:853375566735212584>', - ChannelStage: '<:stage:853375583521210468>', - // ChannelStore: '<:store:853375601175691266>', - ChannelCategory: '<:category:853375615260819476>', - ChannelThread: '<:thread:865033845753249813>' - }, - - userFlags: { - Staff: '<:discordEmployee:848742947826434079>', - Partner: '<:partneredServerOwner:848743051593777152>', - Hypesquad: '<:hypeSquadEvents:848743108283072553>', - BugHunterLevel1: '<:bugHunter:848743239850393640>', - HypeSquadOnlineHouse1: '<:hypeSquadBravery:848742910563844127>', - HypeSquadOnlineHouse2: '<:hypeSquadBrilliance:848742840649646101>', - HypeSquadOnlineHouse3: '<:hypeSquadBalance:848742877537370133>', - PremiumEarlySupporter: '<:earlySupporter:848741030102171648>', - TeamPseudoUser: 'TeamPseudoUser', - BugHunterLevel2: '<:bugHunterGold:848743283080822794>', - VerifiedBot: '<:verifiedbot_rebrand1:938928232667947028><:verifiedbot_rebrand2:938928355707879475>', - VerifiedDeveloper: '<:earlyVerifiedBotDeveloper:848741079875846174>', - CertifiedModerator: '<:discordCertifiedModerator:877224285901582366>', - BotHTTPInteractions: 'BotHTTPInteractions', - Spammer: 'Spammer', - Quarantined: 'Quarantined' - }, - - status: { - online: '<:online:848937141639577690>', - idle: '<:idle:848937158261211146>', - dnd: '<:dnd:848937173780135986>', - offline: '<:offline:848939387277672448>', - streaming: '<:streaming:848937187479519242>' - }, - - maybeNitroDiscrims: ['1111', '2222', '3333', '4444', '5555', '6666', '6969', '7777', '8888', '9999'], - - capes: [ - /* supporter capes */ - { name: 'patreon1', purchasable: false /* moulberry no longer offers */ }, - { name: 'patreon2', purchasable: false /* moulberry no longer offers */ }, - { name: 'fade', custom: `${rawCapeUrl}fade.gif`, purchasable: true }, - { name: 'lava', custom: `${rawCapeUrl}lava.gif`, purchasable: true }, - { name: 'mcworld', custom: `${rawCapeUrl}mcworld_compressed.gif`, purchasable: true }, - { name: 'negative', custom: `${rawCapeUrl}negative_compressed.gif`, purchasable: true }, - { name: 'space', custom: `${rawCapeUrl}space_compressed.gif`, purchasable: true }, - { name: 'void', custom: `${rawCapeUrl}void.gif`, purchasable: true }, - { name: 'tunnel', custom: `${rawCapeUrl}tunnel.gif`, purchasable: true }, - /* Staff capes */ - { name: 'contrib' }, - { name: 'mbstaff' }, - { name: 'ironmoon' }, - { name: 'gravy' }, - { name: 'nullzee' }, - /* partner capes */ - { name: 'thebakery' }, - { name: 'dsm' }, - { name: 'packshq' }, - { name: 'furf' }, - { name: 'skytils' }, - { name: 'sbp' }, - { name: 'subreddit_light' }, - { name: 'subreddit_dark' }, - { name: 'skyclient' }, - { name: 'sharex' }, - { name: 'sharex_white' }, - /* streamer capes */ - { name: 'alexxoffi' }, - { name: 'jakethybro' }, - { name: 'krusty' }, - { name: 'krusty_day' }, - { name: 'krusty_night' }, - { name: 'krusty_sunset' }, - { name: 'soldier' }, - { name: 'zera' }, - { name: 'secondpfirsisch' }, - { name: 'stormy_lh' } - ].map((value, index) => ({ ...value, index })), - - roleMap: [ - { name: '*', id: '792453550768390194' }, - { name: 'Admin Perms', id: '746541309853958186' }, - { name: 'Sr. Moderator', id: '782803470205190164' }, - { name: 'Moderator', id: '737308259823910992' }, - { name: 'Helper', id: '737440116230062091' }, - { name: 'Trial Helper', id: '783537091946479636' }, - { name: 'Contributor', id: '694431057532944425' }, - { name: 'Giveaway Donor', id: '784212110263451649' }, - { name: 'Giveaway (200m)', id: '810267756426690601' }, - { name: 'Giveaway (100m)', id: '801444430522613802' }, - { name: 'Giveaway (50m)', id: '787497512981757982' }, - { name: 'Giveaway (25m)', id: '787497515771232267' }, - { name: 'Giveaway (10m)', id: '787497518241153025' }, - { name: 'Giveaway (5m)', id: '787497519768403989' }, - { name: 'Giveaway (1m)', id: '787497521084891166' }, - { name: 'Suggester', id: '811922322767609877' }, - { name: 'Partner', id: '767324547312779274' }, - { name: 'Level Locked', id: '784248899044769792' }, - { name: 'No Files', id: '786421005039173633' }, - { name: 'No Reactions', id: '786421270924361789' }, - { name: 'No Links', id: '786421269356740658' }, - { name: 'No Bots', id: '786804858765312030' }, - { name: 'No VC', id: '788850482554208267' }, - { name: 'No Giveaways', id: '808265422334984203' }, - { name: 'No Support', id: '790247359824396319' } - ], - - roleWhitelist: { - 'Partner': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Suggester': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper', 'Trial Helper', 'Contributor'], - 'Level Locked': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'No Files': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'No Reactions': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'No Links': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'No Bots': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'No VC': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'No Giveaways': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper'], - 'No Support': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway Donor': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (200m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (100m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (50m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (25m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (10m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (5m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (1m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'] - } -} as const); - -export const ArgumentMatches = Object.freeze({ - ...AkairoArgumentMatches -} as const); - -export const ArgumentTypes = Object.freeze({ - ...AkairoArgumentTypes, - DURATION: 'duration', - CONTENT_WITH_DURATION: 'contentWithDuration', - PERMISSION: 'permission', - SNOWFLAKE: 'snowflake', - DISCORD_EMOJI: 'discordEmoji', - ROLE_WITH_DURATION: 'roleWithDuration', - ABBREVIATED_NUMBER: 'abbreviatedNumber', - GLOBAL_USER: 'globalUser' -} as const); - -export const BlockedReasons = Object.freeze({ - ...BuiltInReasons, - DISABLED_GUILD: 'disabledGuild', - DISABLED_GLOBAL: 'disabledGlobal', - ROLE_BLACKLIST: 'roleBlacklist', - USER_GUILD_BLACKLIST: 'userGuildBlacklist', - USER_GLOBAL_BLACKLIST: 'userGlobalBlacklist', - RESTRICTED_GUILD: 'restrictedGuild', - CHANNEL_GUILD_BLACKLIST: 'channelGuildBlacklist', - CHANNEL_GLOBAL_BLACKLIST: 'channelGlobalBlacklist', - RESTRICTED_CHANNEL: 'restrictedChannel' -} as const); - -export const CommandHandlerEvents = Object.freeze({ - ...AkairoCommandHandlerEvents -} as const); - -export const moulberryBushRoleMap = deepLock([ - { name: '*', id: '792453550768390194' }, - { name: 'Admin Perms', id: '746541309853958186' }, - { name: 'Sr. Moderator', id: '782803470205190164' }, - { name: 'Moderator', id: '737308259823910992' }, - { name: 'Helper', id: '737440116230062091' }, - { name: 'Trial Helper', id: '783537091946479636' }, - { name: 'Contributor', id: '694431057532944425' }, - { name: 'Giveaway Donor', id: '784212110263451649' }, - { name: 'Giveaway (200m)', id: '810267756426690601' }, - { name: 'Giveaway (100m)', id: '801444430522613802' }, - { name: 'Giveaway (50m)', id: '787497512981757982' }, - { name: 'Giveaway (25m)', id: '787497515771232267' }, - { name: 'Giveaway (10m)', id: '787497518241153025' }, - { name: 'Giveaway (5m)', id: '787497519768403989' }, - { name: 'Giveaway (1m)', id: '787497521084891166' }, - { name: 'Suggester', id: '811922322767609877' }, - { name: 'Partner', id: '767324547312779274' }, - { name: 'Level Locked', id: '784248899044769792' }, - { name: 'No Files', id: '786421005039173633' }, - { name: 'No Reactions', id: '786421270924361789' }, - { name: 'No Links', id: '786421269356740658' }, - { name: 'No Bots', id: '786804858765312030' }, - { name: 'No VC', id: '788850482554208267' }, - { name: 'No Giveaways', id: '808265422334984203' }, - { name: 'No Support', id: '790247359824396319' } -] as const); - -export type PronounCode = keyof typeof pronounMapping; -export type Pronoun = typeof pronounMapping[PronounCode]; diff --git a/src/lib/utils/BushLogger.ts b/src/lib/utils/BushLogger.ts deleted file mode 100644 index 4acda69..0000000 --- a/src/lib/utils/BushLogger.ts +++ /dev/null @@ -1,315 +0,0 @@ -import chalk from 'chalk'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { bold, Client, EmbedBuilder, escapeMarkdown, PartialTextBasedChannelFields, type Message } from 'discord.js'; -import { stripVTControlCharacters as stripColor } from 'node:util'; -import repl, { REPLServer, REPL_MODE_STRICT } from 'repl'; -import { WriteStream } from 'tty'; -import { type SendMessageType } from '../extensions/discord-akairo/BushClient.js'; -import { colors } from './BushConstants.js'; -import { inspect } from './BushUtils.js'; - -let REPL: REPLServer; -let replGone = false; - -export function init() { - const kFormatForStdout = Object.getOwnPropertySymbols(console).find((sym) => sym.toString() === 'Symbol(kFormatForStdout)')!; - const kFormatForStderr = Object.getOwnPropertySymbols(console).find((sym) => sym.toString() === 'Symbol(kFormatForStderr)')!; - - REPL = repl.start({ - useColors: true, - terminal: true, - useGlobal: true, - replMode: REPL_MODE_STRICT, - breakEvalOnSigint: true, - ignoreUndefined: true - }); - - const apply = (stream: WriteStream, symbol: symbol): ProxyHandler['apply'] => - function apply(target, thisArg, args) { - if (stream.isTTY) { - stream.moveCursor(0, -1); - stream.write('\n'); - stream.clearLine(0); - } - - const ret = target(...args); - - if (stream.isTTY) { - const formatted = (console as any)[symbol](args) as string; - - stream.moveCursor(0, formatted.split('\n').length); - if (!replGone) { - REPL.displayPrompt(true); - } - } - - return ret; - }; - - global.console.log = new Proxy(console.log, { - apply: apply(process.stdout, kFormatForStdout) - }); - - global.console.warn = new Proxy(console.warn, { - apply: apply(process.stderr, kFormatForStderr) - }); - - REPL.on('exit', () => { - replGone = true; - process.exit(0); - }); -} - -/** - * Parses the content surrounding by `<<>>` and emphasizes it with the given color or by making it bold. - * @param content The content to parse. - * @param color The color to emphasize the content with. - * @param discordFormat Whether or not to format the content for discord. - * @returns The formatted content. - */ -function parseFormatting( - content: any, - color: 'blueBright' | 'blackBright' | 'redBright' | 'yellowBright' | 'greenBright' | '', - discordFormat = false -): string | typeof content { - if (typeof content !== 'string') return content; - return content - .split(/<<|>>/) - .map((value, index) => { - if (discordFormat) { - return index % 2 === 0 ? escapeMarkdown(value) : bold(escapeMarkdown(value)); - } else { - return index % 2 === 0 || !color ? value : chalk[color](value); - } - }) - .join(''); -} - -/** - * Inspects the content and returns a string. - * @param content The content to inspect. - * @param depth The depth the content will inspected. Defaults to `2`. - * @param colors Whether or not to use colors in the output. Defaults to `true`. - * @returns The inspected content. - */ -function inspectContent(content: any, depth = 2, colors = true): string { - if (typeof content !== 'string') { - return inspect(content, { depth, colors }); - } - return content; -} - -/** - * Generates a formatted timestamp for logging. - * @returns The formatted timestamp. - */ -function getTimeStamp(): string { - const now = new Date(); - const minute = pad(now.getMinutes()); - const hour = pad(now.getHours()); - const date = `${pad(now.getMonth() + 1)}/${pad(now.getDate())}`; - return `${date} ${hour}:${minute}`; -} - -/** - * Pad a two-digit number. - */ -function pad(num: number) { - return num.toString().padStart(2, '0'); -} - -/** - * Custom logging utility for the bot. - */ -export class BushLogger { - /** - * @param client The client. - */ - public constructor(public client: Client) {} - - /** - * Logs information. Highlight information by surrounding it in `<<>>`. - * @param header The header displayed before the content, displayed in cyan. - * @param content The content to log, highlights displayed in bright blue. - * @param sendChannel Should this also be logged to discord? Defaults to false. - * @param depth The depth the content will inspected. Defaults to 0. - */ - public get log() { - return this.info; - } - - /** - * Sends a message to the log channel. - * @param message The parameter to pass to {@link PartialTextBasedChannelFields.send}. - * @returns The message sent. - */ - public async channelLog(message: SendMessageType): Promise { - const channel = await this.client.utils.getConfigChannel('log'); - if (channel === null) return null; - return await channel.send(message).catch(() => null); - } - - /** - * Sends a message to the error channel. - * @param message The parameter to pass to {@link PartialTextBasedChannelFields.send}. - * @returns The message sent. - */ - public async channelError(message: SendMessageType): Promise { - const channel = await this.client.utils.getConfigChannel('error'); - if (!channel) { - void this.error( - 'BushLogger', - `Could not find error channel, was originally going to send: \n${inspect(message, { - colors: true - })}\n${new Error().stack?.substring(8)}`, - false - ); - return null; - } - return await channel.send(message); - } - - /** - * Logs debug information. Only works in dev is enabled in the config. - * @param content The content to log. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public debug(content: any, depth = 0): void { - if (!this.client.config.isDevelopment) return; - const newContent = inspectContent(content, depth, true); - console.log(`${chalk.bgMagenta(getTimeStamp())} ${chalk.magenta('[Debug]')} ${newContent}`); - } - - /** - * Logs raw debug information. Only works in dev is enabled in the config. - * @param content The content to log. - */ - public debugRaw(...content: any): void { - if (!this.client.config.isDevelopment) return; - console.log(`${chalk.bgMagenta(getTimeStamp())} ${chalk.magenta('[Debug]')}`, ...content); - } - - /** - * Logs verbose information. Highlight information by surrounding it in `<<>>`. - * @param header The header printed before the content, displayed in grey. - * @param content The content to log, highlights displayed in bright black. - * @param sendChannel Should this also be logged to discord? Defaults to `false`. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public async verbose(header: string, content: any, sendChannel = false, depth = 0): Promise { - if (!this.client.config.logging.verbose) return; - const newContent = inspectContent(content, depth, true); - console.log(`${chalk.bgGrey(getTimeStamp())} ${chalk.grey(`[${header}]`)} ${parseFormatting(newContent, 'blackBright')}`); - if (!sendChannel) return; - const embed = new EmbedBuilder() - .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) - .setColor(colors.gray) - .setTimestamp(); - await this.channelLog({ embeds: [embed] }); - } - - /** - * Logs very verbose information. Highlight information by surrounding it in `<<>>`. - * @param header The header printed before the content, displayed in purple. - * @param content The content to log, highlights displayed in bright black. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public async superVerbose(header: string, content: any, depth = 0): Promise { - if (!this.client.config.logging.verbose) return; - const newContent = inspectContent(content, depth, true); - console.log( - `${chalk.bgHex('#949494')(getTimeStamp())} ${chalk.hex('#949494')(`[${header}]`)} ${chalk.hex('#b3b3b3')(newContent)}` - ); - } - - /** - * Logs raw very verbose information. - * @param header The header printed before the content, displayed in purple. - * @param content The content to log. - */ - public async superVerboseRaw(header: string, ...content: any[]): Promise { - if (!this.client.config.logging.verbose) return; - console.log(`${chalk.bgHex('#a3a3a3')(getTimeStamp())} ${chalk.hex('#a3a3a3')(`[${header}]`)}`, ...content); - } - - /** - * Logs information. Highlight information by surrounding it in `<<>>`. - * @param header The header displayed before the content, displayed in cyan. - * @param content The content to log, highlights displayed in bright blue. - * @param sendChannel Should this also be logged to discord? Defaults to `false`. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public async info(header: string, content: any, sendChannel = true, depth = 0): Promise { - if (!this.client.config.logging.info) return; - const newContent = inspectContent(content, depth, true); - console.log(`${chalk.bgCyan(getTimeStamp())} ${chalk.cyan(`[${header}]`)} ${parseFormatting(newContent, 'blueBright')}`); - if (!sendChannel) return; - const embed = new EmbedBuilder() - .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) - .setColor(colors.info) - .setTimestamp(); - await this.channelLog({ embeds: [embed] }); - } - - /** - * Logs warnings. Highlight information by surrounding it in `<<>>`. - * @param header The header displayed before the content, displayed in yellow. - * @param content The content to log, highlights displayed in bright yellow. - * @param sendChannel Should this also be logged to discord? Defaults to `false`. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public async warn(header: string, content: any, sendChannel = true, depth = 0): Promise { - const newContent = inspectContent(content, depth, true); - console.warn( - `${chalk.bgYellow(getTimeStamp())} ${chalk.yellow(`[${header}]`)} ${parseFormatting(newContent, 'yellowBright')}` - ); - - if (!sendChannel) return; - const embed = new EmbedBuilder() - .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) - .setColor(colors.warn) - .setTimestamp(); - await this.channelError({ embeds: [embed] }); - } - - /** - * Logs errors. Highlight information by surrounding it in `<<>>`. - * @param header The header displayed before the content, displayed in bright red. - * @param content The content to log, highlights displayed in bright red. - * @param sendChannel Should this also be logged to discord? Defaults to `false`. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public async error(header: string, content: any, sendChannel = true, depth = 0): Promise { - const newContent = inspectContent(content, depth, true); - console.warn( - `${chalk.bgRedBright(getTimeStamp())} ${chalk.redBright(`[${header}]`)} ${parseFormatting(newContent, 'redBright')}` - ); - if (!sendChannel) return; - const embed = new EmbedBuilder() - .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) - .setColor(colors.error) - .setTimestamp(); - await this.channelError({ embeds: [embed] }); - return; - } - - /** - * Logs successes. Highlight information by surrounding it in `<<>>`. - * @param header The header displayed before the content, displayed in green. - * @param content The content to log, highlights displayed in bright green. - * @param sendChannel Should this also be logged to discord? Defaults to `false`. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public async success(header: string, content: any, sendChannel = true, depth = 0): Promise { - const newContent = inspectContent(content, depth, true); - console.log( - `${chalk.bgGreen(getTimeStamp())} ${chalk.greenBright(`[${header}]`)} ${parseFormatting(newContent, 'greenBright')}` - ); - if (!sendChannel) return; - const embed = new EmbedBuilder() - .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) - .setColor(colors.success) - .setTimestamp(); - await this.channelLog({ embeds: [embed] }).catch(() => {}); - } -} diff --git a/src/lib/utils/BushUtils.ts b/src/lib/utils/BushUtils.ts deleted file mode 100644 index 75aded3..0000000 --- a/src/lib/utils/BushUtils.ts +++ /dev/null @@ -1,612 +0,0 @@ -import { - Arg, - BushClient, - CommandMessage, - SlashEditMessageType, - SlashSendMessageType, - timeUnits, - type BaseBushArgumentType, - type BushInspectOptions, - type SlashMessage -} from '#lib'; -import { humanizeDuration as humanizeDurationMod } from '@notenoughupdates/humanize-duration'; -import assert from 'assert/strict'; -import cp from 'child_process'; -import deepLock from 'deep-lock'; -import { Util as AkairoUtil } from 'discord-akairo'; -import { - Constants as DiscordConstants, - EmbedBuilder, - Message, - OAuth2Scopes, - PermissionFlagsBits, - PermissionsBitField, - type APIEmbed, - type APIMessage, - type CommandInteraction, - type InteractionReplyOptions, - type PermissionsString -} from 'discord.js'; -import got from 'got'; -import { DeepWritable } from 'ts-essentials'; -import { inspect as inspectUtil, promisify } from 'util'; -import * as Format from '../common/util/Format.js'; - -export type StripPrivate = { [K in keyof T]: T[K] extends Record ? StripPrivate : T[K] }; -export type ValueOf = T[keyof T]; - -/** - * Capitalizes the first letter of the given text - * @param text The text to capitalize - * @returns The capitalized text - */ -export function capitalize(text: string): string { - return text.charAt(0).toUpperCase() + text.slice(1); -} - -export const exec = promisify(cp.exec); - -/** - * Runs a shell command and gives the output - * @param command The shell command to run - * @returns The stdout and stderr of the shell command - */ -export async function shell(command: string): Promise<{ stdout: string; stderr: string }> { - return await exec(command); -} - -/** - * Appends the correct ordinal to the given number - * @param n The number to append an ordinal to - * @returns The number with the ordinal - */ -export function ordinal(n: number): string { - const s = ['th', 'st', 'nd', 'rd'], - v = n % 100; - return n + (s[(v - 20) % 10] || s[v] || s[0]); -} - -/** - * Chunks an array to the specified size - * @param arr The array to chunk - * @param perChunk The amount of items per chunk - * @returns The chunked array - */ -export function chunk(arr: T[], perChunk: number): T[][] { - return arr.reduce((all, one, i) => { - const ch: number = Math.floor(i / perChunk); - (all as any[])[ch] = [].concat(all[ch] || [], one as any); - return all; - }, []); -} - -/** - * Fetches a user's uuid from the mojang api. - * @param username The username to get the uuid of. - * @returns The the uuid of the user. - */ -export async function mcUUID(username: string, dashed = false): Promise { - const apiRes = (await got.get(`https://api.ashcon.app/mojang/v2/user/${username}`).json()) as UuidRes; - return dashed ? apiRes.uuid : apiRes.uuid.replace(/-/g, ''); -} - -export interface UuidRes { - uuid: string; - username: string; - username_history?: { username: string }[] | null; - textures: { - custom: boolean; - slim: boolean; - skin: { - url: string; - data: string; - }; - raw: { - value: string; - signature: string; - }; - }; - created_at: string; -} - -/** - * Generate defaults for {@link inspect}. - * @param options The options to create defaults with. - * @returns The default options combined with the specified options. - */ -function getDefaultInspectOptions(options?: BushInspectOptions): BushInspectOptions { - return { - showHidden: options?.showHidden ?? false, - depth: options?.depth ?? 2, - colors: options?.colors ?? false, - customInspect: options?.customInspect ?? true, - showProxy: options?.showProxy ?? false, - maxArrayLength: options?.maxArrayLength ?? Infinity, - maxStringLength: options?.maxStringLength ?? Infinity, - breakLength: options?.breakLength ?? 80, - compact: options?.compact ?? 3, - sorted: options?.sorted ?? false, - getters: options?.getters ?? true, - numericSeparator: options?.numericSeparator ?? true - }; -} - -/** - * Uses {@link inspect} with custom defaults. - * @param object - The object you would like to inspect. - * @param options - The options you would like to use to inspect the object. - * @returns The inspected object. - */ -export function inspect(object: any, options?: BushInspectOptions): string { - const optionsWithDefaults = getDefaultInspectOptions(options); - - if (!optionsWithDefaults.inspectStrings && typeof object === 'string') return object; - - return inspectUtil(object, optionsWithDefaults); -} - -/** - * Responds to a slash command interaction. - * @param interaction The interaction to respond to. - * @param responseOptions The options for the response. - * @returns The message sent. - */ -export async function slashRespond( - interaction: CommandInteraction, - responseOptions: SlashSendMessageType | SlashEditMessageType -): Promise { - const newResponseOptions = typeof responseOptions === 'string' ? { content: responseOptions } : responseOptions; - if (interaction.replied || interaction.deferred) { - delete (newResponseOptions as InteractionReplyOptions).ephemeral; // Cannot change a preexisting message to be ephemeral - return (await interaction.editReply(newResponseOptions)) as Message | APIMessage; - } else { - await interaction.reply(newResponseOptions); - return await interaction.fetchReply().catch(() => undefined); - } -} - -/** - * Takes an array and combines the elements using the supplied conjunction. - * @param array The array to combine. - * @param conjunction The conjunction to use. - * @param ifEmpty What to return if the array is empty. - * @returns The combined elements or `ifEmpty`. - * - * @example - * const permissions = oxford(['Administrator', 'SendMessages', 'ManageMessages'], 'and', 'none'); - * console.log(permissions); // Administrator, SendMessages and ManageMessages - */ -export function oxford(array: string[], conjunction: string, ifEmpty?: string): string | undefined { - const l = array.length; - if (!l) return ifEmpty; - if (l < 2) return array[0]; - if (l < 3) return array.join(` ${conjunction} `); - array = array.slice(); - array[l - 1] = `${conjunction} ${array[l - 1]}`; - return array.join(', '); -} - -/** - * Add or remove an item from an array. All duplicates will be removed. - * @param action Either `add` or `remove` an element. - * @param array The array to add/remove an element from. - * @param value The element to add/remove from the array. - */ -export function addOrRemoveFromArray(action: 'add' | 'remove', array: T[], value: T): T[] { - const set = new Set(array); - action === 'add' ? set.add(value) : set.delete(value); - return [...set]; -} - -/** - * Remove an item from an array. All duplicates will be removed. - * @param array The array to remove an element from. - * @param value The element to remove from the array. - */ -export function removeFromArray(array: T[], value: T): T[] { - return addOrRemoveFromArray('remove', array, value); -} - -/** - * Add an item from an array. All duplicates will be removed. - * @param array The array to add an element to. - * @param value The element to add to the array. - */ -export function addToArray(array: T[], value: T): T[] { - return addOrRemoveFromArray('add', array, value); -} - -/** - * Surrounds a string to the begging an end of each element in an array. - * @param array The array you want to surround. - * @param surroundChar1 The character placed in the beginning of the element. - * @param surroundChar2 The character placed in the end of the element. Defaults to `surroundChar1`. - */ -export function surroundArray(array: string[], surroundChar1: string, surroundChar2?: string): string[] { - return array.map((a) => `${surroundChar1}${a}${surroundChar2 ?? surroundChar1}`); -} - -/** - * Gets the duration from a specified string. - * @param content The string to look for a duration in. - * @param remove Whether or not to remove the duration from the original string. - * @returns The {@link ParsedDuration}. - */ -export function parseDuration(content: string, remove = true): ParsedDuration { - if (!content) return { duration: 0, content: null }; - - // eslint-disable-next-line prefer-const - let duration: number | null = null; - // Try to reduce false positives by requiring a space before the duration, this makes sure it still matches if it is - // in the beginning of the argument - let contentWithoutTime = ` ${content}`; - - for (const unit in timeUnits) { - const regex = timeUnits[unit as keyof typeof timeUnits].match; - const match = regex.exec(contentWithoutTime); - const value = Number(match?.groups?.[unit]); - if (!isNaN(value)) duration! += value * timeUnits[unit as keyof typeof timeUnits].value; - - if (remove) contentWithoutTime = contentWithoutTime.replace(regex, ''); - } - // remove the space added earlier - if (contentWithoutTime.startsWith(' ')) contentWithoutTime.replace(' ', ''); - return { duration, content: contentWithoutTime }; -} - -export interface ParsedDuration { - duration: number | null; - content: string | null; -} - -/** - * Converts a duration in milliseconds to a human readable form. - * @param duration The duration in milliseconds to convert. - * @param largest The maximum number of units to display for the duration. - * @param round Whether or not to round the smallest unit displayed. - * @returns A humanized string of the duration. - */ -export function humanizeDuration(duration: number, largest?: number, round = true): string { - if (largest) return humanizeDurationMod(duration, { language: 'en', maxDecimalPoints: 2, largest, round })!; - else return humanizeDurationMod(duration, { language: 'en', maxDecimalPoints: 2, round })!; -} - -/** - * Creates a formatted relative timestamp from a duration in milliseconds. - * @param duration The duration in milliseconds. - * @returns The formatted relative timestamp. - */ -export function timestampDuration(duration: number): string { - return ``; -} - -/** - * Creates a timestamp from a date. - * @param date The date to create a timestamp from. - * @param style The style of the timestamp. - * @returns The formatted timestamp. - * - * @see - * **Styles:** - * - **t**: Short Time ex. `16:20` - * - **T**: Long Time ex. `16:20:30 ` - * - **d**: Short Date ex. `20/04/2021` - * - **D**: Long Date ex. `20 April 2021` - * - **f**: Short Date/Time ex. `20 April 2021 16:20` - * - **F**: Long Date/Time ex. `Tuesday, 20 April 2021 16:20` - * - **R**: Relative Time ex. `2 months ago` - */ -export function timestamp( - date: D, - style: TimestampStyle = 'f' -): D extends Date ? string : undefined { - if (!date) return date as unknown as D extends Date ? string : undefined; - return `` as unknown as D extends Date ? string : undefined; -} - -export type TimestampStyle = 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R'; - -/** - * Creates a human readable representation between a date and the current time. - * @param date The date to be compared with the current time. - * @param largest The maximum number of units to display for the duration. - * @param round Whether or not to round the smallest unit displayed. - * @returns A humanized string of the delta. - */ -export function dateDelta(date: Date, largest = 3, round = true): string { - return humanizeDuration(new Date().getTime() - date.getTime(), largest, round); -} - -/** - * Combines {@link timestamp} and {@link dateDelta} - * @param date The date to be compared with the current time. - * @param style The style of the timestamp. - * @returns The formatted timestamp. - * - * @see - * **Styles:** - * - **t**: Short Time ex. `16:20` - * - **T**: Long Time ex. `16:20:30 ` - * - **d**: Short Date ex. `20/04/2021` - * - **D**: Long Date ex. `20 April 2021` - * - **f**: Short Date/Time ex. `20 April 2021 16:20` - * - **F**: Long Date/Time ex. `Tuesday, 20 April 2021 16:20` - * - **R**: Relative Time ex. `2 months ago` - */ -export function timestampAndDelta(date: Date, style: TimestampStyle = 'D'): string { - return `${timestamp(date, style)} (${dateDelta(date)} ago)`; -} - -/** - * Convert a hex code to an rbg value. - * @param hex The hex code to convert. - * @returns The rbg value. - */ -export function hexToRgb(hex: string): string { - const arrBuff = new ArrayBuffer(4); - const vw = new DataView(arrBuff); - vw.setUint32(0, parseInt(hex, 16), false); - const arrByte = new Uint8Array(arrBuff); - - return `${arrByte[1]}, ${arrByte[2]}, ${arrByte[3]}`; -} - -/** - * Wait an amount in milliseconds. - * @returns A promise that resolves after the specified amount of milliseconds - */ -export const sleep = promisify(setTimeout); - -/** - * List the methods of an object. - * @param obj The object to get the methods of. - * @returns A string with each method on a new line. - */ -export function getMethods(obj: Record): string { - // modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class/31055217#31055217 - let props: string[] = []; - let obj_: Record = new Object(obj); - - do { - const l = Object.getOwnPropertyNames(obj_) - .concat(Object.getOwnPropertySymbols(obj_).map((s) => s.toString())) - .sort() - .filter( - (p, i, arr) => - typeof Object.getOwnPropertyDescriptor(obj_, p)?.['get'] !== 'function' && // ignore getters - typeof Object.getOwnPropertyDescriptor(obj_, p)?.['set'] !== 'function' && // ignore setters - typeof obj_[p] === 'function' && // only the methods - p !== 'constructor' && // not the constructor - (i == 0 || p !== arr[i - 1]) && // not overriding in this prototype - props.indexOf(p) === -1 // not overridden in a child - ); - - const reg = /\(([\s\S]*?)\)/; - props = props.concat( - l.map( - (p) => - `${obj_[p] && obj_[p][Symbol.toStringTag] === 'AsyncFunction' ? 'async ' : ''}function ${p}(${ - reg.exec(obj_[p].toString())?.[1] - ? reg - .exec(obj_[p].toString())?.[1] - .split(', ') - .map((arg) => arg.split('=')[0].trim()) - .join(', ') - : '' - });` - ) - ); - } while ( - (obj_ = Object.getPrototypeOf(obj_)) && // walk-up the prototype chain - Object.getPrototypeOf(obj_) // not the the Object prototype methods (hasOwnProperty, etc...) - ); - - return props.join('\n'); -} - -/** - * List the symbols of an object. - * @param obj The object to get the symbols of. - * @returns An array of the symbols of the object. - */ -export function getSymbols(obj: Record): symbol[] { - let symbols: symbol[] = []; - let obj_: Record = new Object(obj); - - do { - const l = Object.getOwnPropertySymbols(obj_).sort(); - - symbols = [...symbols, ...l]; - } while ( - (obj_ = Object.getPrototypeOf(obj_)) && // walk-up the prototype chain - Object.getPrototypeOf(obj_) // not the the Object prototype methods (hasOwnProperty, etc...) - ); - - return symbols; -} - -/** - * Checks if a user has a certain guild permission (doesn't check channel permissions). - * @param message The message to check the user from. - * @param permissions The permissions to check for. - * @returns The missing permissions or null if none are missing. - */ -export function userGuildPermCheck( - message: CommandMessage | SlashMessage, - permissions: typeof PermissionFlagsBits[keyof typeof PermissionFlagsBits][] -): PermissionsString[] | null { - if (!message.inGuild()) return null; - const missing = message.member?.permissions.missing(permissions) ?? []; - - return missing.length ? missing : null; -} - -/** - * Check if the client has certain permissions in the guild (doesn't check channel permissions). - * @param message The message to check the client user from. - * @param permissions The permissions to check for. - * @returns The missing permissions or null if none are missing. - */ -export function clientGuildPermCheck(message: CommandMessage | SlashMessage, permissions: bigint[]): PermissionsString[] | null { - const missing = message.guild?.members.me?.permissions.missing(permissions) ?? []; - - return missing.length ? missing : null; -} - -/** - * Check if the client has permission to send messages in the channel as well as check if they have other permissions - * in the guild (or the channel if `checkChannel` is `true`). - * @param message The message to check the client user from. - * @param permissions The permissions to check for. - * @param checkChannel Whether to check the channel permissions instead of the guild permissions. - * @returns The missing permissions or null if none are missing. - */ -export function clientSendAndPermCheck( - message: CommandMessage | SlashMessage, - permissions: bigint[] = [], - checkChannel = false -): PermissionsString[] | null { - if (!message.inGuild() || !message.channel) return null; - - const missing: PermissionsString[] = []; - const sendPerm = message.channel.isThread() ? 'SendMessages' : 'SendMessagesInThreads'; - - // todo: remove once forum channels are fixed - if (message.channel.parent === null && message.channel.isThread()) return null; - - if (!message.guild.members.me!.permissionsIn(message.channel!.id).has(sendPerm)) missing.push(sendPerm); - - missing.push( - ...(checkChannel - ? message.guild!.members.me!.permissionsIn(message.channel!.id!).missing(permissions) - : clientGuildPermCheck(message, permissions) ?? []) - ); - - return missing.length ? missing : null; -} - -export { deepLock as deepFreeze }; -export { Arg as arg }; -export { Format as format }; -export { DiscordConstants as discordConstants }; -export { AkairoUtil as akairo }; - -/** - * The link to invite the bot with all permissions. - */ -export function invite(client: BushClient) { - return client.generateInvite({ - permissions: - PermissionsBitField.All - - PermissionFlagsBits.UseEmbeddedActivities - - PermissionFlagsBits.ViewGuildInsights - - PermissionFlagsBits.Stream, - scopes: [OAuth2Scopes.Bot, OAuth2Scopes.ApplicationsCommands] - }); -} - -/** - * Asset multiple statements at a time. - * @param args - */ -export function assertAll(...args: any[]): void { - for (let i = 0; i < args.length; i++) { - assert(args[i], `assertAll index ${i} failed`); - } -} - -/** - * Casts a string to a duration and reason for slash commands. - * @param arg The argument received. - * @param message The message that triggered the command. - * @returns The casted argument. - */ -export async function castDurationContent( - arg: string | ParsedDuration | null, - message: CommandMessage | SlashMessage -): Promise { - const res = typeof arg === 'string' ? await Arg.cast('contentWithDuration', message, arg) : arg; - - return { duration: res?.duration ?? 0, content: res?.content ?? '' }; -} - -export interface ParsedDurationRes { - duration: number; - content: string; -} - -/** - * Casts a string to a the specified argument type. - * @param type The type of the argument to cast to. - * @param arg The argument received. - * @param message The message that triggered the command. - * @returns The casted argument. - */ -export async function cast( - type: T, - arg: BaseBushArgumentType[T] | string, - message: CommandMessage | SlashMessage -) { - return typeof arg === 'string' ? await Arg.cast(type, message, arg) : arg; -} - -/** - * Overflows the description of an embed into multiple embeds. - * @param embed The options to be applied to the (first) embed. - * @param lines Each line of the description as an element in an array. - */ -export function overflowEmbed(embed: Omit, lines: string[], maxLength = 4096): EmbedBuilder[] { - const embeds: EmbedBuilder[] = []; - - const makeEmbed = () => { - embeds.push(new EmbedBuilder().setColor(embed.color ?? null)); - return embeds.at(-1)!; - }; - - for (const line of lines) { - let current = embeds.length ? embeds.at(-1)! : makeEmbed(); - let joined = current.data.description ? `${current.data.description}\n${line}` : line; - if (joined.length > maxLength) { - current = makeEmbed(); - joined = line; - } - - current.setDescription(joined); - } - - if (!embeds.length) makeEmbed(); - - if (embed.author) embeds.at(0)?.setAuthor(embed.author); - if (embed.title) embeds.at(0)?.setTitle(embed.title); - if (embed.url) embeds.at(0)?.setURL(embed.url); - if (embed.fields) embeds.at(-1)?.setFields(embed.fields); - if (embed.thumbnail) embeds.at(-1)?.setThumbnail(embed.thumbnail.url); - if (embed.footer) embeds.at(-1)?.setFooter(embed.footer); - if (embed.image) embeds.at(-1)?.setImage(embed.image.url); - if (embed.timestamp) embeds.at(-1)?.setTimestamp(new Date(embed.timestamp)); - - return embeds; -} - -/** - * Formats an error into a string. - * @param error The error to format. - * @param colors Whether to use colors in the output. - * @returns The formatted error. - */ -export function formatError(error: Error | any, colors = false): string { - if (!error) return error; - if (typeof error !== 'object') return String.prototype.toString.call(error); - if ( - getSymbols(error) - .map((s) => s.toString()) - .includes('Symbol(nodejs.util.inspect.custom)') - ) - return inspect(error, { colors }); - - return error.stack; -} - -export function deepWriteable(obj: T): DeepWritable { - return obj as DeepWritable; -} diff --git a/src/lib/utils/CanvasProgressBar.ts b/src/lib/utils/CanvasProgressBar.ts deleted file mode 100644 index fb4f778..0000000 --- a/src/lib/utils/CanvasProgressBar.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { CanvasRenderingContext2D } from 'canvas'; - -/** - * I just copy pasted this code from stackoverflow don't yell at me if there is issues for it - * @author @TymanWasTaken - */ -export class CanvasProgressBar { - private readonly x: number; - private readonly y: number; - private readonly w: number; - private readonly h: number; - private readonly color: string; - private percentage: number; - private p?: number; - private ctx: CanvasRenderingContext2D; - - public constructor( - ctx: CanvasRenderingContext2D, - dimension: { x: number; y: number; width: number; height: number }, - color: string, - percentage: number - ) { - ({ x: this.x, y: this.y, width: this.w, height: this.h } = dimension); - this.color = color; - this.percentage = percentage; - this.p = undefined; - this.ctx = ctx; - } - - public draw(): void { - // ----------------- - this.p = this.percentage * this.w; - if (this.p <= this.h) { - this.ctx.beginPath(); - this.ctx.arc( - this.h / 2 + this.x, - this.h / 2 + this.y, - this.h / 2, - Math.PI - Math.acos((this.h - this.p) / this.h), - Math.PI + Math.acos((this.h - this.p) / this.h) - ); - this.ctx.save(); - this.ctx.scale(-1, 1); - this.ctx.arc( - this.h / 2 - this.p - this.x, - this.h / 2 + this.y, - this.h / 2, - Math.PI - Math.acos((this.h - this.p) / this.h), - Math.PI + Math.acos((this.h - this.p) / this.h) - ); - this.ctx.restore(); - this.ctx.closePath(); - } else { - this.ctx.beginPath(); - this.ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, (3 / 2) * Math.PI); - this.ctx.lineTo(this.p - this.h + this.x, 0 + this.y); - this.ctx.arc(this.p - this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, (3 / 2) * Math.PI, Math.PI / 2); - this.ctx.lineTo(this.h / 2 + this.x, this.h + this.y); - this.ctx.closePath(); - } - this.ctx.fillStyle = this.color; - this.ctx.fill(); - } - - // public showWholeProgressBar(){ - // this.ctx.beginPath(); - // this.ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, 3 / 2 * Math.PI); - // this.ctx.lineTo(this.w - this.h + this.x, 0 + this.y); - // this.ctx.arc(this.w - this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, 3 / 2 *Math.PI, Math.PI / 2); - // this.ctx.lineTo(this.h / 2 + this.x, this.h + this.y); - // this.ctx.strokeStyle = '#000000'; - // this.ctx.stroke(); - // this.ctx.closePath(); - // } - - public get PPercentage(): number { - return this.percentage * 100; - } - - public set PPercentage(x: number) { - this.percentage = x / 100; - } -} diff --git a/src/listeners/automod/automodCreate.ts b/src/listeners/automod/automodCreate.ts new file mode 100644 index 0000000..529651c --- /dev/null +++ b/src/listeners/automod/automodCreate.ts @@ -0,0 +1,16 @@ +import { BushListener, MessageAutomod, type BushClientEvents } from '#lib'; + +export default class AutomodMessageCreateListener extends BushListener { + public constructor() { + super('automodCreate', { + emitter: 'client', + event: 'messageCreate', + category: 'message' + }); + } + + public async exec(...[message]: BushClientEvents['messageCreate']) { + if (message.member === null) return; + return new MessageAutomod(message); + } +} diff --git a/src/listeners/automod/automodUpdate.ts b/src/listeners/automod/automodUpdate.ts new file mode 100644 index 0000000..d68759f --- /dev/null +++ b/src/listeners/automod/automodUpdate.ts @@ -0,0 +1,17 @@ +import { BushListener, MessageAutomod, type BushClientEvents } from '#lib'; + +export default class AutomodMessageUpdateListener extends BushListener { + public constructor() { + super('automodUpdate', { + emitter: 'client', + event: 'messageUpdate', + category: 'message' + }); + } + + public async exec(...[_, newMessage]: BushClientEvents['messageUpdate']) { + const fullMessage = newMessage.partial ? await newMessage.fetch().catch(() => null) : newMessage; + if (!fullMessage?.member) return; + return new MessageAutomod(fullMessage); + } +} diff --git a/src/listeners/automod/memberAutomod.ts b/src/listeners/automod/memberAutomod.ts new file mode 100644 index 0000000..01eb56c --- /dev/null +++ b/src/listeners/automod/memberAutomod.ts @@ -0,0 +1,21 @@ +import { BushClientEvents, BushListener, MemberAutomod } from '#lib'; +import chalk from 'chalk'; + +export default class PresenceAutomodListener extends BushListener { + public constructor() { + super('memberAutomod', { + emitter: 'client', + event: 'guildMemberUpdate' + }); + } + + public async exec(...[_, newMember]: BushClientEvents['guildMemberUpdate']) { + if (!(await newMember.guild.hasFeature('automodMembers'))) return; + if (!(await newMember.guild.hasFeature('automod'))) return; + + new MemberAutomod(newMember); + console.log( + `${chalk.hex('#ff7105')('[MemberAutomod]')} Created a new MemberAutomod for ${newMember.user.tag} (${newMember.user.id})` + ); + } +} diff --git a/src/listeners/automod/presenceAutomod.ts b/src/listeners/automod/presenceAutomod.ts new file mode 100644 index 0000000..f361508 --- /dev/null +++ b/src/listeners/automod/presenceAutomod.ts @@ -0,0 +1,27 @@ +import { BushClientEvents, BushListener, PresenceAutomod } from '#lib'; +import chalk from 'chalk'; + +/* export default */ class PresenceAutomodListener extends BushListener { + public constructor() { + super('presenceAutomod', { + emitter: 'client', + event: 'presenceUpdate' + }); + } + + public async exec(...[_, newPresence]: BushClientEvents['presenceUpdate']) { + if (!newPresence.member || !newPresence.guild) return; + + if (!newPresence.activities.length) return; + + if (!(await newPresence.guild.hasFeature('automodPresence'))) return; + if (!(await newPresence.guild.hasFeature('automod'))) return; + + new PresenceAutomod(newPresence); + console.log( + `${chalk.hex('#ffe605')('[PresenceAutomod]')} Created a new PresenceAutomod for ${newPresence.member.user.tag} (${ + newPresence.member.user.id + })` + ); + } +} diff --git a/src/listeners/commands/commandError.ts b/src/listeners/commands/commandError.ts index f12881a..bfa857c 100644 --- a/src/listeners/commands/commandError.ts +++ b/src/listeners/commands/commandError.ts @@ -1,8 +1,8 @@ import { capitalize, colors, format, formatError, SlashMessage, type BushCommandHandlerEvents } from '#lib'; import { type AkairoMessage, type Command } from 'discord-akairo'; import { ChannelType, Client, EmbedBuilder, escapeInlineCode, GuildTextBasedChannel, type Message } from 'discord.js'; -import { bold } from '../../lib/common/util/Format.js'; -import { BushListener } from '../../lib/extensions/discord-akairo/BushListener.js'; +import { BushListener } from '../../../lib/extensions/discord-akairo/BushListener.js'; +import { bold } from '../../../lib/utils/Format.js'; export default class CommandErrorListener extends BushListener { public constructor() { diff --git a/src/listeners/interaction/interactionCreate.ts b/src/listeners/interaction/interactionCreate.ts index 91bcae6..8dd753b 100644 --- a/src/listeners/interaction/interactionCreate.ts +++ b/src/listeners/interaction/interactionCreate.ts @@ -1,4 +1,4 @@ -import { AutoMod, BushListener, emojis, format, oxford, surroundArray, type BushClientEvents } from '#lib'; +import { BushListener, emojis, format, handleAutomodInteraction, oxford, surroundArray, type BushClientEvents } from '#lib'; import { InteractionType } from 'discord.js'; export default class InteractionCreateListener extends BushListener { @@ -22,7 +22,7 @@ export default class InteractionCreateListener extends BushListener { } else if (interaction.isButton()) { const id = interaction.customId; if (['paginate_', 'command_', 'confirmationPrompt_', 'appeal'].some((s) => id.startsWith(s))) return; - else if (id.startsWith('automod;')) void AutoMod.handleInteraction(interaction); + else if (id.startsWith('automod;')) void handleAutomodInteraction(interaction); else if (id.startsWith('button-role;') && interaction.inCachedGuild()) { const [, roleId] = id.split(';'); const role = interaction.guild.roles.cache.get(roleId); diff --git a/src/listeners/member-custom/bushLevelUpdate.ts b/src/listeners/member-custom/bushLevelUpdate.ts index 0281288..702f7cc 100644 --- a/src/listeners/member-custom/bushLevelUpdate.ts +++ b/src/listeners/member-custom/bushLevelUpdate.ts @@ -52,10 +52,12 @@ export default class BushLevelUpdateListener extends BushListener { } try { if (promises.length) await Promise.all(promises); - } catch (e) { + } catch (e: any) { await member.guild.error( 'bushLevelUpdate', - `There was an error adding level roles to ${member.user.tag} upon reaching to level ${newLevel}.\n${e?.message ?? e}` + `There was an error adding level roles to ${member.user.tag} upon reaching to level ${newLevel}.\n${ + 'message' in e ? e.message : e + }` ); } } diff --git a/src/listeners/message/automodCreate.ts b/src/listeners/message/automodCreate.ts deleted file mode 100644 index 0f089a3..0000000 --- a/src/listeners/message/automodCreate.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { AutoMod, BushListener, type BushClientEvents } from '#lib'; - -export default class AutomodMessageCreateListener extends BushListener { - public constructor() { - super('automodCreate', { - emitter: 'client', - event: 'messageCreate', - category: 'message' - }); - } - - public async exec(...[message]: BushClientEvents['messageCreate']) { - return new AutoMod(message); - } -} diff --git a/src/listeners/message/automodUpdate.ts b/src/listeners/message/automodUpdate.ts deleted file mode 100644 index fa00f92..0000000 --- a/src/listeners/message/automodUpdate.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AutoMod, BushListener, type BushClientEvents } from '#lib'; - -export default class AutomodMessageUpdateListener extends BushListener { - public constructor() { - super('automodUpdate', { - emitter: 'client', - event: 'messageUpdate', - category: 'message' - }); - } - - public async exec(...[_, newMessage]: BushClientEvents['messageUpdate']) { - const fullMessage = newMessage.partial ? await newMessage.fetch().catch(() => null) : newMessage; - if (!fullMessage) return; - return new AutoMod(fullMessage); - } -} diff --git a/src/tasks/cache/updateCache.ts b/src/tasks/cache/updateCache.ts index 87636e8..595a872 100644 --- a/src/tasks/cache/updateCache.ts +++ b/src/tasks/cache/updateCache.ts @@ -1,8 +1,8 @@ -import { Time } from '#constants'; import { Global, Guild, Shared, type BushClient } from '#lib'; import type { Client } from 'discord.js'; import config from '../../../config/options.js'; -import { BushTask } from '../../lib/extensions/discord-akairo/BushTask.js'; +import { BushTask } from '../../../lib/extensions/discord-akairo/BushTask.js'; +import { Time } from '../../../lib/utils/BushConstants.js'; export default class UpdateCacheTask extends BushTask { public constructor() { diff --git a/src/tasks/cache/updateHighlightCache.ts b/src/tasks/cache/updateHighlightCache.ts index 44ddd90..4ab5544 100644 --- a/src/tasks/cache/updateHighlightCache.ts +++ b/src/tasks/cache/updateHighlightCache.ts @@ -1,5 +1,5 @@ -import { Time } from '#constants'; -import { BushTask } from '../../lib/extensions/discord-akairo/BushTask.js'; +import { BushTask } from '../../../lib/extensions/discord-akairo/BushTask.js'; +import { Time } from '../../../lib/utils/BushConstants.js'; export default class UpdateHighlightCacheTask extends BushTask { public constructor() { diff --git a/src/tasks/cache/updatePriceItemCache.ts b/src/tasks/cache/updatePriceItemCache.ts index 9809cbd..55115cc 100644 --- a/src/tasks/cache/updatePriceItemCache.ts +++ b/src/tasks/cache/updatePriceItemCache.ts @@ -12,7 +12,12 @@ export default class UpdatePriceItemCache extends BushTask { public async exec() { const [bazaar, currentLowestBIN, averageLowestBIN, auctionAverages] = (await Promise.all( - PriceCommand.urls.map(({ url }) => got.get(url).json().catch(undefined)) + PriceCommand.urls.map(({ url }) => + got + .get(url) + .json() + .catch(() => undefined) + ) )) as [Bazaar?, LowestBIN?, LowestBIN?, AuctionAverages?]; const itemNames = new Set([ diff --git a/src/tasks/feature/handleReminders.ts b/src/tasks/feature/handleReminders.ts index 7863c9a..1e44083 100644 --- a/src/tasks/feature/handleReminders.ts +++ b/src/tasks/feature/handleReminders.ts @@ -29,7 +29,8 @@ export default class HandlerRemindersTask extends BushTask { void this.client.users .send( entry.user, - `The reminder you set ${dateDelta(entry.created)} ago has expired: ${format.bold(entry.content)}\n${entry.messageUrl}` + `The reminder you set ${dateDelta(entry.created)} ago has expired: ${format.bold(entry.content)} +${entry.messageUrl}` ) .catch(() => false); void entry.update({ notified: true }); diff --git a/src/tasks/stats/guildCount.ts b/src/tasks/stats/guildCount.ts index 262f00c..f52dc95 100644 --- a/src/tasks/stats/guildCount.ts +++ b/src/tasks/stats/guildCount.ts @@ -1,5 +1,5 @@ import { BushTask, Time } from '#lib'; -import { GuildCount } from '../../lib/models/shared/GuildCount.js'; +import { GuildCount } from '../../../lib/models/shared/GuildCount.js'; export default class GuildCountTask extends BushTask { public constructor() { diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 0000000..6d2834a --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../dist/src", + "composite": true + }, + "references": [{ "path": "../lib" }, { "path": "../config" }], + "include": ["src/**/*.ts"] +} diff --git a/test.js b/test.js deleted file mode 100644 index d0840ec..0000000 --- a/test.js +++ /dev/null @@ -1,365 +0,0 @@ -/* eslint-disable */ -// @ts-check - -import { createCanvas, registerFont } from 'canvas'; -import fs from 'fs/promises'; -import path, { dirname, join } from 'path'; -import tinycolor from 'tinycolor2'; -import { fileURLToPath } from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -registerFont(join(__dirname, 'assets', 'Faithful.ttf'), { family: 'ComplianceSans' }); -registerFont(join(dirname(fileURLToPath(import.meta.url)), 'assets', 'Roboto-Regular.ttf'), { family: 'Roboto' }); - -/** @typedef {string} McItemId */ -/** @typedef {string} SbItemId */ -/** @typedef {string} MojangJson */ -/** @typedef {`${SbItemId}:${number}` | ''} SbRecipeItem */ -/** @typedef {{[Location in `${'A' | 'B' | 'C'}${1 | 2 | 3}`]: SbRecipeItem;}} SbRecipe */ -/** @typedef {'WIKI_URL' | ''} InfoType */ -/** @typedef {`${'WOLF' | 'BLAZE' | 'EMAN'}_${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`} Slayer */ -/** @typedef {'0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'a'|'b'|'c'|'d'|'e'|'f'} code */ -/** - * @typedef RawNeuItem - * @property {McItemId} itemid - * @property {string} displayname - * @property {MojangJson} nbttag - * @property {number} damage - * @property {string[]} lore - * @property {SbRecipe} [recipe] - * @property {SbItemId} internalname - * @property {InfoType} infoType - * @property {string[]} [info] - * @property {string} crafttext - * @property {boolean} [vanilla] - * @property {boolean} [useneucraft] - * @property {Slayer} [slayer_req] - * @property {string} [clickcommand] - * @property {number} [x] - * @property {number} [y] - * @property {number} [z] - * @property {string} [island] - * @property {{ type: string; cost: any[]; result: SbItemId }[]} [recipes] - * @property {SbItemId} [parent] - * @property {boolean} [noseal] - */ - -const FormattingCodes = { - Black: '§0', - DarkBlue: '§1', - DarkGreen: '§2', - DarkAqua: '§3', - DarkRed: '§4', - DarkPurple: '§5', - Gold: '§6', - Gray: '§7', - DarkGray: '§8', - Blue: '§9', - Green: '§a', - Aqua: '§b', - Red: '§c', - LightPurple: '§d', - Yellow: '§e', - White: '§f', - - Obfuscated: '§k', - Bold: '§l', - Strikethrough: '§m', - Underline: '§n', - Italic: '§o', - Reset: '§r' -}; - -const formattingInfo = { - [FormattingCodes.Black]: { - foreground: 'rgb(0, 0, 0)', - foregroundDarker: 'rgb(0, 0, 0)', - background: 'rgb(0, 0, 0)', - backgroundDarker: 'rgb(0, 0, 0)', - ansi: '\u001b[0;30m' - }, - [FormattingCodes.DarkBlue]: { - foreground: 'rgb(0, 0, 170)', - foregroundDarker: 'rgb(0, 0, 118)', - background: 'rgb(0, 0, 42)', - backgroundDarker: 'rgb(0, 0, 29)', - ansi: '\u001b[0;34m' - }, - [FormattingCodes.DarkGreen]: { - foreground: 'rgb(0, 170, 0)', - foregroundDarker: 'rgb(0, 118, 0)', - background: 'rgb(0, 42, 0)', - backgroundDarker: 'rgb(0, 29, 0)', - ansi: '\u001b[0;32m' - }, - [FormattingCodes.DarkAqua]: { - foreground: 'rgb(0, 170, 170)', - foregroundDarker: 'rgb(0, 118, 118)', - background: 'rgb(0, 42, 42)', - backgroundDarker: 'rgb(0, 29, 29)', - ansi: '\u001b[0;36m' - }, - [FormattingCodes.DarkRed]: { - foreground: 'rgb(170, 0, 0)', - foregroundDarker: 'rgb(118, 0, 0)', - background: 'rgb(42, 0, 0)', - backgroundDarker: 'rgb(29, 0, 0)', - ansi: '\u001b[0;31m' - }, - [FormattingCodes.DarkPurple]: { - foreground: 'rgb(170, 0, 170)', - foregroundDarker: 'rgb(118, 0, 118)', - background: 'rgb(42, 0, 42)', - backgroundDarker: 'rgb(29, 0, 29)', - ansi: '\u001b[0;35m' - }, - [FormattingCodes.Gold]: { - foreground: 'rgb(255, 170, 0)', - foregroundDarker: 'rgb(178, 118, 0)', - background: 'rgb(42, 42, 0)', - backgroundDarker: 'rgb(29, 29, 0)', - ansi: '\u001b[0;33m' - }, - [FormattingCodes.Gray]: { - foreground: 'rgb(170, 170, 170)', - foregroundDarker: 'rgb(118, 118, 118)', - background: 'rgb(42, 42, 42)', - backgroundDarker: 'rgb(29, 29, 29)', - ansi: '\u001b[0;37m' - }, - [FormattingCodes.DarkGray]: { - foreground: 'rgb(85, 85, 85)', - foregroundDarker: 'rgb(59, 59, 59)', - background: 'rgb(21, 21, 21)', - backgroundDarker: 'rgb(14, 14, 14)', - ansi: '\u001b[0;90m' - }, - [FormattingCodes.Blue]: { - foreground: 'rgb(85, 85, 255)', - foregroundDarker: 'rgb(59, 59, 178)', - background: 'rgb(21, 21, 63)', - backgroundDarker: 'rgb(14, 14, 44)', - ansi: '\u001b[0;94m' - }, - [FormattingCodes.Green]: { - foreground: 'rgb(85, 255, 85)', - foregroundDarker: 'rgb(59, 178, 59)', - background: 'rgb(21, 63, 21)', - backgroundDarker: 'rgb(14, 44, 14)', - ansi: '\u001b[0;92m' - }, - [FormattingCodes.Aqua]: { - foreground: 'rgb(85, 255, 255)', - foregroundDarker: 'rgb(59, 178, 178)', - background: 'rgb(21, 63, 63)', - backgroundDarker: 'rgb(14, 44, 44)', - ansi: '\u001b[0;96m' - }, - [FormattingCodes.Red]: { - foreground: 'rgb(255, 85, 85)', - foregroundDarker: 'rgb(178, 59, 59)', - background: 'rgb(63, 21, 21)', - backgroundDarker: 'rgb(44, 14, 14)', - ansi: '\u001b[0;91m' - }, - [FormattingCodes.LightPurple]: { - foreground: 'rgb(255, 85, 255)', - foregroundDarker: 'rgb(178, 59, 178)', - background: 'rgb(63, 21, 63)', - backgroundDarker: 'rgb(44, 14, 44)', - ansi: '\u001b[0;95m' - }, - [FormattingCodes.Yellow]: { - foreground: 'rgb(255, 255, 85)', - foregroundDarker: 'rgb(178, 178, 59)', - background: 'rgb(63, 63, 21)', - backgroundDarker: 'rgb(44, 44, 14)', - ansi: '\u001b[0;93m' - }, - [FormattingCodes.White]: { - foreground: 'rgb(255, 255, 255)', - foregroundDarker: 'rgb(178, 178, 178)', - background: 'rgb(63, 63, 63)', - backgroundDarker: 'rgb(44, 44, 44)', - ansi: '\u001b[0;97m' - }, - - [FormattingCodes.Obfuscated]: { ansi: '\u001b[8m' }, - [FormattingCodes.Bold]: { ansi: '\u001b[1m' }, - [FormattingCodes.Strikethrough]: { ansi: '\u001b[9m' }, - [FormattingCodes.Underline]: { ansi: '\u001b[4m' }, - [FormattingCodes.Italic]: { ansi: '\u001b[3m' }, - [FormattingCodes.Reset]: { ansi: '\u001b[0m' } -}; - -/** - * stolen from NEU - * @param {string} displayname - * @returns {code} - */ -function getPrimaryColourCode(displayname) { - let lastColourCode = -99; - let currentColour = 0; - const mostCommon = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for (let i = 0; i < displayname.length; i++) { - const c = displayname.charAt(i); - if (c === '\u00A7') { - lastColourCode = i; - } else if (lastColourCode === i - 1) { - const colIndex = '0123456789abcdef'.indexOf(c); - if (colIndex >= 0) { - currentColour = colIndex; - } else { - currentColour = 0; - } - } else if ('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf(c) >= 0) { - if (currentColour > 0) { - mostCommon[currentColour] = mostCommon[currentColour]++; - } - } - } - let mostCommonCount = 0; - for (let index = 0; index < mostCommon.length; index++) { - if (mostCommon[index] > mostCommonCount) { - mostCommonCount = mostCommon[index]; - currentColour = index; - } - } - - /** @type {code} */ - // @ts-ignore - const code = '0123456789abcdef'.charAt(currentColour); - return code; -} - -/** - * @param {number} decimal - */ -function decimalToHex(decimal) { - return decimal.toString(16).padStart(6, '0'); -} - -/** - * @param {RawNeuItem} item - * @returns {Buffer} - */ -function tooltip(item) { - const background = '#100010'; - - const width = 1920; - const height = 1080; - const scale = 10; - - const itemRender = createCanvas(width, height), - ctx = itemRender.getContext('2d'); - - // ctx.fillStyle = '#000'; - // ctx.fillRect(0, 0, width, height); - - // ctx.globalAlpha = 0.94; - ctx.fillStyle = background; - - // top outside - ctx.fillRect(scale, 0, width - 2 * scale, scale); - - // bottom outside - ctx.fillRect(scale, height - scale, width - 2 * scale, scale); - - // left outside - ctx.fillRect(0, scale, scale, height - 2 * scale); - - // right outside - ctx.fillRect(width - scale, scale, scale, height - 2 * scale); - - // middle - ctx.fillRect(2 * scale, 2 * scale, width - 4 * scale, height - 4 * scale); - - // ctx.globalAlpha = 0.78; - - const borderColorStart = parseInt(new tinycolor(getPrimaryColour(item.displayname)).toHex(), 16); - const borderColorEnd = ((borderColorStart & 0xfefefe) >> 1) | (borderColorStart & 0xff000000); - - const borderColorStartStr = `#${decimalToHex(borderColorStart)}`; - const borderColorEndStr = `#${decimalToHex(borderColorEnd)}`; - - console.log(borderColorStartStr, borderColorEndStr); - - ctx.fillStyle = borderColorStartStr; - - // top highlight - ctx.fillRect(scale, scale, width - 2 * scale, scale); - - // left highlight - ctx.fillRect(scale, 2 * scale, scale, height - 3 * scale); - - // bottom highlight - { - const x = 2 * scale, - y = height - 2 * scale, - w = width - 3 * scale, - h = scale; - const gradient = ctx.createLinearGradient(x, y, x + w, y + h); - gradient.addColorStop(0, borderColorStartStr); - gradient.addColorStop(1, borderColorEndStr); - ctx.fillStyle = gradient; - - ctx.fillRect(x, y, w, h); - } - - // right highlight - { - const x = width - 2 * scale, - y = 2 * scale, - w = scale, - h = height - 4 * scale; - const gradient = ctx.createLinearGradient(x, y, x + w, y + h); - gradient.addColorStop(0, borderColorStartStr); - gradient.addColorStop(1, borderColorEndStr); - ctx.fillStyle = gradient; - - ctx.fillRect(x, y, w, h); - } - - ctx.font = `50px ComplianceSans`; - ctx.fillText(stripCodes(item.displayname), scale * 4, scale * 7); - - for (let i = 0; i < item.lore.length; i++) { - const line = item.lore[i]; - - ctx.fillStyle = `#${decimalToHex(parseInt(new tinycolor(getPrimaryColour(line)).toHex(), 16))}`; - ctx.fillText(stripCodes(line), scale * 4, scale * (7 + (i + 1) * 5)); - } - - return itemRender.toBuffer('image/png'); -} - -/** - * @param {string} displayname - */ -function getPrimaryColour(displayname) { - const code = getPrimaryColourCode(displayname); - return formattingInfo[`§${code}`].foregroundDarker; -} - -/** - * @param {string} str - * @returns {string} - */ -function stripCodes(str) { - for (const format in formattingInfo) { - // @ts-ignore - str = str.replaceAll(new RegExp(format, 'ig'), ''); - } - return str; -} - -const repo = path.join(__dirname, 'neu-item-repo-dangerous'); -const itemPath = path.join(repo, 'items'); -const items = await fs.readdir(itemPath); - -const randomItem = items[Math.floor(Math.random() * items.length)]; -/** @type {RawNeuItem} */ -const item = (await import(path.join(itemPath, randomItem), { assert: { type: 'json' } })).default; - -console.log(randomItem); -fs.writeFile('./test.png', tooltip(item)); diff --git a/test.png b/test.png deleted file mode 100644 index 942a85e..0000000 Binary files a/test.png and /dev/null differ diff --git a/tests/arguments/abbreviatedNumber.test.ts b/tests/arguments/abbreviatedNumber.test.ts index 2cf326d..cd34bc5 100644 --- a/tests/arguments/abbreviatedNumber.test.ts +++ b/tests/arguments/abbreviatedNumber.test.ts @@ -1,6 +1,6 @@ +import { CommandMessage } from '#lib'; import { expect, test } from 'vitest'; -import { abbreviatedNumber } from '../../src/arguments/abbreviatedNumber.js'; -import { CommandMessage } from '../../src/lib/index.js'; +import { abbreviatedNumber } from '../../lib/arguments/abbreviatedNumber.js'; const message = {} as CommandMessage; diff --git a/tooltips.nnb b/tooltips.nnb deleted file mode 100644 index 6e36999..0000000 --- a/tooltips.nnb +++ /dev/null @@ -1,118 +0,0 @@ -{ - "cells": [ - { - "language": "markdown", - "source": [ - "# Thingy" - ], - "outputs": [] - }, - { - "language": "typescript", - "source": [ - "function drawGradientRect(\n\tzLevel: number,\n\tleft: number,\n\ttop: number,\n\tright: number,\n\tbottom: number,\n\tstartColor: number,\n\tendColor: number\n) {\n\tconst startAlpha = ((startColor >> 24) & 255) / 255.0;\n\tconst startRed = ((startColor >> 16) & 255) / 255.0;\n\tconst startGreen = ((startColor >> 8) & 255) / 255.0;\n\tconst startBlue = (startColor & 255) / 255.0;\n\tconst endAlpha = ((endColor >> 24) & 255) / 255.0;\n\tconst endRed = ((endColor >> 16) & 255) / 255.0;\n\tconst endGreen = ((endColor >> 8) & 255) / 255.0;\n\tconst endBlue = (endColor & 255) / 255.0;\n\n\tconsole.dir({ startAlpha, startRed, startGreen, startBlue, endAlpha, endRed, endGreen, endBlue });\n\tconsole.dir({\n\t\tstartAlpha: color(startAlpha),\n\t\tstartRed: color(startRed),\n\t\tstartGreen: color(startGreen),\n\t\tstartBlue: color(startBlue),\n\t\tendAlpha: color(endAlpha),\n\t\tendRed: color(endRed),\n\t\tendGreen: color(endGreen),\n\t\tendBlue: color(endBlue)\n\t});\n}\n\nfunction color(num: number) {\n\treturn Math.floor(num * 255);\n}\n\nconst zLevel = 300;\nconst backgroundColor = 0xF0100010;\ndrawGradientRect(\n zLevel,\n 0,\n 0,\n 0,\n 0,\n backgroundColor,\n backgroundColor\n);" - ], - "outputs": [ - { - "items": [ - { - "mime": "application/vnd.code.notebook.stdout", - "value": [ - "{", - " startAlpha: 0.9411764705882353,", - " startRed: 0.06274509803921569,", - " startGreen: 0,", - " startBlue: 0.06274509803921569,", - " endAlpha: 0.9411764705882353,", - " endRed: 0.06274509803921569,", - " endGreen: 0,", - " endBlue: 0.06274509803921569", - "}", - "{", - " startAlpha: 240,", - " startRed: 16,", - " startGreen: 0,", - " startBlue: 16,", - " endAlpha: 240,", - " endRed: 16,", - " endGreen: 0,", - " endBlue: 16", - "}", - "" - ] - } - ] - } - ] - }, - { - "language": "markdown", - "source": [ - "# Thingy 2" - ], - "outputs": [] - }, - { - "language": "typescript", - "source": [ - "enum FormattingCodes {\n\tBlack = '§0',\n\tDarkBlue = '§1',\n\tDarkGreen = '§2',\n\tDarkAqua = '§3',\n\tDarkRed = '§4',\n\tDarkPurple = '§5',\n\tGold = '§6',\n\tGray = '§7',\n\tDarkGray = '§8',\n\tBlue = '§9',\n\tGreen = '§a',\n\tAqua = '§b',\n\tRed = '§c',\n\tLightPurple = '§d',\n\tYellow = '§e',\n\tWhite = '§f',\n\n\tObfuscated = '§k',\n\tBold = '§l',\n\tStrikethrough = '§m',\n\tUnderline = '§n',\n\tItalic = '§o',\n\tReset = '§r'\n}\n\nconst formattingInfo = {\n\t[FormattingCodes.Black]: {\n\t\tforeground: 'rgb(0, 0, 0)',\n\t\tforegroundDarker: 'rgb(0, 0, 0)',\n\t\tbackground: 'rgb(0, 0, 0)',\n\t\tbackgroundDarker: 'rgb(0, 0, 0)',\n\t\tansi: '\\u001b[0;30m'\n\t},\n\t[FormattingCodes.DarkBlue]: {\n\t\tforeground: 'rgb(0, 0, 170)',\n\t\tforegroundDarker: 'rgb(0, 0, 118)',\n\t\tbackground: 'rgb(0, 0, 42)',\n\t\tbackgroundDarker: 'rgb(0, 0, 29)',\n\t\tansi: '\\u001b[0;34m'\n\t},\n\t[FormattingCodes.DarkGreen]: {\n\t\tforeground: 'rgb(0, 170, 0)',\n\t\tforegroundDarker: 'rgb(0, 118, 0)',\n\t\tbackground: 'rgb(0, 42, 0)',\n\t\tbackgroundDarker: 'rgb(0, 29, 0)',\n\t\tansi: '\\u001b[0;32m'\n\t},\n\t[FormattingCodes.DarkAqua]: {\n\t\tforeground: 'rgb(0, 170, 170)',\n\t\tforegroundDarker: 'rgb(0, 118, 118)',\n\t\tbackground: 'rgb(0, 42, 42)',\n\t\tbackgroundDarker: 'rgb(0, 29, 29)',\n\t\tansi: '\\u001b[0;36m'\n\t},\n\t[FormattingCodes.DarkRed]: {\n\t\tforeground: 'rgb(170, 0, 0)',\n\t\tforegroundDarker: 'rgb(118, 0, 0)',\n\t\tbackground: 'rgb(42, 0, 0)',\n\t\tbackgroundDarker: 'rgb(29, 0, 0)',\n\t\tansi: '\\u001b[0;31m'\n\t},\n\t[FormattingCodes.DarkPurple]: {\n\t\tforeground: 'rgb(170, 0, 170)',\n\t\tforegroundDarker: 'rgb(118, 0, 118)',\n\t\tbackground: 'rgb(42, 0, 42)',\n\t\tbackgroundDarker: 'rgb(29, 0, 29)',\n\t\tansi: '\\u001b[0;35m'\n\t},\n\t[FormattingCodes.Gold]: {\n\t\tforeground: 'rgb(255, 170, 0)',\n\t\tforegroundDarker: 'rgb(178, 118, 0)',\n\t\tbackground: 'rgb(42, 42, 0)',\n\t\tbackgroundDarker: 'rgb(29, 29, 0)',\n\t\tansi: '\\u001b[0;33m'\n\t},\n\t[FormattingCodes.Gray]: {\n\t\tforeground: 'rgb(170, 170, 170)',\n\t\tforegroundDarker: 'rgb(118, 118, 118)',\n\t\tbackground: 'rgb(42, 42, 42)',\n\t\tbackgroundDarker: 'rgb(29, 29, 29)',\n\t\tansi: '\\u001b[0;37m'\n\t},\n\t[FormattingCodes.DarkGray]: {\n\t\tforeground: 'rgb(85, 85, 85)',\n\t\tforegroundDarker: 'rgb(59, 59, 59)',\n\t\tbackground: 'rgb(21, 21, 21)',\n\t\tbackgroundDarker: 'rgb(14, 14, 14)',\n\t\tansi: '\\u001b[0;90m'\n\t},\n\t[FormattingCodes.Blue]: {\n\t\tforeground: 'rgb(85, 85, 255)',\n\t\tforegroundDarker: 'rgb(59, 59, 178)',\n\t\tbackground: 'rgb(21, 21, 63)',\n\t\tbackgroundDarker: 'rgb(14, 14, 44)',\n\t\tansi: '\\u001b[0;94m'\n\t},\n\t[FormattingCodes.Green]: {\n\t\tforeground: 'rgb(85, 255, 85)',\n\t\tforegroundDarker: 'rgb(59, 178, 59)',\n\t\tbackground: 'rgb(21, 63, 21)',\n\t\tbackgroundDarker: 'rgb(14, 44, 14)',\n\t\tansi: '\\u001b[0;92m'\n\t},\n\t[FormattingCodes.Aqua]: {\n\t\tforeground: 'rgb(85, 255, 255)',\n\t\tforegroundDarker: 'rgb(59, 178, 178)',\n\t\tbackground: 'rgb(21, 63, 63)',\n\t\tbackgroundDarker: 'rgb(14, 44, 44)',\n\t\tansi: '\\u001b[0;96m'\n\t},\n\t[FormattingCodes.Red]: {\n\t\tforeground: 'rgb(255, 85, 85)',\n\t\tforegroundDarker: 'rgb(178, 59, 59)',\n\t\tbackground: 'rgb(63, 21, 21)',\n\t\tbackgroundDarker: 'rgb(44, 14, 14)',\n\t\tansi: '\\u001b[0;91m'\n\t},\n\t[FormattingCodes.LightPurple]: {\n\t\tforeground: 'rgb(255, 85, 255)',\n\t\tforegroundDarker: 'rgb(178, 59, 178)',\n\t\tbackground: 'rgb(63, 21, 63)',\n\t\tbackgroundDarker: 'rgb(44, 14, 44)',\n\t\tansi: '\\u001b[0;95m'\n\t},\n\t[FormattingCodes.Yellow]: {\n\t\tforeground: 'rgb(255, 255, 85)',\n\t\tforegroundDarker: 'rgb(178, 178, 59)',\n\t\tbackground: 'rgb(63, 63, 21)',\n\t\tbackgroundDarker: 'rgb(44, 44, 14)',\n\t\tansi: '\\u001b[0;93m'\n\t},\n\t[FormattingCodes.White]: {\n\t\tforeground: 'rgb(255, 255, 255)',\n\t\tforegroundDarker: 'rgb(178, 178, 178)',\n\t\tbackground: 'rgb(63, 63, 63)',\n\t\tbackgroundDarker: 'rgb(44, 44, 44)',\n\t\tansi: '\\u001b[0;97m'\n\t},\n\n\t[FormattingCodes.Obfuscated]: { ansi: '\\u001b[8m' },\n\t[FormattingCodes.Bold]: { ansi: '\\u001b[1m' },\n\t[FormattingCodes.Strikethrough]: { ansi: '\\u001b[9m' },\n\t[FormattingCodes.Underline]: { ansi: '\\u001b[4m' },\n\t[FormattingCodes.Italic]: { ansi: '\\u001b[3m' },\n\t[FormattingCodes.Reset]: { ansi: '\\u001b[0m' }\n} as const;" - ], - "outputs": [] - }, - { - "language": "markdown", - "source": [ - "# Thingy 3" - ], - "outputs": [] - }, - { - "language": "typescript", - "source": [ - "type code = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f';\n\n// stolen from NEU\nfunction getPrimaryColourCode(displayname: string): code {\n\tlet lastColourCode = -99;\n\tlet currentColour = 0;\n\tconst mostCommon = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];\n\tfor (let i = 0; i < displayname.length; i++) {\n\t\tconst c = displayname.charAt(i);\n\t\tif (c === '\\u00A7') {\n\t\t\tlastColourCode = i;\n\t\t} else if (lastColourCode === i - 1) {\n\t\t\tconst colIndex = '0123456789abcdef'.indexOf(c);\n\t\t\tif (colIndex >= 0) {\n\t\t\t\tcurrentColour = colIndex;\n\t\t\t} else {\n\t\t\t\tcurrentColour = 0;\n\t\t\t}\n\t\t} else if ('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf(c) >= 0) {\n\t\t\tif (currentColour > 0) {\n\t\t\t\tmostCommon[currentColour] = mostCommon[currentColour]++;\n\t\t\t}\n\t\t}\n\t}\n\tlet mostCommonCount = 0;\n\tfor (let index = 0; index < mostCommon.length; index++) {\n\t\tif (mostCommon[index] > mostCommonCount) {\n\t\t\tmostCommonCount = mostCommon[index];\n\t\t\tcurrentColour = index;\n\t\t}\n\t}\n\n\treturn '0123456789abcdef'.charAt(currentColour);\n}\n\nfunction getPrimaryColour(displayname: string) {\n\tconst code = getPrimaryColourCode(displayname);\n\treturn formattingInfo[`§${code}`].foregroundDarker;\n}\n\nfunction stripCodes(str: string) {\n\tfor (const format in formattingInfo) {\n\t\tstr = str.replaceAll(format, '');\n\t}\n\treturn str;\n}\n" - ], - "outputs": [] - }, - { - "language": "markdown", - "source": [ - "# Thingy 4" - ], - "outputs": [] - }, - { - "language": "typescript", - "source": [ - "import tinycolor from 'tinycolor2';\nimport canvas from 'canvas';\nimport path from 'path';\n\ntype McItemId = Lowercase;\ntype SbItemId = Uppercase;\ntype MojangJson = string;\ntype SbRecipeItem = `${SbItemId}:${number}` | '';\ntype SbRecipe = {\n\t[Location in `${'A' | 'B' | 'C'}${1 | 2 | 3}`]: SbRecipeItem;\n};\ntype InfoType = 'WIKI_URL' | '';\ntype Slayer = `${'WOLF' | 'BLAZE' | 'EMAN'}_${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`;\ninterface RawNeuItem {\n\titemid: McItemId;\n\tdisplayname: string;\n\tnbttag: MojangJson;\n\tdamage: number;\n\tlore: string[];\n\trecipe?: SbRecipe;\n\tinternalname: SbItemId;\n\tmodver: string;\n\tinfoType: InfoType;\n\tinfo?: string[];\n\tcrafttext: string;\n\tvanilla?: boolean;\n\tuseneucraft?: boolean;\n\tslayer_req?: Slayer;\n\tclickcommand?: string;\n\tx?: number;\n\ty?: number;\n\tz?: number;\n\tisland?: string;\n\trecipes?: { type: string; cost: any[]; result: SbItemId }[];\n\t/** @deprecated */\n\tparent?: SbItemId;\n\tnoseal?: boolean;\n}\n\nfunction tooltip(item: RawNeuItem) {\n\tconst background = '#100010';\n\n\tconst width = 1000;\n\tconst height = 250;\n\tconst scale = 10;\n\n\tcanvas.registerFont(path.join(__dirname, 'assets', 'Faithful.ttf'), { family: 'Compliance Sans' });\n\tcanvas.registerFont(path.join(__dirname, 'assets', 'Roboto-Regular.ttf'), { family: 'Roboto' });\n\n\tconst itemRender = canvas.createCanvas(width, height),\n\t\tctx = itemRender.getContext('2d');\n\n\tctx.globalAlpha = 0.94;\n\tctx.fillStyle = background;\n\n\t// top outside\n\tctx.fillRect(scale, 0, width - 2 * scale, scale);\n\n\t// bottom outside\n\tctx.fillRect(scale, height - scale, width - 2 * scale, scale);\n\n\t// left outside\n\tctx.fillRect(0, scale, scale, height - 2 * scale);\n\n\t// right outside\n\tctx.fillRect(width - scale, scale, scale, height - 2 * scale);\n\n\t// middle\n\tctx.fillRect(2 * scale, 2 * scale, width - 4 * scale, height - 4 * scale);\n\n\tctx.globalAlpha = 0.78;\n\n\tconst borderColorStart = parseInt(new tinycolor(getPrimaryColour(item.displayname)).toHex(), 16);\n\tconst borderColorEnd = ((borderColorStart & 0xfefefe) >> 1) | (borderColorStart & 0xff000000);\n\n\tconst borderColorStartStr = `#${borderColorStart.toString(16)}`;\n\tconst borderColorEndStr = `#${borderColorEnd.toString(16)}`;\n\n\tctx.fillStyle = borderColorStartStr;\n\n\t// top highlight\n\tctx.fillRect(scale, scale, width - 2 * scale, scale);\n\n\t// left highlight\n\tctx.fillRect(scale, 2 * scale, scale, height - 3 * scale);\n\n\t// bottom highlight\n\t{\n\t\tconst x = 2 * scale,\n\t\t\ty = height - 2 * scale,\n\t\t\tw = width - 3 * scale,\n\t\t\th = scale;\n\t\tconst gradient = ctx.createLinearGradient(x, y, x + w, y + h);\n\t\tgradient.addColorStop(0, borderColorStartStr);\n\t\tgradient.addColorStop(1, borderColorEndStr);\n\t\tctx.fillStyle = gradient;\n\n\t\tctx.fillRect(x, y, w, h);\n\t}\n\n\t// right highlight\n\t{\n\t\tconst x = width - 2 * scale,\n\t\t\ty = 2 * scale,\n\t\t\tw = scale,\n\t\t\th = height - 4 * scale;\n\t\tconst gradient = ctx.createLinearGradient(x, y, x + w, y + h);\n\t\tgradient.addColorStop(0, borderColorStartStr);\n\t\tgradient.addColorStop(1, borderColorEndStr);\n\t\tctx.fillStyle = gradient;\n\n\t\tctx.fillRect(x, y, w, h);\n\t}\n\n\tctx.font = `48px Roboto`;\n\tctx.fillText(stripCodes(item.displayname), scale * 4, scale * 7);\n\n\tconst buf = itemRender.toBuffer();\n\treturn buf;\n}\n" - ], - "outputs": [] - }, - { - "language": "typescript", - "source": [ - "import fs from 'fs/promises';\nimport path from 'path';\n\nconst repo = path.join(__dirname, 'neu-item-repo-dangerous');\nconst itemPath = path.join(repo, 'items');\nconst items = await fs.readdir(itemPath);\n\nconst randomItem = items[Math.floor(Math.random() * items.length)];\nconst item = (await import(path.join(itemPath, randomItem), { assert: { type: 'json' } })).default as RawNeuItem;\n\nconsole.log(stripCodes(item.displayname));\ntooltip(item);\n" - ], - "outputs": [ - { - "items": [ - { - "mime": "application/vnd.code.notebook.stdout", - "value": [ - "Enderman Minion IV", - "" - ] - } - ] - }, - { - "items": [ - { - "mime": "image/png", - "value": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAD6CAYAAAAyVW3pAAAABmJLR0QA/wD/AP+gvaeTAAAgAElEQVR4nO3daZAkaX3f8efJoyqzju7qY3q659je7tnZnZ1ddlmWJWSWsBEskgOQhfElwMIYAmNH6JXDjvBLXtt+Y4etCFsOh7yBQbIt2QohWSCMEIhjEQgWWPaaa+fqnpk+q6orq7Iy8/GLmu6uI6urKiu7O6vm+4noiN2eOvKorH5++fyf55GiTwVR2Or3sQAAAAAAoGFLbBX6eZx22BsCAAAAAAB6I6ADAAAAAJAABHQAAAAAABKAgA4AAAAAQAIQ0AEAAAAASAACOgAAAAAACUBABwAAAAAgAQjoAAAAAAAkAAEdAAAAAIAEMAqisNXPA594/sVXD3tjAAAAAAAYN698+5N95W560AEAAAAASAACOgAAAAAACUBABwAAAAAgAQjoAAAAAAAkAAEdAAAAAIAEIKADAAAAAJAABHQAAAAAABKAgA4AAAAAQAIQ0AEAAAAASAAj7hd85duffDzu1wQAAAAAIGmeeP7FV+N8vdgDOgAAAAAADwYV66tR4g4AAAAAQAIQ0AEAAAAASABK3AEAAAAAiKS1xF0NWfFOQAcAAAAAIIJhA3k7StwBAAAAAEgAetABAAAAAIgkiPXV6EEHAAAAACABCOgAAAAAACQAJe4AAAAAAEQS7yxx9KADAAAAAJAABHQAAAAAABKAEncAAAAAACJpLXEPhpzUnYAOAAORIjv5mG6mZ7S6u6Eq2294SvnHvVExGvf9AwAAiM+wgbwdAX2MnD7/WTudOa03/+7WG/+pUnNWYv7YAA8mqaXkwvKvW1b27N515lZXg5XLX3B8rxzvDCHHYNz3DwAAIOkYgw4AfZpeeH+qObwKIUTKmtdmTv/N9HFtU5zGff8AAADiF/T505+R6kG380v6wvI/suN+3Vrlln/rzd9y4n5dANEtP/35XLd/u33pv1SqOzdDv+l0Iy8Xn/jnWSFk6HNvvPrvduruZqTe4Ez+nD7I70fNuO/fKNN0Sz785L/KxvmaYRVWZx79p3bKnm853wddb73oRkYuPvEvW67HwHfUW6/8mx2lKO4CAKAdPegARo6VO9f15qKdP6d3C+fDCgJvoN+PmnHfP/RWKb3ZMeHAQddbL2HXY6V02SecAwAQjoAOYOTYueWuPbqZfPd/G9bO9quhSbXb70fNuO8fetvZ7gzoB11vvYQ91yld5vMEABgjqs+f/hDQAYwcO3tGk5oZ2k0+TJjoZfvut9zy5k/qzb+rFN/wNlb+1D2s9zxK475/6M11bvqB77S0Ig663nqxc+3DI5SoFC+xLAAAAF2M1Bj0MOWtn3tO+epQf+yZnRgYMVKXdm5JqxTfaLn2zfQJTTdzh3bjUalA3L3++7XNO9+sm9as5tW2Are6Oja1uuO+f6NMBXV17+Yf1br9e9qe0yZmnjObf1fe/Gnd2bne9fzV3a2Ov31KBaJSuurnChf32wddrrdejNSMZqQmWq5H17nj+16Jv7kAAHQx8gG9Vrnpl9b/st77kQBGWeBXlaalhJCaFKLRM9ceGDITrb11vldRupGJfUB6vbYW1GtrYxtcx33/RpFSvjjob10weUFvD+jOzltBaf0HA/99dEqXvJaALsKvt14a12NrFg8b4w4AwGhr/VunhrwNTYk7gJGwvfb9enO1jJ1/uOMGY3M5rVu9F2ysfK1rjyOAcJXim35jOZj9cXN2fnHgG/phw012tgcL+QAAJJ1SrT/DIqADGBmV0v7Y1ZQ1p+lGfq93XEpNWNmH9gKBU7rCRFR4wESfkKaZ75WU69xtCdLt11svUmrCzi22BPTAryrXuUVABwDgACNf4n6Ywtad3Vj9M3frzp+7jX9Py4mZ5ww7f95IWTOarmeEUr7wPUdVKzeDSvF1b2frp17U5WQ0PS2zk0/qmYlHjZQ1q+lGTkrNlIFfUW51LagU3/CK6z/wVODGOp7PMCdlZvKCkcmf0830jKYbGSm1tAx8R/leRbnOil8pXfZ3tl7xlOovA6XtBe30o5/LHPSYzTvfdDdXv743GdXu8c1MPGqY6VlN0y2pgrryvZKq7tzwi+s/9GqVm6GNvfz0O4wTZ/+W1fy7t1751zuBX1X5mXeaE9PPGkaqoEmpiWrlRrC5+vXa7jq/mp6WU/MvpDL5Jd1MTcsgqImasxJs33vJrRRf79m4zEw8ps8vfcxu/70KPBUEVRH4NeVW1wK3uhI45at+tXzdj9KQPu7P53GoFN/0Z0798v3/k8LOn9PLmz/2hBAinT2ja3p6L0A45cu+buQGLm8/e+E3MmZ6dqCbl2HrSfdyXOfvqPavm5Q1p2UmHzfs3LJupqakbthSCCkC31FefVs55Wv+zvZrfrdru5sH8Xo4TJXSJT9ln2wK2FLY+WW9vPlyX1/6KfvU/etx/7utUrrC8moAgDEU79QqBPQB7Y5nTVnz2vzyJ2zDbO1RkFITRsqUudRFLVe4aLhz7wnuvvU/q2717kCtkvz0M8b0wvvTYQFDN3LSzuV0O/ewXjjxbnP12u/WapUbQ/dK6EZGFk7+Ympy5lljd5xv679npW5kZco6oeWmnjL9hQ8EG3e+UY8yxjGMmZrae8+0fUqbX/qYpZv5liAh9bTU9LQ007NafvoZs7j+g/r6rT+u9dPo040JOTX//tTEzLMt4zTt3LJuLT9k3770X52asxLML33Cau6J1XRb2Lll3c4t2fdufLla2vjhgQ3U9m3e23bNkLqWE7qRk2Z6RstOPiamTr5X1Gsbwda9794/jsNd4Ef1+Twu9dpa4LnFYHfiqUx+eS+gN5e3q8BT1fJVP1t420h9x43z+TPMgpw+9UI6V3jCCFunXtfyUjfzIp05oxfm3iOc0lVv/fZX3GEmqQs/nvvXmJSGMFJ5mUs9ruUKjxvu3PPB3bf+10gcz8NWKb3pF+beLYTYL9ezc0t9B3Q7v6y3l/mxvBoAAL1R4j4g3chKKQ0xv/RrVnvjOUzKOqGdeuTTdsqa6/tYT82/L3Xi7K9a/fT+6WZOW1j+uGWYE0NNhGWmZ7XT5/+JPTn7nBkWzru994kzH06fOPuraSmHX9nKSBU0IYTQdFueXPqY3S3oNpuYeadZOPneVD+vb1oz2sT0M6GBTWqGnJp/XypbuGg0h/O2R4mZUy+kpTw48w16Lsz0tHbizIfSC+c+ZWu6PdR5PIrP53FrLnNvHuPa/N/Vnet+EIze3JHjev7S9mnt9KOfzeQKT4aG8zB2fsk4ff4zdnbywgBfLq3l3bqRuX88/75lmDnZ6wZY43j+48Qfz+6UUGr/Zxi1nRu+71VbXmaQJQw7H6vuj20HAAAHGdFGyPHRjYzMFh43dsNkPzTdknOLf8eSsvdT8jPPmlMn/3pfgXP/9W05Mfsus/cjwxlmQZ565NP2IPvULD/9jDl96pfSUd9/bzvuv39h7t1mP+Fk19Tce8x+QvHk7F8zlRDSdVb9WmWlo6w8k1/SJ2beZQa+o6o71/2w5fc03ZZW27jKjv0YYNub2blFfWH51y2ppSKH9MP+fCaBU76818jXzZxmpk9oUktJK3N6bwcqpcsjGQTG8fyZ6RPaqUc+aTd6s8PGSHf/kZohTy7+PcvOR1vbXjey94/n1Ngcz27imphm//UC4ZSvtVxHu9dbr+dKzbx/Pe6fS7e6GrC8GgBgPAV9/vRnpMo/k0A3stLK7gY0JZzyNb8R5ipK00yRzpzRMxOPGu2Nu5R1UssWnjDKmz/tWuKnGxk5s/BCaDh3nTt+ceNHnltdCZTvKd3MSTu3qOennzE13Za5qadNTRv8dEqpibnFj1phS1H59XKwU3zVrzmrQeDXlG7Y0sqc1bKTF4z2EDk5+y7TKb3pVYqXQoNRzVkJrrz8+XLz72bP/Eq6udzcMHNS00yRnWws76MCT5U2f+JVSm/4gVcOpJaS+alnjNzU21pvRkhNZgtPGtv3vnNgl6mVPaOvXvvdamX7VU8IISZm32XOnv7g/o0FqUvdyMjrr/77SuA7StPT8syj/6zjxoWZPqE5BwTAne1XvGrlduhVqOtpoZs5LW2f0tKZM3r75ySdOaVPz7/PXL/9J27Y83s5zM9nUjilS75Sgdjdh8zEOb3ubqrmyg9niKWcbrz2HyoH/fvp85+105nTw5eMhDiK89ff/p2KZf+k1MXJxY+mw246BX5NVYqvezXnTiCEEo1hHxcM3ci2PlZqcu7sR9I3Xv9NJ/CrAwU83cjI/YoYJZzyW/eP587943m6y/E8MTLXw2GqlC752cnHWpdbyy/r9dq9A1sZdm5Ja6/EYtJGAAD6Q0AfUMo6qaWsk5rnFoM7136nWnM6glg9bZ/S5pf/od0eevNTbz+wwTcx+wtmWInz5p1v3Z88ralt6ghRKb7ub975Vn1+6eOWlT0bqUGdLTwVWtK9dfcv3M3Vb7jtk8AVxV8K3ci5c4sftVpLGKUozP2NVKV4yYmyHbuvYabnNDM9rXluMVi9+oWOsaBO6YovhBK5qadaQrqVfUjvFdBdZ9XfDedCCLGz9arXEtCFEMW177mB7yghGgFip/iaPzn7Cy2t9+aJyMLcv0nRMyAa5oScXvhAqv2Gw+Tsc+b2ve/Wvfr2wL1Nh/n5TIrAr6la5Za/+5m3c8u64W7tHSu/XgpGdQxx/+fvEyHn7+nEnb/c9NvNlD3f8f3ilK/6d9/6/Wp7lcr67a+6J87+SrpRCr9LCd3MaYW5d5sbK1/vceOq8XK7PcmN75M5rV7bDu6+9T+6Hs+TSx/vOJ65wlOJO569tX9lDNdhXS1d8oRQ6eZhCXZuSS+uvXTgd62dW+o4581DUwAAQHejVcMXIp05o+dnnjMH+UlnooXZXSqoq9uX/1tYY08IIUTNuR2s3/5Kx/rLjV63btlOivzUUx03TMqbP6tvrv4/t1tDK/AdtXr1ix0N3X5NhpTGb6x+w91Y+VpHON/le2W1euW/O66z2tLgsrJn9ZQ1P9RnavbMh9NCKXXn2u90naipuP79jsZhyupddunW1lqOURB0LpFdc+61PMb3djqOaxzj7YUQwqsX1d3rv1crbfy4dX+kLjOTFyLfPDucz2eyNJewZ/Ln9PzU00bYvyVfZ1m3Clx1+/Jvj8X5m5x5zmjfP7d6J1i9+qXQ7ywVuOre9f9dbcz2vTuWuhG4c1PPmL2uvbAybxXU1cqVFw88nhsrXx2J43nUvHpRtX8P27nFjsqfdlZuseW8B35V1XYGm5UfAIDR0dSOU91/+jXyPei5wkUjV7g40H5sr71UH2bW8+L6X9U9d/3AHrqdrZ956syHldTMvRaepltSN7IyrGGasua0jnGnKlAbK1/tWeoc+I7avvc9d3rhhYHGgZvpGa29lNV17vjbd7/Z8z2V8sXa7T9xT537VMtyYnZ+WR9m1uV0ZkEvbfyo3q0xLYQQbvVex/ELK9FvF/i1nleGCno/Jm6bq19389NPm629VMs9e6m6OYzPZ9JUy5c8Ie5PDih1qen7H+Oj6ak7vEPUOH8bPc7fK4d4/qKvn93MTJ/QTGuuYybvjZWv1VRQ7/oGSgVi/dZXamce+1ym+ZrQjay08+f0SvGNgc5vaeNHfR3P2dMfbDue6ZG5Hg6TU7ripay5vaFXUjNlyj6td/sbqht52X7D1Clf85UinwMAxlOcc8AIMQY96MehvPWzni0NpXxRdzdCJhmzQoNkOrPQcS6c8jXfqxf7OuU7W68MXIppZRc73nN77aV6v+vUVsvXfL9eanlwOnNm6O7l7XudPeTNGuuI3ws8d2vvx/cqI9uI9upF5bmtx9FMFSJ33R3G5/P4tYbG6s6toFHd0Nb7rHzhdAT09l7qZOv//K137H8c52+3F7r9Z1BW9mzH94tfLweVYu8Kh3rtbtCYyHFvq4QQSvQeG99ZkTCe10OYsIn2hhd2w+ugSfvs/FJH5UHzxI4AAOBgI9+DfvSUcJ2VvhobKvA6G3xa+GTrKetkZ0DfudZ3T3Td3VSNic36X6YrbKKrQZfBqVZuBdnJC3vbnkpPD9Wg9evloJ8e+Juv/8cDJ7oaNUFQEUJM7P1/PxUB4Q7n83k8DgoYSjjlq35jTe19rrPqDzqR2PFqHTPdOH+rfZ2/wPc7qqWkTM75a4Tp1g10ylc7Vk/oxilf8dOZhZbvqLR9esCbyv0fTxX4IcNZknM8j0tt54avAlc1T/Rn55b0rTt/Hvr4xvjz5kOphFNk/XMAwDiLt+lJQB9Q4Luq29jsYYSteT7oRFdevaxSAwT0xrrAzc8vqUGXwWnvuR62x6nm3ApGoZezH7qRkZnJJwwrc0ZLWXOabuY0TUsJTU/LwK+q3R+vXlaGOdk2c7Ue6TgO/vkc3WPtlK547QH9KMrbw8YRRS1t6hwvfTjfL4OJp/e1YzZ2IUSv2b97PTbse/IgKqgPcDzD9nt0r4+4KOWLSumqn5l4dO9aS9sLmtRSUgVuxwGyskstwxrc6r0gyoSXAAA8qEY+oK/f/mqt1+zdcVLqcN5K08OWIRqsbPugcZ3h79ka5g0zL5ef/nxukNdoN8wa3kIIUa8dPFZ0FBipaW164YVUdvJCx/JNuzTdkrs3M1J26EP61B4UI63ONpIqxTf87XvfadnhsFm3B5mU47gFwSDfL8ku2Q+7Wef7/Re+tN78U/df86Dvl87jETYR5HhTLTd94vroV0qXWwK6kJq0cou6U3yz5Xoz0yc03cy2fOlVy1eP+44TAAAjZeQD+rgIXSfYGyxwD9pY17TQJdeHMuxEQKM+IVNu6m3G3NmPpPfXAI66OyN9GGLRK1j7Xlmt3/7TEb8jMb49to3hEm03kEKGVXQT+G7H85sncUOnw7oXVS1d9oQQLZOQ2rnljoDeGH/euhEsrwYAGH/x9i8ySVxSqCBkgqLB5luTcrD7Lb4/xJLlXQzai99usB7EuPSaVKm/yZeykxf0uYf+trUfzkfF6EyglgyHMxlXcoTt3+D7GASd906kZvR9bWh65w3EYb9fehvn8xqdV99WjSEH+8fGzj3c8Qeqff1zFbiqthN9xRQAAB5E9KAnhO9XO34ntcHGc2t6eqDH+57T0QKt7lwfajmcem1zqFtIg/Sw9fFqffx7PG+naaaYOf1Bq3324sB3VGnjx/VK6bLvuUUV+DsqbNz+qUc+bfezlnun6D2wcY6lPhpRNm6cg1ayx0yHLWuo65m+n985UWJjPe2Dn9U66d4gwxuizlafLIf3eXBKVzwzPbt318RMz2iGOSF3VxqRUhdW9mxLQHfKb7G8GgAAAyKgJ0RYwzNlzWqdS0aFk9IQhjkxWECvb3eE6c0733Cd0pUjbFGNfItYCCFEZvKCYZj5luPvOiv+ypUvVHstAdc4975oHj/ab7DomGRsPA7nfWO1MyGSHbCH5dVLqn1/UtZs3zehzPSs1n4dePWDh8CM1+c/WZzyZX9i9l0tv7Nyy3p588eeEEKk7FN6Y6iWannO0W4lAADHYf9vXxxtEUrcEyJsxvZ05mzfNe4pe0EftLTaKV/rWGc4P/2OB3RdoW49rf2V+lrZh/XdHundn7vX/7DW7/rsmp6XxxcuKOkdTDwl4Mk2/L65zm2/fS31dObhjjWyu2kvlxZCiJpz+xAnkRz3czqcavm635i1vbnMfXHvHIWNP3eKV5ggDgAw9prbOnEgoCdErbLS0fDMTpzX+y1bz009aQzawHTKVwOh/JZerlzhomHlHhps8Hti7R+HRk9c5/HYD9ThF1V7wOh24bX3ngvlq37WcxeisbbzMOueRw8VD0oIGefQFf++9fuZ76W6c7NzmTQzq2UmzvX8fklZc1rKnm97nBK1ys0ePbIPcsg+3H1XyhfVnestx9/KLRq7N1zax6TXa2uBV996kE4AAACxIKAfuv4aTK5zy28sq7b/WKmZcmr+fT2nWjdSU9rE9DsGHq6gAleVNn/S1sMhxcnFf2ClrJNH8Nk47AblUY4rbcvnQkpN631KpNTF9KkPpB/sYNHNg3A89m8ghc0JcOAzYwrSh6VeWwtcZ9VvP49T8+9LHzQbu5SamD71S/dnDN9/nu+VVbWl6gdHrVJqLVnXjaxMWSc0qaVk2l5o+puhhFNieTUAwIOiW3VltLYsAb2nowlOSvmivPVKR4NmcvY5szD3vNmtLNRMTcmF5U9Yg8yO3Gzrzjfd/RnkG/unG7Y8ff4zdmHuefOwlzVKTsA46Pz2/gw0JkpqurkipcjPPHvgcAFNt+Xc4t+1rOxZfWfrZ8cxfX2TcQ/C8Wl8VqOF6vDXinHjYhHfd15p4686PtdmelY7+fCvWbqR7/hukVpKzp79iGVlO6t4Shsv1w93wjFukvXiFC977Z99K7ekW7lFXQkpmyuS2sM8AADozwhPEtdoPKUzp/X8zDuHeiXfK6vK9mvHfrd/6+536xPTzxhC6k0NVymmF15IZycvGqXNlz23eidQvqf0VFazc+f0iem3G1IzZXnrZ56u28LOLw90Tuvuplq7/VV35tQvt6xxK6Qhp+bfn5488Z5UpfSmX6vc8j13K/C9ihAqUJphSamlpG7kpJme0VLpE3Lt1pddzx1uFvf49NvAjqcRXt255uen394SyKfm3582UtNaaf0H9XptI1DKE1JLyVT6hMxMPmbkp582dSMnhRCiuPFDL1t4cu/5mm7J049+LuPXi0Hd3VZ+vRhsr33fa4wBPWgfKXFvNVyATrbkn7/y5stefvoZv71c3cqe1c889rnMTvGS79Xu+UoFwkzPaJmJ84am2x3B3a8Xg+La9/q4iZXs4zEMKXWRm36q600/Mz2nNT7v+7+zMmc1IUTX51S2X/MCv3M1j268+paq19YDMz2zd3Pfzj2sG+5ky2uooM7yagAARDSCAb21LZErXDRyhYtD7Uetcts/3IDeX/vHczeCjTvfrE/PvzfVnilS9oI+Yy+Ejt1UqrEEjpV9SI8yq3dx7aW6mZ7WJmae62jIaXpa5gpPGrnCkz2Pcco6WffczY7fm6kpeebCb2TDn9W6gbNnPmTNnvlQx6M2Vv/M3b77F50LKydEZfs1z58vB7qZ22u4SqmJiZl3mBMz7zCFUEIFvuqsdFCiXlsLquUbvlK+kFK7/1wpUtYJTVgnNPv+I8tbP/c9d2N8E0ibziXgDnfXrdzD+vzSJ+zej+x06vxnuqwfpoRbvRfcfvO3Kt3+/eD/j4+VWxxi/z7ddX20g/ZPqUDcu/EHtVOPfMpuzPC9T2qmzBUeN4R4vO27pf1LLFD3bn65FrZs24NEaoacXmi7kdqm/RLJFi6a2cLFro93Kyu+O0BAF0IIp3zVbw7oVu5hw1KLLa/RWK7z2O95AwBwROJtolDi3lP0ssf2Ccj6KWfduvMtt7jeWRZ6ELd6Nyhv/tTrtQTRQdZv/d/a2q0/rqmgrsL3ufePmZ4a8c9Tt8PX+/wHQV3cu/l/akJ5KvyxUoQNQ1CBq9ZufrnaCPD1Yw4gD3T+EVE/93GMNTp+h7ef9dp6sHrli06/Kxq0bFVQV3ev/171aMaej/L5OzrV8hWv4xhJKZt/55SPcqlOAADGyxgEqsF+2sfP9RpDevQTMSmxdvOPauu3v1LrXXqohFtdDe5c/ZKjlCd8b3fd4WgNzNL6D+q33vzPTmnjx3UVeAPvqZFKWkDvp8EdX2PcKV31V65+qerXi32V+bvVe8HKlS9Wd2e7jnLMh7uBJNqug8HffTSMa+iKcu6P50ZCzVkJVi79dqW89XOv3/erlq95K5dfdCrFSwOEvc7v+UEkZ06MgxzvTaTGcmsH30x0GH8OAEBkI1XiPuyETMdn0G1Worj2Ur28+RMvW7hoZPLndTM9oxlmTkppCN+vCNe55+9s/9wrb77s7U6c1JiobDj12nqwdvMPaxsrX3MzE4/o6cwZPW0vaLqRkZpuS023WnqBVeAq39tRbnU9cJ3bBzTKRvG8Da5avubfeO03K9nCRSMzcd5IWSc13chIqZky8B0V+I6qVW77ldIlv7L9uqfUfpYPgqrQVNdK4lBRhjSMjnHYmV4haBz2sT9efVut3fiD6vbd72iZyfOGnVvUDbMgNT0jhZAi8B3l1YuqunPdd4qXvJpza+D5LMbr8x/m+HdQKU9Ud276dn4ptP1Qr20EnruVkLlIAAA4Ko0/fUEMfwFlQRS2+nngE8+/+Go/j3vl2598fLhNAgAAAAAg+c49+2/7ysmXf/gv+srJCStJBgAAAADgwTRSJe4AAAAAACRPPEPRCOgAAAAAAEQS7xwxlLgDAAAAAJAA9KADAAAAADAUStwBAAAAADhGlLgDAAAAADB26EEHAAAAAGBISgVDvwYBHQAAAACACOII5c0ocQcAAAAAIAHoQQcAAAAAIBbDTRpHQAcAAAAAIBJmcQcAAAAAYOzQgw4AAAAAQCwocQcAAAAA4BhQ4g4AAAAAwNihBx0AAAAAgCGpvc706L3qBHQAAAAAACJQihJ3AAAAAADGDj3oAAAAAADEKoj0LAI6AAAAAACRRAvi3VDiDgAAAABAAtCDDgAAAABArKJNHkdABwAAAAAgEmZxBwAAAABg7NCDDgAAAADAkIKO+eIG710noAMAAAAAEEEQUOIOAAAAAMDYoQcdAAAAAIBDMVgPOwEdAAAAAIBIKHEHAAAAAGDs0IMOAAAAAMCh6Jja/UAEdAAAAAAAIhksgPdCiTsAAAAAAAlADzoAAAAAAENSXeeL638iOQI6AAAAAAARqO6pPBJK3AEAAAAASAACOgAAAAAACUCJOwAAAAAAkVDiDgAAAADA2CGgAwAAAACQAJS4AwAAAAAQSRDrq9GDDgAAAABAAtCDDgAAAADAkOJYEp2ADgAAAABABCqOVN6EEncAAAAAABKAgA4AAAAAQAJQ4g4AAAAAQCSUuAMAAAAAMHYI6AAAAAAAJAAl7gAAAAAAREKJOwAAAAAAY4cedAAAAAAAhhTHkugEdAAAAPcAmoQAAAXTSURBVAAAIlAqiPX1KHEHAAAAACABCOgAAAAAACQAJe4AAAAAAETCLO4AAAAAAIwdAjoAAAAAAAlAiTsAAAAAAJFQ4g4AAAAAwNihBx0AAAAAgCEEMS2HTkAHAAAAACCCIK5kfh8l7gAAAAAAJAABHQAAAACABKDEHQAAAACASJjFHQAAAACAsUNABwAAAAAgAShxBwAAAAAgEkrcAQAAAAAYO/SgAwAAAAAwBBVTRzoBHQAAAACACFRcyfw+StwBAAAAAEgAAjoAAAAAAAlAiTsAAAAAAJEEsb4aPegAAAAAACQAAR0AAAAAgASgxB0AAAAAgEiYxR0AAAAAgLFDDzoAAAAAAEOIazl0AjoAAAAAABGouJL5fZS4AwAAAACQAAR0AAAAAAASgBJ3AAAAAAAiCWJ9NXrQAQAAAABIAAI6AAAAAAAJQIk7AAAAAACRMIs7AAAAAABjhx50AAAAAACGEMQ0VxwBHQAAAACACIKAEncAAAAAAMYOAR0AAAAAgASgxB0AAAAAgMjiK3OnBx0AAAAAgEgYgw4AAAAAwNihxB0AAAAAgMhiWmNN0IMOAAAAAEBE8YVzIehBBwAAAABgKCqmoegEdAAAAAAAItgP5vEkdAI6AAAAAACRMIs7AAAAAABjhx50AAAAAACGNnxvOgEdAAAAAIBIKHEHAAAAAGDs0IMOAAAAAMDQhl8TnYAOAAAAAEAkw4fyZgR0AAAAAACGoGIaik5ABwAAAAAggs5gPlxSJ6ADAAAAABAJs7gDAAAAADB26EEHAAAAACA20XvVCegAAAAAAERCiTsAAAAAAGOHHnQAAAAAAGJDiTsAAAAAAEcs3hJ3AjoAAAAAAEPoXA89GgI6AAAAAAARdA/mQaTXI6ADAAAAABBJtCDeDbO4AwAAAACQAPSgAwAAAAAQu8EHphPQAQAAAACIJN5Z3ClxBwAAAAAgAehBBwAAAAAgdpS4AwAAAABwROItcSegAwAAAAAwhCCm1dYI6AAAAAAARNA7mA/Ww05ABwAAAAAgEmZxBwAAAABg7BDQAQAAAAA4NP0PUKfEHQAAAACASGKaHe4+etABAAAAAEgAAjoAAAAAAIem/4nkKHEHAAAAACCSeGdxJ6ADAAAAADAEFVNOJ6ADAAAAABBBXMF8FwEdAAAAAIBI4k3oTBIHAAAAAEACENABAAAAAEgAStwBAAAAAIgkiPXV6EEHAAAAACABCOgAAAAAACQAJe4AAAAAAEQS7yzuBHQAAAAAACKKcy10StwBAAAAAIggznAuBD3oAAAAAABEFG9CpwcdAAAAAIAEIKADAAAAAJAAlLgDAAAAABAJJe4AAAAAAIwdAjoAAAAAAAlAiTsAAAAAAJEEsb4aAR0AAAAAgIiCGDM6Je4AAAAAAEQQZzgXgh50AAAAAAAiYhZ3AAAAAADGDgEdAAAAAIAEoMQdAAAAAIBIKHEHAAAAAGDsENABAAAAAEiA2Evcn3j+xVc7fxu123//eWqgl4g61/1w2zn4FPtHv52DHcdh3y/K/jXea/DtPMptbLzf0R7L6PsXbemHo91OpY7nmh34WdFOujjKa71xvo/+czbwO6lh3u9otnP/dB/1dg72vP3tTO511Po9lMzt7Ly8k3m9j8J2hv/dSd71Ht+xPMptHOb9Du954duZrGu9e3soWddQ96ZGcrbz4OZQcq6jg9vAydnOdvSgAwAAAACQAAR0HJKod+sAAACGQRsEwOgioAMAAAAAkAAEdAAAAAAAEoCAjh7iXdcPAACgP7RBADx4RiKgR544GQAAAACAETESAR1x4C4HAAA4DrRBAKBfBHQAAAAAABKAgA4AwIhiCBgAAONF9vvAgihsHeaGAAAAAAAwjrbEVqGfx9GDDgAAAABAAhDQAQAAAABIAAI6AAAAAAAJQEAHAAAAACABCOgAAAAAACQAAR0AAAAAgAQgoAMAAAAAkAAEdAAAAAAAEoCADgAAAABAAvx/698dMJgpvRYAAAAASUVORK5CYII=" - } - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..44301c8 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,67 @@ +{ + "compilerOptions": { + // #region Projects + "incremental": true, + "composite": true, + // #endregion + + // #region Language and Environment + "target": "ESNext", + "lib": ["ESNext"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "useDefineForClassFields": true, + // #endregion + + // #region Modules + "module": "ESNext", + "moduleResolution": "Node", + // "baseUrl": "./", + "paths": { + "#lib": ["./lib/index.ts"], + "#constants": ["./lib/utils/BushConstants.ts"], + "#args": ["./lib/arguments/index.ts"], + "#commands": ["./src/commands/index.ts"], + "#tags": ["./lib/common/tags.ts"] + }, + "resolveJsonModule": true, + // "noResolve": true, + // #endregion + + // #region JavaScript Support + "allowJs": false, + "checkJs": false, + "maxNodeModuleJsDepth": 0, + // #endregion + + // #region Emit + "sourceMap": true, + "removeComments": true, + "importsNotUsedAsValues": "remove", + "newLine": "lf", + "noEmitOnError": true, + "preserveConstEnums": true, + "preserveValueImports": true, + // #endregion + + // #region Interop Constraints + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + // #endregion + + // #region Type Checking + "strict": true, + "useUnknownInCatchVariables": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "allowUnusedLabels": true, + "allowUnreachableCode": true, + // #endregion + + // #region Completeness + "skipDefaultLibCheck": false, + "skipLibCheck": true + // #endregion + } +} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 59f6ecc..8b91633 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,15 +1,8 @@ { - "extends": "./tsconfig.json", - "include": [ - "src/**/*.ts", - "src/**/*d.ts", - "lib/**/*.ts", - "ecosystem.config.cjs", - ".eslintrc.cjs", - "config/**/*.ts", - "tests/**/*.ts", - "vite.config.ts", - "tooltips*", - "test*" - ] + "extends": "./tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true + }, + "include": ["**/*"] } diff --git a/tsconfig.json b/tsconfig.json index 6d24566..0310e3d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,32 +1,9 @@ { + "extends": "./tsconfig.base.json", "compilerOptions": { - "module": "ESNext", - "target": "ESNext", - "moduleResolution": "Node", - "outDir": "dist", - "lib": ["ESNext"], - "sourceMap": true, - "incremental": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "resolveJsonModule": true, - "noImplicitOverride": true, - "noErrorTruncation": true, - "strict": true, - "baseUrl": "./", - "useUnknownInCatchVariables": false, - "forceConsistentCasingInFileNames": true, - "allowSyntheticDefaultImports": true, - "preserveValueImports": true, - "removeComments": true, - "paths": { - "#lib": ["./src/lib/index.ts"], - "#constants": ["./src/lib/utils/BushConstants.ts"], - "#args": ["./src/arguments/index.ts"], - "#commands": ["./src/commands/index.ts"], - "#tags": ["./src/lib/common/tags.js"] - } + "outDir": "./dist" }, - "include": ["src/**/*.ts", "src/**/*d.ts", "lib/**/*.ts", "test.js"], - "exclude": ["dist", "node_modules"] + "references": [{ "path": "./src" }, { "path": "./lib" }, { "path": "./config" }], + "include": ["./src/**/*.ts", "./lib/**/*.ts"], + "files": ["./package.json", "config/Config.ts", "config/example-options.ts", "config/options.ts"] } -- cgit