Skip to content

Commit 88be096

Browse files
committed
Refactor: Notices scrapper and enhance myStats.js. Connected #4
1 parent 4578077 commit 88be096

File tree

3 files changed

+129
-118
lines changed

3 files changed

+129
-118
lines changed

src/bot.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Client, Collection, IntentsBitField, EmbedBuilder, PermissionsBitField, ChannelType, Partials, ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder, Events, MessageFlags } from 'discord.js';
1+
import { Client, Collection, IntentsBitField, EmbedBuilder, PermissionsBitField, ChannelType, Partials, ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder, Events, MessageFlags, Widget } from 'discord.js';
22
import { REST } from '@discordjs/rest';
33
import { Routes } from 'discord-api-types/v9';
44
import dotenv from 'dotenv';

src/commands/slash/myStats.js

Lines changed: 90 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { SlashCommandBuilder, EmbedBuilder, PermissionsBitField } from 'discord.js';
2+
import { promisify } from 'node:util';
23

34
const VERIFIED_ROLE_ID = process.env.VERIFIED_ROLE_ID || 'YOUR_VERIFIED_ROLE_ID_HERE';
45

@@ -12,60 +13,42 @@ export async function execute(interaction) {
1213
return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true });
1314
}
1415

16+
if (interaction.replied || interaction.deferred) {
17+
console.warn(`[myStats] Interaction ${interaction.id} already acknowledged. Skipping.`);
18+
return;
19+
}
20+
21+
await interaction.deferReply();
22+
1523
const userId = interaction.user.id;
1624
const guildId = interaction.guild.id;
1725
const db = interaction.client.db;
18-
const isModerator = interaction.member.permissions.has(PermissionsBitField.Flags.KickMembers || PermissionsBitField.Flags.BanMembers);
19-
const hasVerifiedRole = interaction.member.roles.cache.has(VERIFIED_ROLE_ID);
20-
21-
await interaction.deferReply();
2226

