Skip to content

Commit f0b3433

Browse files
committed
ref: move metrics into own file & add env var condition
1 parent 6bffdcb commit f0b3433

File tree

4 files changed

+226
-211
lines changed

4 files changed

+226
-211
lines changed

.env.template

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ MAX_AUTOCOMPLETE_RESULTS=25
2323
# If set, a metrics message will be sent to this guild and channel - some metadata is persisted through the file .metrics.json
2424
METRICS_GUILD=
2525
METRICS_CHANNEL=
26-
# After how many seconds on interval the metrics message will be updated - 30 is the default
26+
# After how many seconds on interval the metrics message will be updated - default is 30 - minimum is 1
2727
METRICS_UPDATE_INTERVAL=30
2828

29+
# After how many seconds on interval all registered guilds should be verified - default is 300 - minimum is 10
30+
GUILD_CHECK_INTERVAL=300
31+
2932
# Can be either development or production
3033
NODE_ENV="development"
3134

src/lib/text.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export function truncField(content: string, endStr = "...") {
1919

2020
/**
2121
* Automatically appends an `s` to the passed `word`, if `num` is not equal to 1.
22-
* This doesn't work for all words, but it's a simple and dynamic way to handle most cases.
22+
* This doesn't work for all words, but it's a simple and dynamic way to handle most cases.
23+
* ⚠️ Only use this for debugging since it's not translation-friendly
2324
* @param word A word in singular form, to auto-convert to plural
2425
* @param num If this is an array, d.js Collection or Map, the amount of items is used
2526
*/

src/main.ts

Lines changed: 15 additions & 209 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
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";
42
import k from "kleur";
53
import "dotenv/config";
64
import { client, botToken } from "@lib/client.ts";
75
import { em, initDatabase } from "@lib/db.ts";
8-
import { cmdInstances, initRegistry, registerCommandsForGuild } from "@lib/registry.ts";
6+
import { initRegistry, registerCommandsForGuild } from "@lib/registry.ts";
97
import { autoPlural } from "@lib/text.ts";
108
import { envVarEq, getEnvVar } from "@lib/env.ts";
119
import { initTranslations } from "@lib/translate.ts";
12-
import { getCommitHash, getHash, ghBaseUrl } from "@lib/misc.ts";
13-
import { Col } from "@lib/embedify.ts";
1410
import { 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 */
9871
async 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-
305111
init();

0 commit comments

Comments
 (0)