1- import { resolve } from "node:path" ;
2- import { readFile , writeFile } from "node:fs/promises" ;
3- import { ActionRowBuilder , ButtonBuilder , ButtonStyle , EmbedBuilder , Events , type Client , type GuildMember , type Message , type MessageCreateOptions } from "discord.js" ;
1+ import { Events , type Client } from "discord.js" ;
42import k from "kleur" ;
53import "dotenv/config" ;
64import { client , botToken } from "@lib/client.ts" ;
75import { em , initDatabase } from "@lib/db.ts" ;
8- import { cmdInstances , initRegistry , registerCommandsForGuild } from "@lib/registry.ts" ;
6+ import { initRegistry , registerCommandsForGuild } from "@lib/registry.ts" ;
97import { autoPlural } from "@lib/text.ts" ;
108import { envVarEq , getEnvVar } from "@lib/env.ts" ;
119import { initTranslations } from "@lib/translate.ts" ;
12- import { getCommitHash , getHash , ghBaseUrl } from "@lib/misc.ts" ;
13- import { Col } from "@lib/embedify.ts" ;
1410import { GuildConfig } from "@models/GuildConfig.model.ts" ;
15- import { UserSettings } from "@models/UserSettings.model.ts" ;
16- import pkg from "@root/package.json" with { type : "json" } ;
11+ import { metChanId , metGuildId , updateMetrics } from "@src/metrics.ts" ;
1712
1813//#region validate env
1914
@@ -63,55 +58,33 @@ async function init() {
6358 } , 1000 ) ;
6459}
6560
66- //#region metrics:vars
6761
68- const metGuildId = getEnvVar ( "METRICS_GUILD" , "stringOrUndefined" ) ;
69- const metChanId = getEnvVar ( "METRICS_CHANNEL" , "stringOrUndefined" ) ;
70- const metUpdIvRaw = getEnvVar ( "METRICS_UPDATE_INTERVAL" , "number" ) ;
71- const metUpdInterval = Math . max ( isNaN ( metUpdIvRaw ) ? 30 : metUpdIvRaw , 3 ) ;
72-
73- const initTime = Date . now ( ) ;
74- const metricsManifFile = resolve ( ".metrics.json" ) ;
75- let metricsData : MetricsManifest | undefined ;
76- let firstMetricRun = true ;
77-
78- //#region metrics:types
79-
80- type MetricsManifest = {
81- msgId : string | null ;
82- metricsHash : string | null ;
83- } ;
62+ //#region intervalChks
8463
85- type MetricsData = {
86- guildsAmt : number ;
87- uptimeStr : string ;
88- slashCmdAmt : number ;
89- ctxCmdAmt : number ;
90- usersAmt : number ;
91- totalMembersAmt : number ;
92- uniqueMembersAmt : number ;
93- } ;
64+ const metUpdIvRaw = getEnvVar ( "METRICS_UPDATE_INTERVAL" , "number" ) ;
65+ const metUpdInterval = Math . max ( isNaN ( metUpdIvRaw ) ? 30 : metUpdIvRaw , 1 ) ;
9466
95- //#region m:intervalChks
67+ const chkGldIntervalRaw = getEnvVar ( "GUILD_CHECK_INTERVAL" , "number" ) ;
68+ const chkGldInterval = Math . max ( isNaN ( chkGldIntervalRaw ) ? 300 : chkGldIntervalRaw , 10 ) ;
9669
9770/** Runs all interval checks */
9871async function intervalChecks ( client : Client , i : number ) {
9972 try {
100- const tasks : Promise < void | unknown > [ ] = [ ] ;
73+ const ivTasks : Promise < void | unknown > [ ] = [ ] ;
10174
102- if ( i === 0 || i % metUpdInterval === 0 ) {
103- tasks . push ( updateMetrics ( client ) ) ;
104- tasks . push ( checkGuilds ( client ) ) ;
105- }
75+ if ( metGuildId && metChanId && ( i === 0 || i % metUpdInterval === 0 ) )
76+ ivTasks . push ( updateMetrics ( client ) ) ;
77+ if ( i === 0 || i % chkGldInterval === 0 )
78+ ivTasks . push ( checkGuilds ( client ) ) ;
10679
107- tasks . length > 0 && await Promise . allSettled ( tasks ) ;
80+ ivTasks . length > 0 && await Promise . allSettled ( ivTasks ) ;
10881 }
10982 catch ( e ) {
11083 console . error ( "Couldn't run interval checks:" , e ) ;
11184 }
11285}
11386
114- //#region m: chkGuildJoin
87+ //#region chkGuildJoin
11588
11689/**
11790 * Checks if guilds were joined while the bot was offline and creates a GuildConfig for them and registers slash commands.
@@ -135,171 +108,4 @@ async function checkGuilds(client: Client) {
135108 await Promise . allSettled ( tasks ) ;
136109}
137110
138- //#region m:updateMetr
139-
140- /** Update metrics */
141- async function updateMetrics ( client : Client ) {
142- try {
143- if ( ! metGuildId || ! metChanId )
144- return ;
145-
146- let slashCmdAmt = 0 , ctxCmdAmt = 0 ;
147- for ( const { type } of [ ...cmdInstances . values ( ) ] ) {
148- if ( type === "slash" )
149- slashCmdAmt ++ ;
150- else if ( type === "ctx" )
151- ctxCmdAmt ++ ;
152- }
153-
154- await client . guilds . fetch ( ) ;
155-
156- const totalMembersAmt = client . guilds . cache
157- . reduce ( ( acc , g ) => acc + g . memberCount , 0 ) ;
158-
159- const memMap = new Map < string , GuildMember > ( ) ;
160- const memMapPromises : Promise < void > [ ] = [ ] ;
161- for ( const g of client . guilds . cache . values ( ) ) {
162- memMapPromises . push ( new Promise ( async ( res , rej ) => {
163- try {
164- await g . members . fetch ( ) ;
165- for ( const m of g . members . cache . values ( ) ) {
166- if ( memMap . has ( m . id ) || m . user . bot || m . user . system || m . user . partial || m . partial )
167- continue ;
168- memMap . set ( m . id , m ) ;
169- }
170- res ( ) ;
171- }
172- catch ( e ) {
173- rej ( e ) ;
174- }
175- } ) ) ;
176- }
177- await Promise . all ( memMapPromises ) ;
178-
179- const latestMetrics = {
180- guildsAmt : client . guilds . cache . size ,
181- uptimeStr : getUptime ( ) ,
182- slashCmdAmt,
183- ctxCmdAmt,
184- usersAmt : ( await em . findAll ( UserSettings ) ) . length ,
185- totalMembersAmt,
186- uniqueMembersAmt : memMap . size ,
187- } as const satisfies MetricsData ;
188-
189- const metricsChan = client . guilds . cache . find ( g => g . id === metGuildId ) ?. channels . cache . find ( c => c . id === metChanId ) ;
190- let metricsMsg : Message | undefined ;
191-
192- try {
193- metricsData = metricsData ?? JSON . parse ( String ( await readFile ( metricsManifFile , "utf8" ) ) ) ;
194- }
195- catch {
196- metricsData = metricsData ?? { msgId : null , metricsHash : null } ;
197- }
198-
199- if ( metricsData && metricsChan && metricsChan . isTextBased ( ) ) {
200- const latestMetHash = getHash ( JSON . stringify ( latestMetrics ) ) ;
201- const metricsChanged = firstMetricRun || metricsData . metricsHash !== latestMetHash ;
202- if ( metricsChanged )
203- metricsData . metricsHash = latestMetHash ;
204-
205- if ( metricsChanged && typeof metricsData . msgId === "string" && metricsData . msgId . length > 0 ) {
206- metricsMsg = ( await metricsChan . messages . fetch ( { limit : 3 , around : metricsData . msgId } ) ) . find ( m => m . id === metricsData ! . msgId ) ;
207-
208- const recreateMsg = async ( ) => {
209- try {
210- await metricsMsg ?. delete ( ) ;
211- }
212- catch {
213- console . warn ( "Couldn't delete metrics message, creating a new one..." ) ;
214- }
215- metricsMsg = await metricsChan ?. send ( await useMetricsMsg ( latestMetrics ) ) ;
216- metricsData ! . msgId = metricsMsg ?. id ;
217- } ;
218-
219- try {
220- if ( ! metricsMsg )
221- recreateMsg ( ) ;
222- else
223- await metricsMsg ?. edit ( await useMetricsMsg ( latestMetrics ) ) ;
224- }
225- catch {
226- recreateMsg ( ) ;
227- }
228- finally {
229- try {
230- await writeFile ( metricsManifFile , JSON . stringify ( metricsData ) ) ;
231- }
232- catch ( e ) {
233- console . error ( "Couldn't write metrics manifest:" , e ) ;
234- }
235- }
236- }
237- else if ( ! metricsData . msgId || metricsData . msgId . length === 0 ) {
238- metricsMsg = await metricsChan ?. send ( await useMetricsMsg ( latestMetrics ) ) ;
239- metricsData . msgId = metricsMsg ?. id ;
240- await writeFile ( metricsManifFile , JSON . stringify ( metricsData ) ) ;
241- }
242-
243- firstMetricRun = false ;
244- }
245- }
246- catch ( e ) {
247- console . error ( "Couldn't update metrics:" , e ) ;
248- }
249- }
250-
251- //#region m:metrEmbed
252-
253- /** Get the metrics / stats embed and buttons */
254- async function useMetricsMsg ( metrics : MetricsData ) {
255- const {
256- uptimeStr, usersAmt,
257- guildsAmt, totalMembersAmt,
258- uniqueMembersAmt, slashCmdAmt,
259- ctxCmdAmt,
260- } = metrics ;
261- const cmdsTotal = slashCmdAmt + ctxCmdAmt ;
262-
263- const ebd = new EmbedBuilder ( )
264- . setTitle ( "Bot metrics:" )
265- . setFields ( [
266- { name : "Uptime:" , value : String ( uptimeStr ) , inline : false } ,
267- { name : "Users:" , value : String ( usersAmt ) , inline : true } ,
268- { name : "Guilds:" , value : String ( guildsAmt ) , inline : true } ,
269- { name : "Members:" , value : `${ totalMembersAmt } total\n${ uniqueMembersAmt } unique` , inline : true } ,
270- { name : `${ autoPlural ( "Command" , cmdsTotal ) } (${ cmdsTotal } ):` , value : `${ slashCmdAmt } ${ autoPlural ( "slash command" , slashCmdAmt ) } \n${ ctxCmdAmt } ${ autoPlural ( "context command" , ctxCmdAmt ) } ` , inline : false } ,
271- ] )
272- . setFooter ( { text : `v${ pkg . version } - ${ await getCommitHash ( true ) } ` } )
273- . setColor ( Col . Info ) ;
274-
275- return {
276- embeds : [ ebd ] ,
277- components : [
278- new ActionRowBuilder ( )
279- . addComponents (
280- new ButtonBuilder ( )
281- . setStyle ( ButtonStyle . Link )
282- . setLabel ( "Open repo at commit" )
283- . setURL ( `${ ghBaseUrl } /tree/${ await getCommitHash ( ) } ` )
284- )
285- . toJSON ( ) ,
286- ] ,
287- } as Pick < MessageCreateOptions , "embeds" | "components" > ;
288- }
289-
290- /** Returns the uptime in a human-readable format */
291- function getUptime ( ) {
292- const upt = Date . now ( ) - initTime ;
293-
294- return ( [
295- [ ( 1000 * 60 * 60 * 24 ) , `${ Math . floor ( upt / ( 1000 * 60 * 60 * 24 ) ) } d` ] ,
296- [ ( 1000 * 60 * 60 ) , `${ Math . floor ( upt / ( 1000 * 60 * 60 ) ) % 24 } h` ] ,
297- [ ( 1000 * 60 ) , `${ Math . floor ( upt / ( 1000 * 60 ) ) % 60 } m` ] ,
298- [ 0 , `${ Math . floor ( upt / 1000 ) % 60 } s` ] ,
299- ] as const )
300- . filter ( ( [ d ] ) => upt >= d )
301- . map ( ( [ , s ] ) => s )
302- . join ( " " ) ;
303- }
304-
305111init ( ) ;
0 commit comments