diff options
Diffstat (limited to 'src/lib')
54 files changed, 0 insertions, 17966 deletions
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<ButtonBuilder> { - return new ActionRowBuilder<ButtonBuilder>().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<ButtonBuilder>().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<boolean> { - this.messageOptions.components = [ - new ActionRowBuilder<ButtonBuilder>().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<boolean>((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<boolean> { - 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(<string | MessagePayload | MessageEditOptions>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<ButtonBuilder>().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<MessageOptions, 'components'>) { - 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<Snowflake>; -type channels = Set<Snowflake>; -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<guild, Collection<word, users>>(); - - //~ /** - //~ * Cached global highlights. - //~ */ - //~ public readonly globalHighlights = new Collection<word, users>(); - - /** - * A collection of cooldowns of when a user last sent a message in a particular guild. - */ - public readonly userLastTalkedCooldown = new Collection<guild, Collection<user, lastMessage>>(); - - /** - * Users that users have blocked - */ - public readonly userBlocks = new Collection<guild, Collection<user, users>>(); - - /** - * Channels that users have blocked - */ - public readonly channelBlocks = new Collection<guild, Collection<user, channels>>(); - - /** - * A collection of cooldowns of when the bot last sent each user a highlight message. - */ - public readonly lastedDMedUserCooldown = new Collection<user, lastDmInfo>(); - - /** - * @param client The client to use. - */ - public constructor(public readonly client: Client) {} - - /** - * Sync the cache with the database. - */ - public async syncCache(): Promise<void> { - 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<Snowflake, HighlightWord> { - // even if there are multiple matches, only the first one is returned - const ret = new Collection<Snowflake, HighlightWord>(); - 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<Collection<HighlightWord, boolean>> { - const highlights = await Highlight.findAll({ where: { guild, user } }); - - const results = new Collection<HighlightWord, boolean>(); - - 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<string | boolean> { - 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<string | boolean> { - 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<boolean> { - 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<HighlightBlockResult> { - 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<HighlightUnblockResult> { - 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<boolean> { - 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<boolean> { - 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<T extends ATC>(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise<ATCR<T>>; -export async function cast<T extends KBAT>(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise<BAT[T]>; -export async function cast(type: AT | ATC, message: CommandMessage | SlashMessage, phrase: string): Promise<any>; -export async function cast( - this: ThisType<Command>, - type: ATC | AT, - message: CommandMessage | SlashMessage, - phrase: string -): Promise<any> { - 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<T extends ATC>(...types: T[]): ATCATCR<T>; -export function compose<T extends KBAT>(...types: T[]): ATCBAT<T>; -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<T extends ATC>(...types: T[]): ATCATCR<T>; -export function composeWithFailure<T extends KBAT>(...types: T[]): ATCBAT<T>; -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<T extends ATC>(...types: T[]): ATCATCR<T>; -export function product<T extends KBAT>(...types: T[]): ATCBAT<T>; -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<T extends ATC>(type: T, min: number, max: number, inclusive?: boolean): ATCATCR<T>; -export function range<T extends KBAT>(type: T, min: number, max: number, inclusive?: boolean): ATCBAT<T>; -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<T extends ATC>(type: T, tag?: any): ATCATCR<T>; -export function tagged<T extends KBAT>(type: T, tag?: any): ATCBAT<T>; -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<T extends ATC>(...types: T[]): ATCATCR<T>; -export function taggedUnion<T extends KBAT>(...types: T[]): ATCBAT<T>; -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<T extends ATC>(type: T, tag?: any): ATCATCR<T>; -export function taggedWithInput<T extends KBAT>(type: T, tag?: any): ATCBAT<T>; -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<T extends ATC>(...types: T[]): ATCATCR<T>; -export function union<T extends KBAT>(...types: T[]): ATCBAT<T>; -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<T extends ATC>(type: T, predicate: ParsedValuePredicate): ATCATCR<T>; -export function validate<T extends KBAT>(type: T, predicate: ParsedValuePredicate): ATCBAT<T>; -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<T extends ATC>(type: T): ATC<ATCR<T>>; -export function withInput<T extends KBAT>(type: T): ATCBAT<T>; -export function withInput(type: AT | ATC): ATC; -export function withInput(type: AT | ATC): ATC { - return Argument.withInput(type as any); -} - -type BushArgumentTypeCasterReturn<R> = R extends BushArgumentTypeCaster<infer S> ? S : R; -/** ```ts - * <R = unknown> = BushArgumentTypeCaster<R> - * ``` */ -type ATC<R = unknown> = BushArgumentTypeCaster<R>; -/** ```ts - * keyof BaseBushArgumentType - * ``` */ -type KBAT = keyof BaseBushArgumentType; -/** ```ts - * <R> = BushArgumentTypeCasterReturn<R> - * ``` */ -type ATCR<R> = BushArgumentTypeCasterReturn<R>; -/** ```ts - * BushArgumentType - * ``` */ -type AT = BushArgumentType; -/** ```ts - * BaseBushArgumentType - * ``` */ -type BAT = BaseBushArgumentType; - -/** ```ts - * <T extends BushArgumentTypeCaster> = BushArgumentTypeCaster<BushArgumentTypeCasterReturn<T>> - * ``` */ -type ATCATCR<T extends BushArgumentTypeCaster> = BushArgumentTypeCaster<BushArgumentTypeCasterReturn<T>>; -/** ```ts - * <T extends keyof BaseBushArgumentType> = BushArgumentTypeCaster<BaseBushArgumentType[T]> - * ``` */ -type ATCBAT<T extends keyof BaseBushArgumentType> = BushArgumentTypeCaster<BaseBushArgumentType[T]>; 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<string>; -export type SbItemId = Uppercase<string>; -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<HideFlagsString> { - 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<string, number>; - pet_levels: number[]; - custom_pet_leveling: Record<string, { type: number; pet_levels: number[]; max_level: number }>; - pet_types: Record<string, string>; -} - -export interface PetNums { - [key: string]: { - [key: string]: { - '1': { - otherNums: number[]; - statNums: Record<string, number>; - }; - '100': { - otherNums: number[]; - statNums: Record<string, number>; - }; - '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 = <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<true | string> { - 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<typeof baseMuteResponse> | ValueOf<typeof permissionsResponse> | 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<ActivePunishment | null> { - 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<boolean> { - 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<boolean> { - 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<ButtonBuilder>({ - 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<R = unknown> = (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<K extends keyof BushClientEvents>(event: K, listener: (...args: BushClientEvents[K]) => Awaitable<void>): this; - once<K extends keyof BushClientEvents>(event: K, listener: (...args: BushClientEvents[K]) => Awaitable<void>): this; - emit<K extends keyof BushClientEvents>(event: K, ...args: BushClientEvents[K]): boolean; - off<K extends keyof BushClientEvents>(event: K, listener: (...args: BushClientEvents[K]) => Awaitable<void>): this; - removeAllListeners<K extends keyof BushClientEvents>(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<Ready extends boolean = boolean> extends AkairoClient<Ready> { - 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<Ready, string, string | null>; - - /* =-=-= 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 <<v17.x.x>>, not <<${process.version}>>.`, false)); - process.exit(2); - } - - this.setMaxListeners(20); - - this.perspective = await google.discoverAPI<any>('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: <ArgumentTypeCaster>duration, - contentWithDuration: <ArgumentTypeCaster>contentWithDuration, - permission: <ArgumentTypeCaster>permission, - snowflake: <ArgumentTypeCaster>snowflake, - discordEmoji: <ArgumentTypeCaster>discordEmoji, - roleWithDuration: <ArgumentTypeCaster>roleWithDuration, - abbreviatedNumber: <ArgumentTypeCaster>abbreviatedNumber, - durationSeconds: <ArgumentTypeCaster>durationSeconds, - globalUser: <ArgumentTypeCaster>globalUser, - messageLink: <ArgumentTypeCaster>messageLink, - tinyColor: <ArgumentTypeCaster>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 <<Sentry>>.`, 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 <<instance database>>.`, false); - } catch (e) { - await this.console.error( - 'startup', - `Failed to connect to <<instance database>> 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 <<shared database>>.`, false); - } catch (e) { - await this.console.error( - 'startup', - `Failed to connect to <<shared database>> 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 <<cache>>.`, 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<string> { - 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<Ready extends boolean = boolean> extends EventEmitter, PatchedElements, AkairoClient<Ready> { - on<K extends keyof BushClientEvents>(event: K, listener: (...args: BushClientEvents[K]) => Awaitable<void>): this; - once<K extends keyof BushClientEvents>(event: K, listener: (...args: BushClientEvents[K]) => Awaitable<void>): this; - emit<K extends keyof BushClientEvents>(event: K, ...args: BushClientEvents[K]): boolean; - off<K extends keyof BushClientEvents>(event: K, listener: (...args: BushClientEvents[K]) => Awaitable<void>): this; - removeAllListeners<K extends keyof BushClientEvents>(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<ArgumentOptions, 'type' | 'prompt'>, 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> | 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<CommandOptions, 'userPermissions' | 'clientPermissions' | 'args'>, - 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<BaseBushCommandOptions, 'helpArgs'> | Omit<BaseBushCommandOptions, 'args'>; - -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<CommandOptions & ExtendedCommandOptions> = {}; - 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<T extends keyof PseudoArguments> = NonNullable<PseudoArguments[T]>; -export type OptArgType<T extends keyof PseudoArguments> = 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<Message>; -}; 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<string, BushCommand>; - public declare categories: Collection<string, Category<string, BushCommand>>; -} - -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<Snowflake, Message> - ]; - 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<Snowflake, boolean>, - all?: boolean - ]; - bushUnlockdown: [ - moderator: GuildMember, - reason: string | undefined, - channelsSuccessMap: Collection<Snowflake, boolean>, - all?: boolean - ]; - massBan: [ - moderator: GuildMember, - guild: Guild, - reason: string | undefined, - results: Collection<Snowflake, BanResponse> - ]; - 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<boolean>; - /** - * 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<GuildDB['enabledFeatures']>; - /** - * 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<GuildDB['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 - */ - toggleFeature(feature: GuildFeatures, moderator?: GuildMember): Promise<GuildDB['enabledFeatures']>; - /** - * Fetches a custom setting for the guild - * @param setting The setting to get - */ - getSetting<K extends keyof GuildModel>(setting: K): Promise<GuildModel[K]>; - /** - * 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<K extends Exclude<keyof GuildModel, 'id'>>( - setting: K, - value: GuildModel[K], - moderator?: GuildMember - ): Promise<GuildModel>; - /** - * 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<TextChannel | 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} - */ - sendLogChannel(logType: GuildLogType, message: string | MessagePayload | MessageOptions): Promise<Message | null | undefined>; - /** - * 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<void>; - /** - * 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<BanResponse>; - /** - * {@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<BanResponse>; - /** - * 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<UnbanResponse>; - /** - * Denies send permissions in specified channels - * @param options The options for locking down the guild - */ - lockdown(options: LockdownOptions): Promise<LockdownResponse>; - quote(rawQuote: APIMessage, channel: GuildTextBasedChannel): Promise<Message | null>; - } -} - -/** - * Represents a guild (or a server) on Discord. - * <info>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}.</info> - */ -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<boolean> { - 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<GuildModel['enabledFeatures']> { - 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<GuildModel['enabledFeatures']> { - 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<GuildModel['enabledFeatures']> { - 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<K extends keyof GuildModel>(setting: K): Promise<GuildModel[K]> { - 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<K extends Exclude<keyof GuildModel, 'id'>>( - setting: K, - value: GuildDB[K], - moderator?: GuildMember - ): Promise<GuildDB> { - 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<TextChannel | undefined> { - 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<Message | null | undefined> { - 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> { - 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<BanResponse> { - // 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<BanResponse> { - 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<UnbanResponse> { - // 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<LockdownResponse> { - 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<Snowflake, Error>(); - const success = new Collection<Snowflake, boolean>(); - const ret = await (async (): Promise<LockdownResponse> => { - 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<Message | null> { - 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<string, Webhook> = 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<WebhookMessageOptions, 'flags'> = {}; - - 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<AttachmentPayload>)) - : 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> = 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<typeof unbanResponse>; - -/** - * 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<string, Error>; 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<boolean>; - /** - * 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<AddRoleResponse>; - /** - * 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<RemoveRoleResponse>; - /** - * 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<MuteResponse>; - /** - * 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<UnmuteResponse>; - /** - * 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<KickResponse>; - /** - * 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<Exclude<BanResponse, typeof banResponse['ALREADY_BANNED']>>; - /** - * Prevents a user from speaking in a channel. - * @param options Options for blocking the user. - */ - bushBlock(options: BlockOptions): Promise<BlockResponse>; - /** - * Allows a user to speak in a channel. - * @param options Options for unblocking the user. - */ - bushUnblock(options: UnblockOptions): Promise<UnblockResponse>; - /** - * Mutes a user using discord's timeout feature. - * @param options Options for timing out the user. - */ - bushTimeout(options: BushTimeoutOptions): Promise<TimeoutResponse>; - /** - * Removes a timeout from a user. - * @param options Options for removing the timeout. - */ - bushRemoveTimeout(options: BushPunishmentOptions): Promise<RemoveTimeoutResponse>; - /** - * 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<boolean> { - 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<AddRoleResponse> { - // 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<RemoveRoleResponse> { - // 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<MuteResponse> { - // 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<UnmuteResponse> { - // 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<KickResponse> { - // 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<Exclude<BanResponse, typeof banResponse['ALREADY_BANNED']>> { - // 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<BlockResponse> { - 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<UnblockResponse> { - 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<TimeoutResponse> { - // 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<RemoveTimeoutResponse> { - // 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<typeof warnResponse>; - -/** - * Response returned when adding a role to a user. - */ -export type AddRoleResponse = ValueOf<typeof addRoleResponse>; - -/** - * Response returned when removing a role from a user. - */ -export type RemoveRoleResponse = ValueOf<typeof removeRoleResponse>; - -/** - * Response returned when muting a user. - */ -export type MuteResponse = ValueOf<typeof muteResponse>; - -/** - * Response returned when unmuting a user. - */ -export type UnmuteResponse = ValueOf<typeof unmuteResponse>; - -/** - * Response returned when kicking a user. - */ -export type KickResponse = ValueOf<typeof kickResponse>; - -/** - * Response returned when banning a user. - */ -export type BanResponse = ValueOf<typeof banResponse>; - -/** - * Response returned when blocking a user. - */ -export type BlockResponse = ValueOf<typeof blockResponse>; - -/** - * Response returned when unblocking a user. - */ -export type UnblockResponse = ValueOf<typeof unblockResponse>; - -/** - * Response returned when timing out a user. - */ -export type TimeoutResponse = ValueOf<typeof timeoutResponse>; - -/** - * Response returned when removing a timeout from a user. - */ -export type RemoveTimeoutResponse = ValueOf<typeof removeTimeoutResponse>; - -/** - * @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<Cached extends boolean = boolean> extends Message<Cached> { - public declare util: CommandUtil<Message>; - - 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<ExtendedUser, 'username' | 'tag' | 'discriminator' | 'isOwner' | 'isSuperUser'>; - -/** - * 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<T> { - includes<S, R extends `${Extract<S, string>}`>( - this: ReadonlyArray<R>, - 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<A, B> extends Model<A, B> { - /** - * 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<ActivePunishmentModel, ActivePunishmentModelCreationAttributes> - 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<GuildModel, GuildModelCreationAttributes> 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 = <T>(et: { [K in keyof T]: PartialBy<GuildSetting, 'configurable' | 'subType' | 'replaceNullWith'> }) => { - 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<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; - -const asGuildFeature = <T>(gf: { [K in keyof T]: PartialBy<GuildFeature, 'hidden' | 'default'> }): { - [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<HighlightModel, HighLightCreationAttributes> 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<LevelModel, LevelModelCreationAttributes> 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<ModLogModel, ModLogModelCreationAttributes> 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<ReminderModel, ReminderModelCreationAttributes> 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<StickyRoleModel, StickyRoleModelCreationAttributes> 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<GlobalModel, GlobalModelCreationAttributes> 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<GuildCountModel, GuildCountCreationAttributes> 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<MemberCountModel, MemberCountCreationAttributes> 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<SharedModel, SharedModelCreationAttributes> 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<StatModel, StatModelCreationAttributes> 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', <any>`${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', <any>`${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<GlobalModel, 'environment'> { - public disabledCommands: string[] = []; - public blacklistedChannels: Snowflake[] = []; - public blacklistedGuilds: Snowflake[] = []; - public blacklistedUsers: Snowflake[] = []; -} - -export class SharedCache implements Omit<SharedModel, 'primaryKey'> { - 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<Snowflake, Guild> {} 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<User[]> { - 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<HasteResults> { - 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<User | null> { - 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 = /<@!?(?<id>\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<string> { - 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<HasteResults> { - 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<K extends keyof GlobalCache>(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<K extends keyof SharedCache>(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<K extends keyof Client['cache']['global']>( - action: 'add' | 'remove', - key: K, - value: Client['cache']['global'][K][0] - ): Promise<Global | void> { - 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<K extends Exclude<keyof Client['cache']['shared'], 'badWords' | 'autoBanCode'>>( - action: 'add' | 'remove', - key: K, - value: Client['cache']['shared'][K][0] - ): Promise<Shared | void> { - 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<K extends keyof Client['cache']['global']>( - key: K, - value: Client['cache']['global'][K] - ): Promise<Global | void> { - 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<K extends Exclude<keyof Client['cache']['shared'], 'badWords' | 'autoBanCode'>>( - key: K, - value: Client['cache']['shared'][K] - ): Promise<Shared | void> { - 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<User | undefined> { - 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<Pronoun | undefined> { - 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<MessageLinkParts[]> { - 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<APIMessage[]> { - 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<Exclude<TextBasedChannel, DMChannel | PartialDMChannel> | 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: '<a:Loading:853419254619963392>', - 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: / (?:(?<milliseconds>-?(?:\d+)?\.?\d+) *(?:milliseconds?|msecs?|ms))/im, - value: Time.Millisecond - }, - seconds: { - match: / (?:(?<seconds>-?(?:\d+)?\.?\d+) *(?:seconds?|secs?|s))/im, - value: Time.Second - }, - minutes: { - match: / (?:(?<minutes>-?(?:\d+)?\.?\d+) *(?:minutes?|mins?|m))/im, - value: Time.Minute - }, - hours: { - match: / (?:(?<hours>-?(?:\d+)?\.?\d+) *(?:hours?|hrs?|h))/im, - value: Time.Hour - }, - days: { - match: / (?:(?<days>-?(?:\d+)?\.?\d+) *(?:days?|d))/im, - value: Time.Day - }, - weeks: { - match: / (?:(?<weeks>-?(?:\d+)?\.?\d+) *(?:weeks?|w))/im, - value: Time.Week - }, - months: { - match: / (?:(?<months>-?(?:\d+)?\.?\d+) *(?:months?|mon|mo))/im, - value: Time.Month - }, - years: { - match: / (?:(?<years>-?(?:\d+)?\.?\d+) *(?:years?|y))/im, - value: Time.Year - } -} as const); - -export const regex = deepLock({ - snowflake: /^\d{15,21}$/im, - - discordEmoji: /<a?:(?<name>[a-zA-Z0-9_]+):(?<id>\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: - /<?https:\/\/(?:ptb\.|canary\.|staging\.)?discord(?:app)?\.com?\/channels\/(?<guild_id>\d{15,21})\/(?<channel_id>\d{15,21})\/(?<message_id>\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<typeof console['log']>['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<Message | null> { - 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<Message | null> { - 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<void> { - 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<void> { - 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<void> { - 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<void> { - 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<void> { - 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<void> { - 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<void> { - 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<T> = { [K in keyof T]: T[K] extends Record<string, any> ? StripPrivate<T[K]> : T[K] }; -export type ValueOf<T> = 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<T>(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<string> { - 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<Message | APIMessage | undefined> { - 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<T>(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<T>(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<T>(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 `<t:${Math.round(new Date().getTime() / 1_000 + duration / 1_000)}:R>`; -} - -/** - * 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<D extends Date | undefined | null>( - date: D, - style: TimestampStyle = 'f' -): D extends Date ? string : undefined { - if (!date) return date as unknown as D extends Date ? string : undefined; - return `<t:${Math.round(date.getTime() / 1_000)}:${style}>` 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, any>): string { - // modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class/31055217#31055217 - let props: string[] = []; - let obj_: Record<string, any> = 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<string, any>): symbol[] { - let symbols: symbol[] = []; - let obj_: Record<string, any> = 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<ParsedDurationRes> { - 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<T extends keyof BaseBushArgumentType>( - 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<APIEmbed, 'description'>, 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<T>(obj: T): DeepWritable<T> { - return obj as DeepWritable<T>; -} 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; - } -} |