Skip to content

Commit 58f3804

Browse files
committed
Add stats command
1 parent c60e1e1 commit 58f3804

File tree

3 files changed

+94
-86
lines changed

3 files changed

+94
-86
lines changed

src/app.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { KARMA_REGEX } from './commands/karma';
88
import { getConfig } from './config';
99
import { createHttpServer } from './http-server';
1010
import { InvalidUsageError } from './types';
11+
import { getWeekNumber } from './utils';
12+
import { getStatsCollection, initDb } from './db';
1113

1214
const MESSAGE_COLLECTOR_CACHE_S = 60 * 60;
1315
const messageCollectorCache = new Cache({ stdTTL: MESSAGE_COLLECTOR_CACHE_S });
@@ -113,6 +115,8 @@ client.on('message', async (msg) => {
113115
}
114116
}
115117

118+
void updateMessagesCount(msg.member?.id, msg.member?.displayName).catch(console.error);
119+
116120
if (/thx|thank|dzięki|dziękuję|dzieki|dziekuje/i.test(msg.content)) {
117121
if (
118122
(thxTimeoutCache.get<Date>(msg.channel.id)?.getTime() ?? 0) <
@@ -126,6 +130,33 @@ client.on('message', async (msg) => {
126130
return;
127131
});
128132

133+
async function updateMessagesCount(
134+
memberId: Discord.GuildMember['id'] | undefined,
135+
displayName: Discord.GuildMember['displayName'] | undefined,
136+
) {
137+
const db = await initDb();
138+
const statsCollection = getStatsCollection(db);
139+
const [year, week] = getWeekNumber(new Date());
140+
const yearWeek = `${year}-${week}`;
141+
142+
await statsCollection.updateOne(
143+
{
144+
memberId,
145+
yearWeek,
146+
},
147+
{
148+
$set: {
149+
memberId,
150+
memberName: displayName,
151+
updatedAt: new Date(),
152+
yearWeek,
153+
},
154+
$inc: { messagesCount: 1 },
155+
},
156+
{ upsert: true },
157+
);
158+
}
159+
129160
function revertCommand(msg: Discord.Message) {
130161
if (!messageCollectorCache.has(msg.id) || msg.channel.type === 'dm') {
131162
return undefined;

src/commands/stats.ts

Lines changed: 48 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { Db } from 'mongodb';
22

3-
import type { StatsCollection } from '../db';
43
import { getStatsCollection, initDb } from '../db';
54
import type { Command } from '../types';
65
import { getDateForWeekNumber, getWeekNumber } from '../utils';
@@ -20,59 +19,36 @@ const stats: Command = {
2019
async execute(msg) {
2120
const db = await initDb();
2221

23-
const { totalStats, statsChangeThisWeek, year1, year2, week1, week2 } =
24-
await getStatsChangeThisWeek(db);
25-
26-
const d1 = formatDate(getDateForWeekNumber(year2, week2 + 1));
27-
const d2 = formatDate(getDateForWeekNumber(year1, week1 + 1));
28-
29-
const ids = [
30-
...new Set([
31-
...totalStats.map(({ memberId }) => memberId),
32-
...statsChangeThisWeek.map(({ memberId }) => memberId),
33-
]),
34-
];
22+
const { totalStats, statsThisWeek, year1, year2, week1, week2 } = await getStatsChangeThisWeek(
23+
db,
24+
);
3525

36-
await Promise.allSettled(ids.map((memberId) => msg.guild?.members.fetch(memberId)));
26+
const d1 = formatDate(getDateForWeekNumber(year2, week2));
27+
const d2 = formatDate(getDateForWeekNumber(year1, week1));
3728

3829
return msg.channel.send(
3930
[
40-
format(
41-
`Najbardziej aktywne w tym tygodniu (${d1}${d2}):`,
42-
statsChangeThisWeek.map((data) => ({
43-
...data,
44-
displayName: msg.guild?.members.cache.get(data.memberId)?.displayName,
45-
})),
46-
),
31+
format(`Najbardziej aktywne osoby w tym tygodniu (${d1}${d2}):`, statsThisWeek),
4732
'\n',
48-
format(
49-
'Najbardziej aktywne osoby od początku istnienia serwera:',
50-
totalStats.map((data) => ({
51-
...data,
52-
displayName: msg.guild?.members.cache.get(data.memberId)?.displayName,
53-
})),
54-
),
33+
format('Najbardziej aktywne osoby od początku istnienia serwera:', totalStats),
5534
].join('\n'),
5635
);
5736
},
5837
};
5938

6039
export default stats;
6140

62-
type Stats = {
63-
readonly memberId: string;
64-
readonly messagesCount: number;
65-
readonly displayName?: string;
66-
};
67-
68-
function format(title: string, stats: readonly Stats[]) {
41+
function format(
42+
title: string,
43+
stats: readonly { messagesCount?: number | null; memberName?: string | null }[],
44+
) {
6945
const messages = [
7046
`**${title}**`,
7147
...stats
72-
.slice(0, 10)
48+
.filter((d) => d.messagesCount)
7349
.map(
74-
({ messagesCount, displayName }, index) =>
75-
`\`${(index + 1).toString().padStart(2, ' ')}\`. ${displayName ?? ''}${
50+
({ messagesCount, memberName }, index) =>
51+
`\`${(index + 1).toString().padStart(2, ' ')}\`. ${memberName ?? ''}${
7652
messagesCount ?? 0
7753
}`,
7854
),
@@ -89,56 +65,50 @@ async function getStatsChangeThisWeek(db: Db) {
8965

9066
now.setDate(now.getDate() - 7);
9167
const [year2, week2] = getWeekNumber(now);
92-
const previousWeek = `${year2}-${week2}`;
93-
94-
const [totalStats, statsThisWeek, statsPreviousWeek] = await Promise.all([
95-
statsCollection
96-
.find(
97-
{
98-
yearWeek: null,
99-
},
100-
{ sort: { messagesCount: 'desc' } },
101-
)
102-
.toArray(),
103-
104-
statsCollection.find({ yearWeek: thisWeek }).toArray(),
105-
106-
statsCollection.find({ yearWeek: previousWeek }).toArray(),
107-
]);
108-
109-
const totalStatsById = resultToMessagesByMemberId(totalStats);
110-
const statsPreviousWeekById = resultToMessagesByMemberId(statsPreviousWeek);
11168

112-
const statsChangeThisWeek = statsThisWeek
113-
.map(({ memberId, messagesCount }) => {
114-
if (!messagesCount) {
115-
return null;
116-
}
69+
const statsThisWeekPromise = statsCollection
70+
.find({ yearWeek: thisWeek })
71+
.sort({ messagesCount: -1 })
72+
.limit(10)
73+
.toArray();
11774

118-
const messagesCountDifference =
119-
messagesCount - (statsPreviousWeekById[memberId] ?? totalStatsById[memberId] ?? 0);
75+
// @todo should aggregate and sum
76+
const totalStatsPromise = getStats(db);
12077

121-
return { memberId, messagesCount: messagesCountDifference };
122-
})
123-
.filter((obj): obj is Stats => !!obj)
124-
.sort((a, b) => b.messagesCount - a.messagesCount);
78+
const [statsThisWeek, totalStats] = await Promise.all([statsThisWeekPromise, totalStatsPromise]);
12579

12680
return {
127-
statsChangeThisWeek: statsChangeThisWeek.slice(0, 10),
128-
totalStats: totalStats.slice(0, 10) as readonly Stats[],
81+
statsThisWeek,
82+
totalStats,
12983
year1,
13084
year2,
13185
week1,
13286
week2,
13387
};
13488
}
13589

136-
function resultToMessagesByMemberId(stats: readonly StatsCollection[]) {
137-
return stats.reduce<Record<StatsCollection['memberId'], StatsCollection['messagesCount']>>(
138-
(acc, { memberId, messagesCount }) => {
139-
acc[memberId] = messagesCount;
140-
return acc;
90+
export type StatsAgg = {
91+
readonly _id: string;
92+
readonly messagesCount: number;
93+
readonly memberName: string;
94+
};
95+
96+
const statsAggregation = [
97+
{
98+
$group: {
99+
_id: '$memberId',
100+
messagesCount: { $sum: '$messagesCount' },
101+
memberName: { $push: '$memberName' },
141102
},
142-
{},
143-
);
144-
}
103+
},
104+
{ $sort: { messagesCount: -1 } },
105+
{ $limit: 10 },
106+
{ $addFields: { memberName: { $arrayElemAt: [{ $reverseArray: '$memberName' }, 0] } } },
107+
];
108+
109+
export const getStats = async (db: Db) => {
110+
const statsCollection = getStatsCollection(db);
111+
112+
const agg = await statsCollection.aggregate<StatsAgg>(statsAggregation).toArray();
113+
return agg;
114+
};

src/db.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@ import { MongoClient } from 'mongodb';
33

44
import { getConfig } from './config';
55

6-
export const initDb = async () => {
7-
const mongoUrl = getConfig('MONGO_URL');
8-
const dbName = mongoUrl.split('/').pop();
9-
const mongoClient = new MongoClient(mongoUrl);
10-
await mongoClient.connect();
11-
const db = mongoClient.db(dbName);
12-
return db;
13-
};
6+
export const initDb = (() => {
7+
let mongoClient: MongoClient | undefined = undefined;
8+
9+
return async () => {
10+
const mongoUrl = getConfig('MONGO_URL');
11+
const dbName = mongoUrl.split('/').pop();
12+
if (!mongoClient) {
13+
const m = new MongoClient(mongoUrl, { keepAlive: true });
14+
mongoClient = await m.connect();
15+
}
16+
17+
const db = mongoClient.db(dbName);
18+
return db;
19+
};
20+
})();
1421

1522
export type StatsCollection = {
1623
readonly memberId: string;

0 commit comments

Comments
 (0)