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 --- 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 + 70 files changed, 18458 insertions(+) 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 (limited to 'lib') 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", + "discord