2022-11-13 08:46:50 -05:00
import Discord , { Client , GatewayIntentBits , Partials } from 'discord.js' ;
import fs from 'node:fs' ;
import { Database } from './database' ;
2022-11-13 19:18:15 -05:00
import timeNames from './timeNames' ;
2022-11-13 08:46:50 -05:00
export class TClient extends Client {
2022-11-14 03:45:40 -05:00
invites : Map < any , any > ;
commands : Discord.Collection < string , any > ;
registry : Array < Discord.ApplicationCommandDataResolvable > ;
2022-11-11 19:58:11 -05:00
config : any ;
tokens : any ;
YTCache : any ;
2022-11-14 03:45:40 -05:00
embed : typeof Discord . EmbedBuilder ;
2022-11-11 19:58:11 -05:00
collection : any ;
messageCollector : any ;
attachmentBuilder : any ;
moment : any ;
2022-11-13 08:46:50 -05:00
xjs : any ;
axios : any ;
2022-11-15 09:06:18 -05:00
ms : any ;
2022-11-11 19:58:11 -05:00
memberCount_LastGuildFetchTimestamp : any ;
2022-11-14 03:45:40 -05:00
userLevels : userLevels ;
punishments : punishments ;
bonkCount : bonkCount ;
bannedWords : bannedWords ;
2022-11-11 19:58:11 -05:00
repeatedMessages : any ;
constructor ( ) {
super ( {
intents : [
GatewayIntentBits . Guilds , GatewayIntentBits . GuildMembers ,
GatewayIntentBits . GuildBans , GatewayIntentBits . GuildInvites ,
GatewayIntentBits . GuildPresences , GatewayIntentBits . GuildMessageReactions ,
GatewayIntentBits . DirectMessages , GatewayIntentBits . MessageContent
] ,
partials : [
Partials . Channel ,
Partials . Reaction ,
Partials . Message
] ,
2022-11-13 08:46:50 -05:00
allowedMentions : { repliedUser : false , parse : [ 'roles' , 'users' ] }
2022-11-11 19:58:11 -05:00
} )
this . invites = new Map ( ) ;
this . commands = new Discord . Collection ( ) ;
2022-11-14 03:45:40 -05:00
this . registry = [ ] ;
2022-11-11 19:58:11 -05:00
this . config = require ( './config.json' ) ;
this . tokens = require ( './tokens.json' ) ;
this . YTCache = {
'UCQ8k8yTDLITldfWYKDs3xFg' : undefined , // Daggerwin
'UCguI73--UraJpso4NizXNzA' : undefined // Machinery Restorer
}
this . embed = Discord . EmbedBuilder ;
this . collection = Discord . Collection ;
this . messageCollector = Discord . MessageCollector ;
this . attachmentBuilder = Discord . AttachmentBuilder ;
2022-11-13 08:46:50 -05:00
this . moment = import ( 'moment' ) ;
this . xjs = import ( 'xml-js' ) ;
this . axios = import ( 'axios' ) ;
2022-11-15 09:06:18 -05:00
this . ms = import ( 'ms' ) ;
2022-11-11 19:58:11 -05:00
this . memberCount_LastGuildFetchTimestamp = 0 ;
2022-11-15 09:06:18 -05:00
this . userLevels = new userLevels ( this ) ;
this . bonkCount = new bonkCount ( this ) ;
this . punishments = new punishments ( this ) ;
this . bannedWords = new bannedWords ( this ) ;
2022-11-11 19:58:11 -05:00
this . repeatedMessages = { } ;
2022-11-13 08:46:50 -05:00
this . setMaxListeners ( 80 )
2022-11-11 19:58:11 -05:00
}
async init ( ) {
this . login ( this . tokens . token_toast ) ;
this . punishments . initLoad ( ) ;
this . bannedWords . initLoad ( ) ;
this . bonkCount . initLoad ( ) ;
this . userLevels . initLoad ( ) . intervalSave ( 15000 ) . disableSaveNotifs ( ) ;
2022-11-14 03:45:40 -05:00
const commandFiles = fs . readdirSync ( './commands/slash' ) . filter ( file = > file . endsWith ( '.ts' ) ) ;
for ( const file of commandFiles ) {
const command = require ( ` ./commands/slash/ ${ file } ` ) ;
this . commands . set ( command . data . name , command )
this . registry . push ( command . data . toJSON ( ) )
}
2022-11-11 19:58:11 -05:00
}
2022-11-13 08:46:50 -05:00
formatPunishmentType ( punishment : Punishment , client : TClient , cancels : Punishment ) {
2022-11-11 19:58:11 -05:00
if ( punishment . type == 'removeOtherPunishment' ) {
2022-11-13 08:46:50 -05:00
cancels || = this . punishments . _content . find ( ( x : Punishment ) = > x . id === punishment . cancels )
2022-11-11 19:58:11 -05:00
return cancels . type [ 0 ] . toUpperCase ( ) + cancels . type . slice ( 1 ) + ' Removed' ;
} else return punishment . type [ 0 ] . toUpperCase ( ) + punishment . type . slice ( 1 ) ;
}
2022-11-13 08:46:50 -05:00
formatTime ( integer : number , accuracy = 1 , options? : formatTimeOpt ) {
2022-11-11 19:58:11 -05:00
let achievedAccuracy = 0 ;
2022-11-15 09:06:18 -05:00
let text :any = '' ;
2022-11-11 19:58:11 -05:00
const { longNames , commas } = options
for ( const timeName of timeNames ) {
if ( achievedAccuracy < accuracy ) {
const fullTimelengths = Math . floor ( integer / timeName . length ) ;
if ( fullTimelengths == 0 ) continue ;
achievedAccuracy ++ ;
text += fullTimelengths + ( longNames ? ( ' ' + timeName . name + ( fullTimelengths === 1 ? '' : 's' ) ) : timeName . name . slice ( 0 , timeName . name === 'month' ? 2 : 1 ) ) + ( commas ? ', ' : ' ' ) ;
integer -= fullTimelengths * timeName . length ;
} else {
break ;
}
}
2022-11-13 08:46:50 -05:00
if ( text . length == 0 ) text = integer + ( options ? . longNames ? ' milliseconds' : 'ms' ) + ( options ? . commas ? ', ' : '' ) ;
if ( options ? . commas ) {
2022-11-11 19:58:11 -05:00
text = text . slice ( 0 , - 2 ) ;
2022-11-13 08:46:50 -05:00
if ( options ? . longNames ) {
2022-11-11 19:58:11 -05:00
text = text . split ( '' ) ;
text [ text . lastIndexOf ( ',' ) ] = ' and' ;
text = text . join ( '' ) ;
}
} return text . trim ( ) ;
}
2022-11-13 08:46:50 -05:00
isStaff ( guildMember : Discord.GuildMember ) {
return this . config . mainServer . staffRoles . map ( ( x : string ) = > this . config . mainServer . roles [ x ] ) . some ( ( x : string ) = > guildMember . roles . cache . has ( x ) )
}
2022-11-15 09:06:18 -05:00
youNeedRole ( interaction : Discord.CommandInteraction , role :string ) {
return interaction . reply ( ` This command is restricted to <@& ${ this . config . mainServer . roles [ role ] } > ` )
}
2022-11-13 08:46:50 -05:00
alignText ( text : string , length : number , alignment : string , emptyChar = ' ' ) {
if ( alignment == 'right' ) {
text = emptyChar . repeat ( length - text . length ) + text ;
} else if ( alignment == 'middle' ) {
const emptyCharsPerSide = ( length - text . length ) / 2 ;
text = emptyChar . repeat ( Math . floor ( emptyCharsPerSide ) ) + text + emptyChar . repeat ( Math . floor ( emptyCharsPerSide ) ) ;
} else {
text = text + emptyChar . repeat ( length - text . length ) ;
} return text ;
}
createTable ( columnTitles = [ ] , rowsData = [ ] , options : createTableOpt , client : TClient ) {
const rows : any = [ ] ;
let { columnAlign = [ ] , columnSeparator = [ ] , columnEmptyChar = [ ] } = options ;
if ( columnSeparator . length < 1 ) columnSeparator . push ( '|' ) ;
columnSeparator = columnSeparator . map ( ( x : string ) = > ` ${ x } ` ) ;
// col widths
const columnWidths = columnTitles . map ( ( title : any , i ) = > Math . max ( title . length , . . . rowsData . map ( ( x : any ) = > x [ i ] . length ) ) ) ;
// first row
rows . push ( columnTitles . map ( ( title , i ) = > {
let text = client . alignText ( title , columnWidths [ i ] , columnAlign [ i ] , columnEmptyChar [ i ] ) ;
if ( columnSeparator [ i ] ) {
text += ' ' . repeat ( columnSeparator [ i ] . length ) ;
}
return text ;
} ) . join ( '' ) )
// big line
rows . push ( '━' . repeat ( rows [ 0 ] . length ) ) ;
//data
// remove unicode
rowsData . map ( ( row : any ) = > {
return row . map ( ( element : string ) = > {
return element . split ( '' ) . map ( ( char : string ) = > {
if ( char . charCodeAt ( 0 ) > 128 ) return '□' ;
} ) . join ( '' )
} )
} )
rows . push ( rowsData . map ( ( row : any ) = > row . map ( ( element : string , i : number ) = > {
return client . alignText ( element , columnWidths [ i ] , columnEmptyChar [ i ] ) + ( i === columnTitles . length - 1 ? '' : columnSeparator [ i ] ) ;
} ) . join ( '' )
) . join ( '\n' ) )
2022-11-11 19:58:11 -05:00
2022-11-13 08:46:50 -05:00
return rows . join ( '\n' ) ;
}
makeModlogEntry ( data : Punishment , client : TClient ) {
const cancels = data . cancels ? client . punishments . _content . find ( ( x : Punishment ) = > x . id === data . cancels ) : null ;
// turn data into embed
const embed = new this . embed ( ) . setColor ( this . config . embedColor ) . setTimestamp ( data . time )
. setTitle ( ` ${ this . formatPunishmentType ( data , client , cancels ) } | Case # ${ data . id } ` ) . addFields (
{ name : '🔹 User' , value : ` <@ ${ data . member } > \` ${ data . member } \` ` , inline : true } ,
{ name : '🔹 Moderator' , value : ` <@ ${ data . moderator } > \` ${ data . moderator } \` ` , inline : true } ,
{ name : '\u200b' , value : ` \ u200b ` , inline : true } ,
2022-11-15 09:06:18 -05:00
{ name : '🔹 Reason' , value : ` \` ${ data . reason || 'Reason unspecified' } \` ` , inline : true } ,
2022-11-13 08:46:50 -05:00
)
if ( data . duration ) {
embed . addFields (
{ name : '🔹 Duration' , value : client.formatTime ( data . duration , 100 ) , inline : true } ,
{ name : '\u200b' , value : '\u200b' , inline : true }
)
}
2022-11-15 09:06:18 -05:00
if ( data . cancels ) embed . addFields ( { name : '🔹 Overwrites' , value : ` This case overwrites Case # ${ cancels . id } \` ${ cancels . reason } \` ` } ) ;
2022-11-13 08:46:50 -05:00
// send embed to log channel
( client . channels . cache . get ( client . config . mainServer . channels . logs ) as Discord . TextChannel ) . send ( { embeds : [ embed ] } )
}
2022-11-15 09:06:18 -05:00
async punish ( client : TClient , interaction : Discord.ChatInputCommandInteraction < 'cached' > , type : string ) {
2022-11-13 08:46:50 -05:00
let result : any ;
2022-11-15 09:06:18 -05:00
if ( ! client . isStaff ( interaction . member as Discord . GuildMember ) ) return this . youNeedRole ( interaction , 'dcmod' )
//if (type !== ('warn' || 'mute') && (interaction.member as Discord.GuildMember).roles.cache.has(client.config.mainServer.roles.idk)) return this.youNeedRole(interaction, 'dcmod');
const time = this . ms ( interaction . options . getString ( 'time' ) ) ;
const reason = interaction . options . getString ( 'reason' ) ? ? 'Reason unspecified' ;
if ( type == 'ban' ) {
const user = interaction . options . getUser ( 'member' ) as Discord . User ;
if ( interaction . user . id == user . id ) return interaction . reply ( ` You cannot ${ type } yourself! ` ) ;
result = await this . punishments . addPunishment ( type , user , { time , reason , interaction } , interaction . user . id ) ;
2022-11-13 08:46:50 -05:00
} else {
2022-11-15 09:06:18 -05:00
const member = interaction . options . getMember ( 'member' ) as Discord . GuildMember ;
if ( interaction . user . id == member . id ) return interaction . reply ( ` You cannot ${ type } yourself! ` ) ;
if ( this . isStaff ( member ) ) return interaction . reply ( ` You cannot ${ type } other staff! ` ) ;
result = await this . punishments . addPunishment ( type , member , { time , reason , interaction } , interaction . user . id ) ;
2022-11-13 08:46:50 -05:00
}
2022-11-15 09:06:18 -05:00
( typeof result == 'string' ? interaction . reply ( { content : ` ${ result } ` } ) : interaction . reply ( { embeds : [ result ] } ) )
2022-11-13 08:46:50 -05:00
} ;
2022-11-15 09:06:18 -05:00
async unPunish ( client : TClient , interaction : Discord.ChatInputCommandInteraction < 'cached' > ) {
if ( ! client . isStaff ( interaction . member as Discord . GuildMember ) ) return this . youNeedRole ( interaction , 'dcmod' ) ;
const punishment = this . punishments . _content . find ( ( x :Punishment ) = > x . id === interaction . options . getInteger ( 'case_id' ) ) ;
if ( ! punishment ) return interaction . reply ( { content : 'Invalid Case #' , ephemeral : true } ) ;
//if (type !== ('warn' || 'mute') && (interaction.member as Discord.GuildMember).roles.cache.has(client.config.mainServer.roles.idk)) return this.youNeedRole(interaction, 'dcmod');
const reason = interaction . options . getString ( 'reason' ) ? ? 'Reason unspecified' ;
const unpunishResult = await this . punishments . removePunishment ( punishment . id , interaction . user . id , reason ) ;
interaction . reply ( unpunishResult )
2022-11-13 08:46:50 -05:00
}
2022-11-15 09:06:18 -05:00
2022-11-13 08:46:50 -05:00
async YTLoop ( YTChannelID : string , YTChannelName : string , DCChannelID : string ) {
const Data = this . xjs . xml2js ( ( await this . axios . get ( ` https://www.youtube.com/feeds/videos.xml?channel_id= ${ YTChannelID } ` , { timeout : 5000 } ) ) , { compact : true , spaces : 2 } ) . catch ( ( ) = > { return null } ) ;
if ( ! Data ) return ;
if ( this . YTCache [ YTChannelID ] == undefined ) {
this . YTCache [ YTChannelID ] = Data . feed . entry [ 0 ] [ 'yt:videoId' ] . _text ;
return ;
}
if ( Data . feed . entry [ 1 ] [ 'yt:videoId' ] . _text == this . YTCache [ YTChannelID ] ) {
this . YTCache [ YTChannelID ] = Data . feed . entry [ 0 ] [ 'yt:videoId' ] . _text
( this . channels . resolve ( DCChannelID ) as Discord . TextChannel ) . send ( ` ** ${ YTChannelName } ** just uploaded a video! \ n ${ Data . feed . entry [ 0 ] . link . _attributes . href } ` )
}
}
2022-11-14 03:45:40 -05:00
}
//class
class bannedWords extends Database {
client : TClient ;
constructor ( client : TClient ) {
super ( './database/bannedWords.json' , 'array' ) ;
this . client = client ;
}
}
class punishments extends Database {
client : TClient ;
constructor ( client : TClient ) {
super ( './database/punishments.json' , 'array' ) ;
this . client = client ;
}
createId ( ) {
2022-11-15 09:06:18 -05:00
return Math . max ( . . . this . client . punishments . _content . map ( ( x :Punishment ) = > x . id ) , 0 ) + 1 ;
2022-11-14 03:45:40 -05:00
}
async addPunishment ( type : string , member : any , options : punOpt , moderator : string ) {
const now = Date . now ( ) ;
const { time , reason , interaction } = options ;
const ms = require ( 'ms' ) ;
2022-11-15 09:06:18 -05:00
let timeInMillis : number ;
2022-11-14 03:45:40 -05:00
if ( type !== 'mute' ) {
timeInMillis = time ? ms ( time ) : null ;
} else {
timeInMillis = time ? ms ( time ) : 2419200000 ;
}
switch ( type ) {
case 'ban' :
2022-11-15 09:06:18 -05:00
const banData : Punishment = { type , id : this.createId ( ) , member : member.id , moderator , time : now } ;
const dm1 : Discord.Message = await member . send ( ` You've been banned from ${ interaction . guild . name } ${ timeInMillis ? ` for ${ this . client . formatTime ( timeInMillis , 4 , { longNames : true , commas : true } )} ( ${ timeInMillis } ms) ` : 'forever' } for reason \` ${ reason || 'Reason unspecified' } \` (Case # ${ banData . id } ) ` ) . catch ( ( ) = > { return interaction . channel . send ( 'Failed to DM user.' ) } )
const banResult = await interaction . guild . bans . create ( member . id , { reason : ` ${ reason || 'Reason unspecified' } | Case # ${ banData . id } ` } ) . catch ( ( err :Error ) = > err . message ) ;
if ( typeof banResult === 'string' ) {
dm1 . delete ( )
return ` Ban was unsuccessful: ${ banResult } `
} else {
if ( timeInMillis ) {
banData . endTime = now + timeInMillis ;
banData . duration = timeInMillis
}
if ( reason ) banData . reason = reason ;
this . client . makeModlogEntry ( banData , this . client ) ;
this . addData ( banData ) . forceSave ( ) ;
return new this . client . embed ( ) . setColor ( this . client . config . embedColor ) . setTitle ( ` Case # ${ banData . id } : Ban ` ) . setDescription ( ` ${ member ? . user ? . tag ? ? member ? . tag } \ n<@ ${ member . id } > \ n( \` ${ member . id } \` ) ` ) . addFields (
{ name : 'Reason' , value : ` \` ${ reason || 'Reason unspecified' } \` ` } ,
{ name : 'Duration' , value : ` ${ timeInMillis ? ` for ${ this . client . formatTime ( timeInMillis , 4 , { longNames : true , commas : true } )} ( ${ timeInMillis } ms) ` : 'forever' } ` }
)
}
2022-11-14 03:45:40 -05:00
case 'softban' :
2022-11-15 09:06:18 -05:00
const guild = member . guild ;
const softbanData : Punishment = { type , id : this.createId ( ) , member : member.user.id , moderator , time : now } ;
const dm2 = Discord . Message = await member . send ( ` You've been softbanned from ${ member . guild . name } ${ timeInMillis ? ` for ${ this . client . formatTime ( timeInMillis , 4 , { longNames : true , commas : true } )} ( ${ timeInMillis } ms) ` : 'forever' } for reason \` ${ reason || 'Reason unspecified' } \` (Case # ${ softbanData . id } ) ` ) . catch ( ( ) = > { return interaction . channel . send ( 'Failed to DM user.' ) } )
const softbanResult = await member . ban ( { deleteMessageDays : 3 , reason : ` ${ reason || 'Reason unspecified' } | Case # ${ softbanData . id } ` } ) . catch ( ( err :Error ) = > err . message ) ;
if ( typeof softbanResult === 'string' ) {
dm2 . delete ( ) ;
return ` Softban was unsuccessful: ${ softbanResult } ` ;
}
2022-11-14 03:45:40 -05:00
case 'kick' :
case 'warn' :
case 'mute' :
}
}
2022-11-15 09:06:18 -05:00
async removePunishment ( caseId :number , moderator :any , reason :string ) : Promise < any > { }
2022-11-14 03:45:40 -05:00
}
class userLevels extends Database {
client : TClient ;
constructor ( client : TClient ) {
super ( './database/userLevels.json' , 'object' ) ;
this . client = client
}
incrementUser ( userid : string ) {
2022-11-15 09:06:18 -05:00
const data = this . _content [ userid ] ; // User's data. Integer for old format, object for new format.
2022-11-14 03:45:40 -05:00
2022-11-15 09:06:18 -05:00
if ( typeof data == 'number' ) { // If user's data is an integer, convert it into object for new format.
this . _content [ userid ] = { messages : data , level : 0 } ;
}
if ( data ) { // If user exists on file...
this . _content [ userid ] . messages ++ ; // Increment their message count
if ( data . messages >= this . algorithm ( data . level + 2 ) ) { // Quietly level up users who can surpass more than 2 levels at once, usually due to manually updating their message count
2022-11-14 03:45:40 -05:00
while ( data . messages > this . algorithm ( data . level + 1 ) ) {
this . _content [ userid ] . level ++ ;
console . log ( ` ${ userid } EXTENDED LEVELUP ${ this . _content [ userid ] . level } ` )
}
2022-11-15 09:06:18 -05:00
} else if ( data . messages >= this . algorithm ( data . level + 1 ) ) { // If user's message count meets/exceeds message requirement for next level...
this . _content [ userid ] . level ++ ; // Level them up.
2022-11-14 03:45:40 -05:00
( this . client . channels . resolve ( this . client . config . mainServer . channels . thismeanswar ) as Discord . TextChannel ) . send ( { content : ` <@ ${ userid } > has reached level ** ${ data . level } **. GG! ` } )
}
2022-11-15 09:06:18 -05:00
} else { // If user doesn't exist on file, create an object for it.
2022-11-14 03:45:40 -05:00
this . _content [ userid ] = { messages : 1 , level : 0 } ;
}
}
2022-11-15 09:06:18 -05:00
algorithm ( level : number ) { // Algorithm for determining levels. If adjusting, recommended to only change the integer at the end of equation.
2022-11-14 03:45:40 -05:00
return level * level * 15 ;
}
2022-11-15 09:06:18 -05:00
}
class bonkCount extends Database {
client : TClient ;
constructor ( client : TClient ) {
super ( './database/bonkCount.json' , 'object' )
this . client = client
}
_incrementUser ( userid : string ) {
const amount = this . _content [ userid ] ;
if ( amount ) this . _content [ userid ] ++ ;
else this . _content [ userid ] = 1 ;
return this ;
}
getUser ( userid : string ) {
return this . _content [ userid ] || 0 ;
}
2022-11-13 08:46:50 -05:00
}