goat
command
count
No description
0
views
0
likes
0
installs
raw source
module.exports = {
config: {
name: "count",
version: "2.8",
author: "ARIYAN",
countDown: 10,
role: 0,
description: {
vi: "Xem bảng xếp hạng tin nhắn dưới dạng ảnh (từ lúc bot vào nhóm).",
en: "View the message count leaderboard as an image (since the bot joined the group)."
},
category: "box chat",
guide: {
vi: " {pn}: Xem thẻ hoạt động của bạn."
+ "\n {pn} @tag: Xem thẻ hoạt động của người được tag."
+ "\n {pn} all: Xem bảng xếp hạng của tất cả thành viên.",
en: " {pn}: View your activity card."
+ "\n {pn} [reply]: View activity card of the replied user."
+ "\n {pn} @tag: View the activity card of tagged users."
+ "\n {pn} all: View the leaderboard of all members."
},
envConfig: {
"ACCESS_TOKEN": "6628568379%7Cc1e620fa708a1d5696fb991c1bde5662"
}
},
langs: {
vi: {
invalidPage: "Số trang không hợp lệ.",
leaderboardTitle: "",
userCardTitle: "",
page: "Trang %1/%2",
reply: "Phản hồi tin nhắn này kèm số trang để xem tiếp.",
totalMessages: "Tổng Tin Nhắn",
serverRank: "Hạng Server",
dailyActivity: "Hoạt Động 7 Ngày Qua",
messageBreakdown: "Phân Tích Tin Nhắn",
busiestDay: "NGÀY BẬN RỘN NHẤT",
text: "Văn Bản",
sticker: "Nhãn Dán",
media: "Tệp",
fallbackName: "Người dùng Facebook"
},
en: {
invalidPage: "Invalid page number.",
leaderboardTitle: "",
userCardTitle: "",
page: "Page %1/%2",
reply: "Reply to this message with a page number to see more.",
totalMessages: "Total Messages",
serverRank: "Server Rank",
dailyActivity: "Last 7 Days Activity",
messageBreakdown: "Message Breakdown",
busiestDay: "BUSIEST DAY",
text: "Text",
sticker: "Sticker",
media: "Media",
fallbackName: "Facebook User"
}
},
onLoad: async function () {
// No font registration needed to avoid "Can't find text" errors
},
onChat: async function ({ event, threadsData, usersData }) {
const { threadID, senderID } = event;
const { resolve } = require("path");
const { readJsonSync, writeJsonSync, ensureFileSync } = require("fs-extra");
const moment = require("moment-timezone");
try {
const members = await threadsData.get(threadID, "members");
const findMember = members.find(user => user.userID == senderID);
if (!findMember) {
members.push({
userID: senderID,
name: await usersData.getName(senderID),
nickname: null,
inGroup: true,
count: 1
});
} else {
findMember.count = (findMember.count || 0) + 1;
}
await threadsData.set(threadID, members, "members");
} catch (err) {
console.error("Failed to update compatible count data:", err);
}
const dataPath = resolve(__dirname, "cache", "count_activity.json");
ensureFileSync(dataPath);
let activityData = {};
try {
activityData = readJsonSync(dataPath);
} catch { }
if (!activityData[threadID]) activityData[threadID] = {};
if (!activityData[threadID][senderID]) {
activityData[threadID][senderID] = {
total: 0,
types: { text: 0, sticker: 0, media: 0 },
daily: {}
};
}
const user = activityData[threadID][senderID];
const today = moment().tz("Asia/Ho_Chi_Minh").format("YYYY-MM-DD");
user.total = (user.total || 0) + 1;
user.daily[today] = (user.daily[today] || 0) + 1;
if (event.attachments.some(att => att.type === 'sticker')) {
user.types.sticker = (user.types.sticker || 0) + 1;
} else if (event.attachments.length > 0) {
user.types.media = (user.types.media || 0) + 1;
} else {
user.types.text = (user.types.text || 0) + 1;
}
const sortedDays = Object.keys(user.daily).sort((a, b) => new Date(b) - new Date(a));
if (sortedDays.length > 7) {
for (let i = 7; i < sortedDays.length; i++) delete user.daily[sortedDays[i]];
}
writeJsonSync(dataPath, activityData, { spaces: 2 });
},
onStart: async function ({ args, threadsData, message, event, api, getLang, envCommands }) {
const { Canvas, loadImage } = require("canvas");
const { resolve } = require("path");
const { createWriteStream, readJsonSync, ensureFileSync } = require("fs-extra");
const axios = require("axios");
const moment = require("moment-timezone");
const { threadID, senderID, mentions, type, messageReply } = event;
const ACCESS_TOKEN = envCommands.count.ACCESS_TOKEN;
const BACKGROUND_URL = "https://i.imgur.com/FEV6Olx.jpeg.jpeg";
const threadData = await threadsData.get(threadID);
const dataPath = resolve(__dirname, "cache", "count_activity.json");
ensureFileSync(dataPath);
let activityData = {};
try {
activityData = readJsonSync(dataPath)[threadID] || {};
} catch { }
const usersInGroup = (await api.getThreadInfo(threadID)).participantIDs;
let combinedData = [];
for (const user of threadData.members) {
if (!usersInGroup.includes(user.userID)) continue;
const activity = activityData[user.userID] || {
total: user.count || 0,
types: { text: 0, sticker: 0, media: 0 },
daily: {}
};
combinedData.push({
uid: user.userID,
name: user.name || getLang("fallbackName"),
count: user.count || 0,
activity
});
}
combinedData.sort((a, b) => b.count - a.count);
combinedData.forEach((user, index) => user.rank = index + 1);
const themes = [
{ primary: '#FF4500', secondary: '#8B949E' },
{ primary: '#00FFFF', secondary: '#8B949E' },
{ primary: '#F8F32B', secondary: '#8B949E' },
{ primary: '#FF00FF', secondary: '#8B949E' },
{ primary: '#00FF00', secondary: '#8B949E' }
];
const theme = themes[Math.floor(Math.random() * themes.length)];
let customBackground;
try {
customBackground = await loadImage(BACKGROUND_URL);
} catch (err) {
console.error("Failed to load custom background.", err);
}
const getAvatar = async (uid, name) => {
try {
const url = `https://graph.facebook.com/${uid}/picture?width=512&height=512&access_token=${ACCESS_TOKEN}`;
const response = await axios.get(url, { responseType: 'arraybuffer' });
return await loadImage(response.data);
} catch (error) {
const canvas = new Canvas(512, 512);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#2196f3';
ctx.fillRect(0, 0, 512, 512);
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 256px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(name.charAt(0).toUpperCase(), 256, 256);
return await loadImage(canvas.toBuffer());
}
};
// Fixed Drawing Function to use System Fonts
const drawGlowingText = (ctx, text, x, y, color, size, blur = 15) => {
if (!text || text === "") return;
ctx.font = `bold ${size}px sans-serif`;
ctx.shadowColor = color;
ctx.shadowBlur = blur;
ctx.fillStyle = color;
ctx.fillText(text, x, y);
ctx.shadowBlur = 0;
};
const fitText = (ctx, text, maxWidth) => {
let currentText = text;
if (ctx.measureText(currentText).width > maxWidth) {
while (ctx.measureText(currentText + '...').width > maxWidth) {
currentText = currentText.slice(0, -1);
}
return currentText + '...';
}
return currentText;
};
const drawCircularAvatar = (ctx, avatar, x, y, radius) => {
ctx.save();
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.clip();
ctx.drawImage(avatar, x - radius, y - radius, radius * 2, radius * 2);
ctx.restore();
};
if (args[0]?.toLowerCase() === 'all') {
const usersPerPage = 10;
const leaderboardUsers = combinedData.filter(u => u.rank > 3);
const totalPages = Math.ceil(leaderboardUsers.length / usersPerPage) || 1;
let page = parseInt(args[1]) || 1;
if (page < 1 || page > totalPages) page = 1;
const startIndex = (page - 1) * usersPerPage;
const pageUsers = leaderboardUsers.slice(startIndex, startIndex + usersPerPage);
const canvas = new Canvas(1200, 1700);
const ctx = canvas.getContext('2d');
if (customBackground) {
ctx.drawImage(customBackground, 0, 0, 1200, 1700);
} else {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, 1200, 1700);
}
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
ctx.fillRect(0, 0, 1200, 1700);
const top3 = combinedData.slice(0, 3);
const podColors = ['#FFD700', '#C0C0C0', '#CD7F32'];
const podPositions = [ { x: 600, y: 300, r: 100 }, { x: 250, y: 320, r: 80 }, { x: 950, y: 320, r: 80 } ];
const rankOrder = [1, 0, 2];
for(const i of rankOrder) {
const user = top3[i];
if (!user) continue;
const pos = podPositions[i];
ctx.strokeStyle = podColors[i];
ctx.lineWidth = 5;
ctx.shadowColor = podColors[i];
ctx.shadowBlur = 20;
ctx.beginPath();
ctx.arc(pos.x, pos.y, pos.r + 5, 0, Math.PI * 2);
ctx.stroke();
ctx.shadowBlur = 0;
const avatar = await getAvatar(user.uid, user.name);
drawCircularAvatar(ctx, avatar, pos.x, pos.y, pos.r);
ctx.textAlign = 'center';
ctx.font = `bold ${pos.r * 0.3}px sans-serif`;
ctx.fillStyle = '#FFFFFF';
ctx.fillText(fitText(ctx, user.name, pos.r * 2.2), pos.x, pos.y + pos.r + 40);
ctx.font = `normal ${pos.r * 0.25}px sans-serif`;
ctx.fillStyle = theme.secondary;
ctx.fillText(`${user.count} msgs`, pos.x, pos.y + pos.r + 75);
ctx.fillStyle = podColors[i];
ctx.beginPath();
ctx.arc(pos.x, pos.y - pos.r + 10, 25, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000000';
ctx.font = `bold 30px sans-serif`;
ctx.fillText(`#${user.rank}`, pos.x, pos.y - pos.r + 20);
}
let currentY = 550;
for (const user of pageUsers) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(50, currentY, 1100, 90);
ctx.textAlign = 'left';
ctx.font = `bold 30px sans-serif`;
ctx.fillStyle = theme.secondary;
ctx.fillText(`#${user.rank}`, 60, currentY + 58);
const avatar = await getAvatar(user.uid, user.name);
drawCircularAvatar(ctx, avatar, 160, currentY + 45, 30);
ctx.fillStyle = '#FFFFFF';
ctx.fillText(fitText(ctx, user.name, 350), 210, currentY + 58);
const barWidth = 350;
const barX = 700;
const progress = (user.count / (top3[0]?.count || user.count)) * barWidth;
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
ctx.fillRect(barX, currentY + 35, barWidth, 20);
ctx.fillStyle = theme.primary;
ctx.fillRect(barX, currentY + 35, progress, 20);
ctx.textAlign = 'right';
ctx.font = `bold 30px sans-serif`;
ctx.fillStyle = theme.primary;
ctx.fillText(user.count, 1140, currentY + 58);
currentY += 105;
}
ctx.textAlign = 'center';
ctx.fillStyle = theme.secondary;
ctx.font = `normal 24px sans-serif`;
ctx.fillText(getLang("page", page, totalPages), 600, 1630);
ctx.fillText(getLang("reply"), 600, 1660);
const path = resolve(__dirname, 'cache', `leaderboard_${threadID}.png`);
const out = createWriteStream(path);
const stream = canvas.createPNGStream();
stream.pipe(out);
out.on('finish', () => {
message.reply({
attachment: require('fs').createReadStream(path)
}, (err, info) => {
if (err) return console.error(err);
global.GoatBot.onReply.set(info.messageID, {
commandName: this.config.name,
messageID: info.messageID,
author: senderID,
threadID: threadID,
type: 'leaderboard'
});
});
});
}
else {
let targetUsers = [];
if (type === "message_reply") {
targetUsers = [messageReply.senderID];
} else if (Object.keys(mentions).length > 0) {
targetUsers = Object.keys(mentions);
} else {
targetUsers = [senderID];
}
for(const uid of targetUsers) {
const user = combinedData.find(u => u.uid == uid);
if (!user) continue;
const canvas = new Canvas(800, 1200);
const ctx = canvas.getContext('2d');
if (customBackground) {
ctx.drawImage(customBackground, 0, 0, 800, 1200);
} else {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, 800, 1200);
}
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
ctx.fillRect(0, 0, 800, 1200);
ctx.shadowColor = theme.primary;
ctx.shadowBlur = 30;
const avatar = await getAvatar(user.uid, user.name);
drawCircularAvatar(ctx, avatar, 400, 200, 100);
ctx.shadowBlur = 0;
ctx.textAlign = 'center';
ctx.font = `bold 40px sans-serif`;
ctx.fillStyle = '#FFFFFF';
ctx.fillText(fitText(ctx, user.name, 600), 400, 340);
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(50, 400, 700, 120);
ctx.beginPath();
ctx.moveTo(400, 415);
ctx.lineTo(400, 505);
ctx.strokeStyle = theme.primary;
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = theme.secondary;
ctx.font = `bold 24px sans-serif`;
ctx.fillText(getLang("serverRank"), 225, 440);
ctx.fillText(getLang("totalMessages"), 575, 440);
ctx.fillStyle = theme.primary;
ctx.font = `bold 48px sans-serif`;
ctx.fillText(`#${user.rank}`, 225, 490);
ctx.fillText(user.count, 575, 490);
const dailyData = user.activity.daily;
const days = [];
for(let i=6; i>=0; i--) {
const day = moment().tz("Asia/Ho_Chi_Minh").subtract(i, 'days');
days.push({
label: day.format('dddd'),
shortLabel: day.format('ddd'),
key: day.format('YYYY-MM-DD'),
count: dailyData[day.format('YYYY-MM-DD')] || 0
});
}
const busiestDay = days.reduce((prev, current) => (prev.count > current.count) ? prev : current, {count: -1});
ctx.textAlign = 'center';
ctx.fillStyle = theme.secondary;
ctx.font = `bold 24px sans-serif`;
ctx.fillText(getLang("busiestDay"), 400, 580);
ctx.fillStyle = '#FFFFFF';
ctx.font = `bold 32px sans-serif`;
ctx.fillText(busiestDay.count > 0 ? `${busiestDay.label} - ${busiestDay.count} msgs` : 'N/A', 400, 625);
ctx.textAlign = 'left';
ctx.fillStyle = theme.secondary;
ctx.font = `bold 24px sans-serif`;
ctx.fillText(getLang("dailyActivity"), 50, 700);
const graphX = 80, graphW = 640, graphH = 120;
const graphY = 850;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
ctx.lineWidth = 1;
ctx.strokeRect(graphX, graphY - graphH, graphW, graphH);
const maxCount = Math.max(...days.map(d => d.count), 1);
ctx.beginPath();
ctx.moveTo(graphX, graphY - (days[0].count / maxCount * graphH));
ctx.strokeStyle = theme.primary;
ctx.lineWidth = 3;
days.forEach((day, i) => {
const x = graphX + (i / 6) * graphW;
const y = graphY - (day.count / maxCount * graphH);
ctx.lineTo(x, y);
ctx.textAlign = 'center';
ctx.fillStyle = theme.secondary;
ctx.font = '18px sans-serif';
ctx.fillText(day.shortLabel, x, graphY + 25);
});
ctx.stroke();
ctx.textAlign = 'left';
ctx.fillStyle = theme.secondary;
ctx.font = `bold 24px sans-serif`;
ctx.fillText(getLang("messageBreakdown"), 50, 920);
const types = user.activity.types;
const totalTypes = types.text + types.sticker + types.media;
const breakdownData = [
{ label: getLang("text"), value: types.text, color: theme.primary },
{ label: getLang("sticker"), value: types.sticker, color: '#3FBAC2' },
{ label: getLang("media"), value: types.media, color: '#F4E409' }
];
const donutY = 1025;
const donutR = 60;
const donutX = 200;
let startAngle = -0.5 * Math.PI;
if(totalTypes > 0) {
breakdownData.forEach(item => {
const sliceAngle = (item.value / totalTypes) * 2 * Math.PI;
ctx.beginPath();
ctx.moveTo(donutX, donutY);
ctx.arc(donutX, donutY, donutR, startAngle, startAngle + sliceAngle);
ctx.closePath();
ctx.fillStyle = item.color;
ctx.fill();
startAngle += sliceAngle;
});
} else {
ctx.beginPath();
ctx.arc(donutX, donutY, donutR, 0, 2 * Math.PI);
ctx.fillStyle = theme.secondary;
ctx.fill();
}
let legendY = 980;
breakdownData.forEach(item => {
const percentage = totalTypes > 0 ? (item.value / totalTypes * 100).toFixed(1) : 0;
ctx.fillStyle = item.color;
ctx.fillRect(350, legendY, 20, 20);
ctx.fillStyle = '#FFFFFF';
ctx.font = `bold 22px sans-serif`;
ctx.fillText(item.label, 380, legendY + 16);
ctx.fillStyle = theme.secondary;
ctx.textAlign = 'right';
ctx.fillText(`${percentage}% (${item.value})`, 750, legendY + 16);
ctx.textAlign = 'left';
legendY += 45;
});
const path = resolve(__dirname, 'cache', `usercard_${uid}.png`);
const out = createWriteStream(path);
const stream = canvas.createPNGStream();
stream.pipe(out);
out.on('finish', () => {
message.reply({ attachment: require('fs').createReadStream(path) });
});
}
}
},
onReply: async function ({ event, Reply, message, getLang }) {
if (event.senderID !== Reply.author || Reply.type !== 'leaderboard') return;
const page = parseInt(event.body);
if (isNaN(page)) return;
try {
message.unsend(Reply.messageID);
const newArgs = ['all', page.toString()];
await this.onStart({
...arguments[0],
args: newArgs,
event: { ...arguments[0].event, body: `/count ${newArgs.join(' ')}` }
});
} catch (e) {
console.error("Error during pagination reply:", e);
message.reply(getLang("invalidPage"));
}
}
};