23-
try {
24-
const statsRow = await new Promise((resolve, reject) => {
25-
db.get(`SELECT messages_sent, voice_time_minutes FROM user_stats WHERE user_id = ? AND guild_id = ?`,
26-
[userId, guildId], (err, row) => {
27-
if (err) reject(err);
28-
else resolve(row);
29-
});
30-
});
27+
const dbGet = promisify(db.get).bind(db);
28+
const dbAll = promisify(db.all).bind(db);
3129

32-
const warnRows = await new Promise((resolve, reject) => {
33-
db.all(`SELECT reason, timestamp FROM warnings WHERE userId = ? AND guildId = ? ORDER BY timestamp DESC LIMIT 3`,
34-
[userId, guildId], (err, rows) => { // Fetch last 3 warn reasons
35-
if (err) reject(err);
36-
else resolve(rows);
37-
});
38-
});
39-
const warnCount = warnRows.length;
30+
const isModerator = interaction.member.permissions.has(
31+
PermissionsBitField.Flags.KickMembers || PermissionsBitField.Flags.BanMembers
32+
);
4033

41-
const reputationRow = await new Promise((resolve, reject) => {
42-
db.get(`SELECT reputation_points FROM reputation WHERE user_id = ? AND guild_id = ?`,
43-
[userId, guildId], (err, row) => {
44-
if (err) reject(err);
45-
else resolve(row);
46-
});
47-
});
34+
const hasVerifiedRole = interaction.member.roles.cache.has(VERIFIED_ROLE_ID);
4835

49-
let verifiedUserRow = null;
50-
let birthdayRow = null;
51-
if (hasVerifiedRole) {
52-
verifiedUserRow = await new Promise((resolve, reject) => {
53-
db.get(`SELECT real_name, email FROM verified_users WHERE user_id = ? AND guild_id = ?`,
54-
[userId, guildId], (err, row) => {
55-
if (err) reject(err);
56-
else resolve(row);
57-
});
58-
});
59-
60-
birthdayRow = await new Promise((resolve, reject) => {
61-
db.get(`SELECT month, day, year FROM birthdays WHERE user_id = ? AND guild_id = ?`,
62-
[userId, guildId], (err, row) => {
63-
if (err) reject(err);
64-
else resolve(row);
65-
});
66-
});
67-
}
36+
try {
37+
const statsRow = await dbGet(
38+
`SELECT messages_sent, voice_time_minutes FROM user_stats WHERE user_id = ? AND guild_id = ?`,
39+
[userId, guildId]
40+
);
41+
42+
const warnRows = await dbAll(
43+
`SELECT reason, timestamp FROM warnings WHERE userId = ? AND guildId = ? ORDER BY timestamp DESC LIMIT 3`,
44+
[userId, guildId]
45+
);
46+
const warnCount = warnRows.length;
6847

48+
const reputationRow = await dbGet(
49+
`SELECT reputation_points FROM reputation WHERE user_id = ? AND guild_id = ?`,
50+
[userId, guildId]
51+
);
6952

7053
const messagesSent = statsRow ? statsRow.messages_sent : 0;
7154
const voiceTimeMinutes = statsRow ? statsRow.voice_time_minutes : 0;
@@ -85,64 +68,73 @@ export async function execute(interaction) {
8568
statusColor = '#FF0000';
8669
}
8770

88-
const embed = new EmbedBuilder()
71+
const publicEmbed = new EmbedBuilder()
8972
.setColor(statusColor)
90-
.setTitle(`📊 ${interaction.user.tag}'s Stats`)
91-
.setDescription('Your activity and standing in this server:')
73+
.setTitle(`📊 ${interaction.user.tag}'s Server Stats`)
74+
.setDescription('Here are some general statistics about your activity and standing in this server:')
9275
.setThumbnail(interaction.user.displayAvatarURL({ dynamic: true }))
9376
.setTimestamp()
9477
.addFields(
9578
{ name: 'Messages Sent', value: messagesSent.toLocaleString(), inline: true },
9679
{ name: 'Voice Time', value: `${voiceTimeMinutes} minutes`, inline: true },
9780
{ name: 'Reputation', value: reputationPoints.toLocaleString(), inline: true },
98-
{ name: 'Warn Status', value: `${warnStatus} (${warnCount} warns)`, inline: true }
81+
{ name: 'Warn Status', value: `${warnCount} warns (${warnStatus})`, inline: true }
9982
);
10083

10184
if (warnCount > 0) {
10285
const warnReasons = warnRows.map((warn, index) =>
10386
`${index + 1}. ${warn.reason} (<t:${Math.floor(warn.timestamp / 1000)}:d>)`
10487
).join('\n');
105-
embed.addFields(
88+
publicEmbed.addFields(
10689
{ name: 'Recent Warn Reasons', value: warnReasons }
10790
);
10891
}
10992

110-
if (hasVerifiedRole && verifiedUserRow) {
111-
embed.addFields(
112-
{ name: '\u200b', value: '**Verified Information:**', inline: false }, // Spacer
113-
{ name: 'Full Name', value: verifiedUserRow.real_name, inline: true },
114-
{ name: 'Email', value: verifiedUserRow.email, inline: true }
93+
await interaction.editReply({ embeds: [publicEmbed] });
94+
95+
const privateEmbed = new EmbedBuilder()
96+
.setColor('#0099FF')
97+
.setTitle(`🔒 ${interaction.user.tag}'s Private Stats`)
98+
.setDescription('This message contains sensitive or personal information.')
99+
.setTimestamp();
100+
console.log(VERIFIED_ROLE_ID);
101+
console.log(hasVerifiedRole);
102+
if (hasVerifiedRole) {
103+
const verifiedUserRow = await dbGet(
104+
`SELECT real_name, email FROM verified_users WHERE user_id = ? AND guild_id = ?`,
105+
[userId, guildId]
106+
);
107+
const birthdayRow = await dbGet(
108+
`SELECT month, day, year FROM birthdays WHERE user_id = ? AND guild_id = ?`,
109+
[userId, guildId]
115110
);
116111

117-
if (birthdayRow) {
118-
let dob = `${birthdayRow.month}/${birthdayRow.day}`;
119-
if (birthdayRow.year) {
120-
dob += `/${birthdayRow.year}`;
121-
}
122-
embed.addFields(
123-
{ name: 'Date of Birth', value: dob, inline: true }
112+
if (verifiedUserRow) {
113+
privateEmbed.addFields(
114+
{ name: '\u200b', value: '**Verified Information:**', inline: false },
115+
{ name: 'Full Name', value: verifiedUserRow.real_name, inline: true },
116+
{ name: 'Email', value: verifiedUserRow.email, inline: true }
124117
);
118+
119+
if (birthdayRow) {
120+
let dob = `${birthdayRow.month}/${birthdayRow.day}`;
121+
if (birthdayRow.year) {
122+
dob += `/${birthdayRow.year}`;
123+
}
124+
privateEmbed.addFields(
125+
{ name: 'Date of Birth', value: dob, inline: true }
126+
);
127+
}
125128
}
126129
}
127130

128-
129131
if (isModerator) {
130-
const modActions = await new Promise((resolve, reject) => {
131-
db.all(`SELECT action_type, COUNT(*) as count FROM moderation_actions WHERE moderator_id = ? AND guild_id = ? GROUP BY action_type`,
132-
[userId, guildId], (err, rows) => {
133-
if (err) reject(err);
134-
else resolve(rows);
135-
});
136-
});
137-
138-
const modStats = {
139-
kicks: 0,
140-
bans: 0,
141-
timeouts: 0,
142-
mutes: 0,
143-
deafens: 0,
144-
};
132+
const modActions = await dbAll(
133+
`SELECT action_type, COUNT(*) as count FROM moderation_actions WHERE moderator_id = ? AND guild_id = ? GROUP BY action_type`,
134+
[userId, guildId]
135+
);
145136

137+
const modStats = { kicks: 0, bans: 0, timeouts: 0, mutes: 0, deafens: 0 };
146138
for (const action of modActions) {
147139
switch (action.action_type) {
148140
case 'kick': modStats.kicks = action.count; break;
@@ -153,8 +145,8 @@ export async function execute(interaction) {
153145
}
154146
}
155147

156-
embed.addFields(
157-
{ name: '\u200b', value: '**Moderation Actions Issued:**', inline: false }, // Spacer
148+
privateEmbed.addFields(
149+
{ name: '\u200b', value: '**Moderation Actions Issued:**', inline: false },
158150
{ name: 'Kicks', value: modStats.kicks.toLocaleString(), inline: true },
159151
{ name: 'Bans', value: modStats.bans.toLocaleString(), inline: true },
160152
{ name: 'Timeouts', value: modStats.timeouts.toLocaleString(), inline: true },
@@ -163,10 +155,24 @@ export async function execute(interaction) {
163155
);
164156
}
165157

166-
await interaction.editReply({ embeds: [embed] });
158+
if (privateEmbed.data.fields && privateEmbed.data.fields.length > 0) {
159+
try {
160+
await interaction.user.send({ embeds: [privateEmbed] });
161+
} catch (dmError) {
162+
console.error(`Error sending DM to ${interaction.user.tag}:`, dmError);
163+
await interaction.followUp({
164+
content: 'I could not send your private stats to your DMs. Please check your privacy settings to allow DMs from server members.',
165+
ephemeral: true
166+
});
167+
}
168+
}
167169

168170
} catch (err) {
169171
console.error('Error fetching stats:', err.message);
170-
await interaction.editReply({ embeds: [new EmbedBuilder().setColor('#FF0000').setDescription(`❌ An error occurred while fetching your stats: ${err.message}`)], ephemeral: true });
172+
const replyMethod = interaction.deferred || interaction.replied ? 'followUp' : 'editReply';
173+
await interaction[replyMethod]({
174+
embeds: [new EmbedBuilder().setColor('#FF0000').setDescription(`❌ An error occurred while fetching your stats: ${err.message}`)],
175+
ephemeral: true
176+
});
171177
}
172-
}
178+
}

src/services/scraper.js

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -164,47 +164,52 @@ export async function scrapeIoeExamNotice() {
164164
}
165165

166166
/**
167-
* Scrapes the latest notice from the Pulchowk Campus website.
168-
* @returns {Promise<object|null>} - A single notice object or null on failure.
167+
* Scrapes all recent notices from the Pulchowk Campus website's homepage widget.
168+
* @returns {Promise<Array<object>>} - A list of notice objects.
169169
*/
170170
export async function scrapePcampusNotice() {
171-
const listUrl = "https://pcampus.edu.np/category/general-notices/";
171+
const listUrl = "https://pcampus.edu.np/";
172172
try {
173173
const listData = await fetchWithRetry(listUrl);
174174
const $list = cheerio.load(listData);
175-
const latestArticle = $list("article").first();
176-
const title = latestArticle.find("h2.entry-title a").text().trim();
177-
const pageLink = latestArticle.find("h2.entry-title a").attr("href");
178-
const date = latestArticle.find("time.entry-date").attr("datetime");
179-
const postId = latestArticle.attr("id");
180-
181-
if (!pageLink) {
182-
console.warn("[scrapePcampusNotice] No page link found in latest article.");
183-
return null;
184-
}
185-
186-
const pageData = await fetchWithRetry(pageLink);
187-
const $page = cheerio.load(pageData);
188-
const attachments = [];
189-
190-
$page(".entry-content a").each((_, el) => {
191-
const href = $page(el).attr("href");
192-
if (href?.includes("/wp-content/uploads/")) {
193-
attachments.push(new URL(href, pageLink).href);
175+
const noticeItems = $list("#recent-posts-2 ul li");
176+
if (noticeItems.length === 0) {
177+
console.warn("[scrapePcampusNotice] Could not find any notices in the widget.");
178+
return [];
194179
}
180+
const noticeDetailPromises = [];
181+
noticeItems.each((_, el) => {
182+
const item = $list(el);
183+
const titleElement = item.find("a");
184+
const pageLink = titleElement.attr("href");
185+
const title = titleElement.text().trim();
186+
const date = item.find(".post-date").text().trim();
187+
if (pageLink) {
188+
const detailPromise = (async () => {
189+
try {
190+
const pageData = await fetchWithRetry(pageLink);
191+
const $page = cheerio.load(pageData);
192+
const attachments = [];
193+
$page(".entry-content a").each((_, a) => {
194+
const href = $page(a).attr("href");
195+
if (href?.includes("/wp-content/uploads/")) {
196+
attachments.push(new URL(href, pageLink).href);
197+
}
198+
});
199+
return { title, link: pageLink, attachments: [...new Set(attachments)], date, source: "Pulchowk Campus" };
200+
} catch (err) {
201+
console.error(`[scrapePcampusNotice] Failed to fetch details for ${pageLink}. Error: ${err.message}`);
202+
return null;
203+
}
204+
})();
205+
noticeDetailPromises.push(detailPromise);
206+
}
195207
});
196-
197-
return {
198-
id: postId,
199-
title,
200-
link: pageLink,
201-
attachments: [...new Set(attachments)],
202-
date,
203-
source: "Pulchowk Campus",
204-
};
208+
const results = await Promise.all(noticeDetailPromises);
209+
return results.filter(notice => notice !== null);
205210
} catch (err) {
206211
console.error("[scrapePcampusNotice] Error during scraping or parsing:", err.message);
207-
return null;
212+
return [];
208213
}
209214
}
210215

@@ -228,7 +233,7 @@ export async function scrapeLatestNotice() {
228233
}
229234

230235
if (pcampus.status === 'fulfilled' && pcampus.value) {
231-
combinedNotices = [...combinedNotices, pcampus.value];
236+
combinedNotices = [...combinedNotices, ...pcampus.value];
232237
} else {
233238
console.error("[scrapeLatestNotice] Pulchowk Campus Notice scraping failed:", pcampus.reason);
234239
}

0 commit comments

Comments
 (0)