@zumito-team/analytics-module 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +237 -0
  2. package/commands/AnalyticsConfig.d.ts +12 -0
  3. package/commands/AnalyticsConfig.js +93 -0
  4. package/commands/AnalyticsConfig.ts +118 -0
  5. package/commands/Stats.d.ts +11 -0
  6. package/commands/Stats.js +52 -0
  7. package/commands/Stats.ts +66 -0
  8. package/config.d.ts +11 -0
  9. package/config.js +13 -0
  10. package/config.ts +14 -0
  11. package/events/discord/GuildMemberAdd.d.ts +7 -0
  12. package/events/discord/GuildMemberAdd.js +24 -0
  13. package/events/discord/GuildMemberAdd.ts +23 -0
  14. package/events/discord/GuildMemberRemove.d.ts +7 -0
  15. package/events/discord/GuildMemberRemove.js +24 -0
  16. package/events/discord/GuildMemberRemove.ts +23 -0
  17. package/events/discord/MessageCreate.d.ts +7 -0
  18. package/events/discord/MessageCreate.js +18 -0
  19. package/events/discord/MessageCreate.ts +16 -0
  20. package/events/discord/VoiceStateUpdate.d.ts +7 -0
  21. package/events/discord/VoiceStateUpdate.js +29 -0
  22. package/events/discord/VoiceStateUpdate.ts +31 -0
  23. package/events/framework/CommandExecuted.d.ts +7 -0
  24. package/events/framework/CommandExecuted.js +17 -0
  25. package/events/framework/CommandExecuted.ts +16 -0
  26. package/index.d.ts +15 -0
  27. package/index.js +61 -0
  28. package/index.ts +69 -0
  29. package/models/CommandDailyStats.d.ts +9 -0
  30. package/models/CommandDailyStats.js +34 -0
  31. package/models/CommandDailyStats.ts +25 -0
  32. package/models/GuildAnalyticsConfig.d.ts +12 -0
  33. package/models/GuildAnalyticsConfig.js +43 -0
  34. package/models/GuildAnalyticsConfig.ts +34 -0
  35. package/models/GuildDailyStats.d.ts +11 -0
  36. package/models/GuildDailyStats.js +40 -0
  37. package/models/GuildDailyStats.ts +31 -0
  38. package/models/VoiceChannelDailyStats.d.ts +8 -0
  39. package/models/VoiceChannelDailyStats.js +31 -0
  40. package/models/VoiceChannelDailyStats.ts +22 -0
  41. package/package.json +21 -0
  42. package/routes/AdminAnalytics.d.ts +11 -0
  43. package/routes/AdminAnalytics.js +55 -0
  44. package/routes/AdminAnalytics.ts +69 -0
  45. package/routes/UserPanelAnalytics.d.ts +11 -0
  46. package/routes/UserPanelAnalytics.js +81 -0
  47. package/routes/UserPanelAnalytics.ts +101 -0
  48. package/services/AnalyticsCollector.d.ts +62 -0
  49. package/services/AnalyticsCollector.js +470 -0
  50. package/services/AnalyticsCollector.ts +537 -0
  51. package/translations/en.json +63 -0
  52. package/translations/es.json +63 -0
  53. package/tsconfig.json +18 -0
  54. package/views/admin-analytics.ejs +170 -0
  55. package/views/user-analytics.ejs +209 -0
package/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # Analytics Module
2
+
3
+ **`@zumito-team/analytics-module`** provides configurable server analytics for your Zumito bot. Tracks messages, members, voice activity, and command usage with per-guild granularity. Includes optional web dashboard pages with Chart.js for both the admin and user panels.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @zumito-team/analytics-module
9
+ ```
10
+
11
+ Add to your `zumito.config.ts`:
12
+
13
+ ```ts
14
+ {
15
+ path: path.join(__dirname, "node_modules", "@zumito-team", "analytics-module"),
16
+ }
17
+ ```
18
+
19
+ ## What it provides
20
+
21
+ ### AnalyticsCollector service
22
+
23
+ Available via `ServiceContainer.getService(AnalyticsCollector)`. Other modules can query analytics data.
24
+
25
+ | Method | Description |
26
+ |---|---|
27
+ | `recordMessage(guildId)` | Track a message sent in a guild |
28
+ | `recordMemberJoin(guildId)` | Track a member joining |
29
+ | `recordMemberLeave(guildId)` | Track a member leaving |
30
+ | `recordVoiceJoin(guildId, channelId, userId)` | Track voice channel join |
31
+ | `recordVoiceLeave(guildId, channelId, userId)` | Track voice channel leave (auto-calculates duration) |
32
+ | `recordCommand(payload)` | Track a command execution (success/failure + execution time) |
33
+ | `recordMemberCount(guildId, count)` | Store current member count snapshot |
34
+ | `getGuildStats(guildId, daysBack)` | Get daily stats for a guild |
35
+ | `getGlobalStatsSummary(daysBack)` | Get aggregated global stats |
36
+ | `getGuildGrowth(daysBack)` | Get active guilds per day |
37
+ | `getMessagesPerDay(daysBack)` | Get messages per day (global) |
38
+ | `getCommandsPerDay(guildId, daysBack)` | Get commands per day |
39
+ | `getTopCommands(guildId, daysBack, limit)` | Get most used commands |
40
+ | `getSlowestCommands(guildId, daysBack, limit)` | Get commands sorted by avg execution time |
41
+ | `getVoiceChannelStats(guildId, daysBack)` | Get per-channel voice stats |
42
+ | `getConfig(guildId)` | Get guild analytics configuration |
43
+ | `updateConfig(guildId, partial)` | Update guild analytics configuration |
44
+ | `runCleanup()` | Run data retention cleanup manually |
45
+ | `startCleanupScheduler()` | Start automatic cleanup scheduler |
46
+ | `stopCleanupScheduler()` | Stop automatic cleanup scheduler |
47
+ | `clearVoiceSessions()` | Clear in-memory voice session tracking |
48
+
49
+ ### Events tracked
50
+
51
+ | Event | Source | Description |
52
+ |---|---|---|
53
+ | `messageCreate` | discord | Message count per guild |
54
+ | `guildMemberAdd` | discord | Join count + member snapshot |
55
+ | `guildMemberRemove` | discord | Leave count + member snapshot |
56
+ | `voiceStateUpdate` | discord | Voice activity (minutes) per guild/channel |
57
+ | `commandExecuted` | framework | Command usage + execution time |
58
+
59
+ ### Commands
60
+
61
+ | Command | Type | Description |
62
+ |---|---|---|
63
+ | `/stats` | Slash + Prefix | Shows server stats embed (last 7 days) |
64
+ | `/analytics-config` | Slash + Prefix (admin-only) | View/toggle analytics settings per guild |
65
+
66
+ ### Admin Panel
67
+
68
+ - **`/admin/analytics`** — Global bot statistics: guild growth, messages/day, commands/day, top commands, slowest commands
69
+ - Date range selector: 7 / 30 / 90 days
70
+ - Chart.js visualizations (line, bar, horizontal bar charts)
71
+
72
+ ### User Panel
73
+
74
+ - **`/panel/:guildId/analytics`** — Per-server statistics: messages, joins/leaves, voice activity, commands, top commands
75
+ - If `track_command_performance` enabled: slowest commands chart
76
+ - If `track_per_channel_voice` enabled: per-channel voice breakdown
77
+ - Date range selector: 7 / 30 / 90 days
78
+
79
+ ## Configuration
80
+
81
+ ### Global defaults
82
+
83
+ Import and configure global defaults before the module initializes:
84
+
85
+ ```ts
86
+ import { AnalyticsModuleConfig } from '@zumito-team/analytics-module';
87
+
88
+ AnalyticsModuleConfig.configure({
89
+ defaultRetentionDays: 90,
90
+ cleanupIntervalHours: 24,
91
+ defaultTrackMessages: true,
92
+ defaultTrackVoice: true,
93
+ defaultTrackMembers: true,
94
+ defaultTrackCommands: true,
95
+ defaultTrackCommandPerformance: false,
96
+ defaultTrackPerChannelVoice: false,
97
+ });
98
+ ```
99
+
100
+ | Setting | Default | Description |
101
+ |---|---|---|
102
+ | `defaultRetentionDays` | `90` | Days to keep data before auto-deletion |
103
+ | `cleanupIntervalHours` | `24` | How often cleanup runs |
104
+ | `defaultTrackMessages` | `true` | Track messages by default |
105
+ | `defaultTrackVoice` | `true` | Track voice activity by default |
106
+ | `defaultTrackMembers` | `true` | Track joins/leaves by default |
107
+ | `defaultTrackCommands` | `true` | Track command usage by default |
108
+ | `defaultTrackCommandPerformance` | `false` | Track execution time per command |
109
+ | `defaultTrackPerChannelVoice` | `false` | Track per-channel voice stats |
110
+
111
+ ### Per-guild configuration
112
+
113
+ Use `/analytics-config` or the public API:
114
+
115
+ ```ts
116
+ const collector = ServiceContainer.getService(AnalyticsCollector);
117
+ await collector.updateConfig(guildId, {
118
+ enabled: true,
119
+ track_commands: true,
120
+ track_command_performance: true, // enable execution time tracking
121
+ retention_days: 180, // 6 months for premium
122
+ });
123
+ ```
124
+
125
+ Per-guild DB fields:
126
+
127
+ | Field | Type | Default |
128
+ |---|---|---|
129
+ | `guild_id` | `string` | Primary key |
130
+ | `enabled` | `boolean` | `true` |
131
+ | `track_messages` | `boolean` | `true` |
132
+ | `track_voice` | `boolean` | `true` |
133
+ | `track_members` | `boolean` | `true` |
134
+ | `track_commands` | `boolean` | `true` |
135
+ | `track_command_performance` | `boolean` | `false` |
136
+ | `track_per_channel_voice` | `boolean` | `false` |
137
+ | `retention_days` | `number` | Globally configured default (90) |
138
+ | `public_stats_page` | `boolean` | `false` |
139
+
140
+ ## Extending
141
+
142
+ ### Consuming AnalyticsCollector in other modules
143
+
144
+ ```ts
145
+ import { AnalyticsCollector, type CommandExecutedPayload } from '@zumito-team/analytics-module';
146
+ import { ServiceContainer } from 'zumito-framework';
147
+
148
+ const collector = ServiceContainer.getService(AnalyticsCollector) as AnalyticsCollector;
149
+
150
+ // Query stats
151
+ const stats = await collector.getGuildStats(guildId, 30);
152
+
153
+ // Get top commands
154
+ const top = await collector.getTopCommands(null, 7, 5);
155
+
156
+ // Get global summary
157
+ const summary = await collector.getGlobalStatsSummary(7);
158
+ ```
159
+
160
+ ### Tracking custom events
161
+
162
+ ```ts
163
+ // Track custom event as a message
164
+ await collector.recordMessage(guildId);
165
+
166
+ // Track a custom metric directly via DB
167
+ const db = ServiceContainer.getService(DatabaseManager);
168
+ const repo = db.getRepository(GuildDailyStats);
169
+ await repo.insert({ id: `${guildId}_${today()}`, guild_id: guildId, date: today(), message_count: 1 });
170
+ ```
171
+
172
+ ### Premium integration
173
+
174
+ Set a higher retention for premium guilds:
175
+
176
+ ```ts
177
+ // In your premium module
178
+ await collector.updateConfig(guildId, { retention_days: 180 });
179
+ ```
180
+
181
+ ## Data models
182
+
183
+ ### GuildDailyStats
184
+
185
+ Per-guild daily aggregate: `id` = `{guild_id}_{date}`
186
+
187
+ | Field | Type |
188
+ |---|---|
189
+ | `id` | `string` (PK) |
190
+ | `guild_id` | `string` |
191
+ | `date` | `string` (YYYY-MM-DD) |
192
+ | `message_count` | `number` |
193
+ | `join_count` | `number` |
194
+ | `leave_count` | `number` |
195
+ | `voice_minutes` | `number` |
196
+ | `command_count` | `number` |
197
+ | `member_count` | `number` |
198
+
199
+ ### CommandDailyStats
200
+
201
+ Per-command daily stats: `id` = `{guild_id}_{command_name}_{date}`
202
+
203
+ | Field | Type |
204
+ |---|---|
205
+ | `id` | `string` (PK) |
206
+ | `guild_id` | `string` |
207
+ | `command_name` | `string` |
208
+ | `date` | `string` (YYYY-MM-DD) |
209
+ | `usage_count` | `number` |
210
+ | `total_execution_time_ms` | `number` |
211
+ | `error_count` | `number` |
212
+
213
+ ### VoiceChannelDailyStats
214
+
215
+ Per-channel voice stats: `id` = `{guild_id}_{channel_id}_{date}`
216
+
217
+ | Field | Type |
218
+ |---|---|
219
+ | `id` | `string` (PK) |
220
+ | `guild_id` | `string` |
221
+ | `channel_id` | `string` |
222
+ | `date` | `string` (YYYY-MM-DD) |
223
+ | `total_minutes` | `number` |
224
+ | `unique_users` | `number` |
225
+
226
+ ## Dependencies
227
+
228
+ - `zumito-framework`
229
+ - `ejs` — Template rendering
230
+ - `@zumito-team/admin-module` — Optional admin panel integration
231
+ - `@zumito-team/user-panel-module` — Optional user panel integration
232
+
233
+ ## Related modules
234
+
235
+ - [**Admin Module**](/guides/modules/admin) — Panel where global bot stats appear
236
+ - [**User Panel**](/guides/modules/user-panel) — Panel where per-server stats appear
237
+ - [**Logger**](/guides/modules/logger) — Complementary event logging
@@ -0,0 +1,12 @@
1
+ import { Command, CommandParameters } from 'zumito-framework';
2
+ import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
3
+ export declare class AnalyticsConfig extends Command {
4
+ private collector;
5
+ name: string;
6
+ categories: string[];
7
+ type: string;
8
+ dm: boolean;
9
+ adminOnly: boolean;
10
+ constructor(collector?: AnalyticsCollector);
11
+ execute({ message, interaction, trans, args }: CommandParameters): Promise<void>;
12
+ }
@@ -0,0 +1,93 @@
1
+ import { Command, CommandType, ServiceContainer } from 'zumito-framework';
2
+ import { EmbedBuilder } from 'zumito-framework/discord';
3
+ import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
4
+ export class AnalyticsConfig extends Command {
5
+ constructor(collector = ServiceContainer.getService(AnalyticsCollector)) {
6
+ super();
7
+ this.collector = collector;
8
+ this.name = 'analytics-config';
9
+ this.categories = ['analytics', 'admin'];
10
+ this.type = CommandType.any;
11
+ this.dm = false;
12
+ this.adminOnly = true;
13
+ }
14
+ async execute({ message, interaction, trans, args }) {
15
+ const target = message || interaction;
16
+ if (!target)
17
+ return;
18
+ const guildId = message?.guildId || interaction?.guildId;
19
+ if (!guildId) {
20
+ await target.reply({ content: trans('noGuild') });
21
+ return;
22
+ }
23
+ const config = await this.collector.getConfig(guildId);
24
+ const subcommand = args instanceof Map ? args.get('action') : (typeof args === 'object' && 'action' in args ? args['action'] : undefined);
25
+ if (!subcommand) {
26
+ const embed = new EmbedBuilder()
27
+ .setTitle(trans('configTitle'))
28
+ .setDescription(trans('configDescription'))
29
+ .addFields({ name: trans('enabled'), value: config.enabled ? '✅ Yes' : '❌ No', inline: true }, { name: trans('trackMessages'), value: config.track_messages ? '✅' : '❌', inline: true }, { name: trans('trackVoice'), value: config.track_voice ? '✅' : '❌', inline: true }, { name: trans('trackMembers'), value: config.track_members ? '✅' : '❌', inline: true }, { name: trans('trackCommands'), value: config.track_commands ? '✅' : '❌', inline: true }, { name: trans('trackPerformance'), value: config.track_command_performance ? '✅' : '❌', inline: true }, { name: trans('trackPerChannelVoice'), value: config.track_per_channel_voice ? '✅' : '❌', inline: true }, { name: trans('retentionDays'), value: (config.retention_days || '90').toString(), inline: true }, { name: trans('publicStats'), value: config.public_stats_page ? '✅' : '❌', inline: true })
30
+ .setColor(0x5865F2);
31
+ if (message) {
32
+ await message.reply({ embeds: [embed] });
33
+ }
34
+ else if (interaction) {
35
+ if (interaction.replied || interaction.deferred) {
36
+ await interaction.editReply({ embeds: [embed] });
37
+ }
38
+ else {
39
+ await interaction.reply({ embeds: [embed], ephemeral: true });
40
+ }
41
+ }
42
+ return;
43
+ }
44
+ switch (subcommand) {
45
+ case 'enable':
46
+ await this.collector.updateConfig(guildId, { enabled: true });
47
+ break;
48
+ case 'disable':
49
+ await this.collector.updateConfig(guildId, { enabled: false });
50
+ break;
51
+ case 'messages':
52
+ await this.collector.updateConfig(guildId, { track_messages: !config.track_messages });
53
+ break;
54
+ case 'voice':
55
+ await this.collector.updateConfig(guildId, { track_voice: !config.track_voice });
56
+ break;
57
+ case 'members':
58
+ await this.collector.updateConfig(guildId, { track_members: !config.track_members });
59
+ break;
60
+ case 'commands':
61
+ await this.collector.updateConfig(guildId, { track_commands: !config.track_commands });
62
+ break;
63
+ case 'performance':
64
+ await this.collector.updateConfig(guildId, { track_command_performance: !config.track_command_performance });
65
+ break;
66
+ case 'retention': {
67
+ const days = args instanceof Map ? args.get('days') : (typeof args === 'object' && 'days' in args ? args['days'] : null);
68
+ if (days && typeof days === 'number') {
69
+ await this.collector.updateConfig(guildId, { retention_days: days });
70
+ }
71
+ break;
72
+ }
73
+ default:
74
+ break;
75
+ }
76
+ const updated = await this.collector.getConfig(guildId);
77
+ const embed = new EmbedBuilder()
78
+ .setTitle(trans('configUpdated'))
79
+ .addFields({ name: trans('enabled'), value: updated.enabled ? '✅ Yes' : '❌ No', inline: true }, { name: trans('trackMessages'), value: updated.track_messages ? '✅' : '❌', inline: true }, { name: trans('trackVoice'), value: updated.track_voice ? '✅' : '❌', inline: true }, { name: trans('trackMembers'), value: updated.track_members ? '✅' : '❌', inline: true }, { name: trans('trackCommands'), value: updated.track_commands ? '✅' : '❌', inline: true }, { name: trans('trackPerformance'), value: updated.track_command_performance ? '✅' : '❌', inline: true }, { name: trans('retentionDays'), value: (updated.retention_days || '90').toString(), inline: true })
80
+ .setColor(0x57F287);
81
+ if (message) {
82
+ await message.reply({ embeds: [embed] });
83
+ }
84
+ else if (interaction) {
85
+ if (interaction.replied || interaction.deferred) {
86
+ await interaction.editReply({ embeds: [embed] });
87
+ }
88
+ else {
89
+ await interaction.reply({ embeds: [embed], ephemeral: true });
90
+ }
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,118 @@
1
+ import { Command, CommandParameters, CommandType, ServiceContainer } from 'zumito-framework';
2
+ import { EmbedBuilder, PermissionFlagsBits } from 'zumito-framework/discord';
3
+ import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
4
+
5
+ export class AnalyticsConfig extends Command {
6
+ name = 'analytics-config';
7
+ categories = ['analytics', 'admin'];
8
+ type = CommandType.any;
9
+ dm = false;
10
+ adminOnly = true;
11
+
12
+ constructor(
13
+ private collector: AnalyticsCollector = ServiceContainer.getService(AnalyticsCollector) as AnalyticsCollector,
14
+ ) {
15
+ super();
16
+ }
17
+
18
+ async execute({ message, interaction, trans, args }: CommandParameters): Promise<void> {
19
+ const target = message || interaction;
20
+ if (!target) return;
21
+
22
+ const guildId = message?.guildId || interaction?.guildId;
23
+ if (!guildId) {
24
+ await target.reply({ content: trans('noGuild') } as any);
25
+ return;
26
+ }
27
+
28
+ const config = await this.collector.getConfig(guildId);
29
+
30
+ const subcommand = args instanceof Map ? args.get('action') : (typeof args === 'object' && 'action' in args ? args['action'] : undefined);
31
+
32
+ if (!subcommand) {
33
+ const embed = new EmbedBuilder()
34
+ .setTitle(trans('configTitle'))
35
+ .setDescription(trans('configDescription'))
36
+ .addFields(
37
+ { name: trans('enabled'), value: config.enabled ? '✅ Yes' : '❌ No', inline: true },
38
+ { name: trans('trackMessages'), value: config.track_messages ? '✅' : '❌', inline: true },
39
+ { name: trans('trackVoice'), value: config.track_voice ? '✅' : '❌', inline: true },
40
+ { name: trans('trackMembers'), value: config.track_members ? '✅' : '❌', inline: true },
41
+ { name: trans('trackCommands'), value: config.track_commands ? '✅' : '❌', inline: true },
42
+ { name: trans('trackPerformance'), value: config.track_command_performance ? '✅' : '❌', inline: true },
43
+ { name: trans('trackPerChannelVoice'), value: config.track_per_channel_voice ? '✅' : '❌', inline: true },
44
+ { name: trans('retentionDays'), value: (config.retention_days || '90').toString(), inline: true },
45
+ { name: trans('publicStats'), value: config.public_stats_page ? '✅' : '❌', inline: true },
46
+ )
47
+ .setColor(0x5865F2);
48
+
49
+ if (message) {
50
+ await message.reply({ embeds: [embed] });
51
+ } else if (interaction) {
52
+ if (interaction.replied || interaction.deferred) {
53
+ await interaction.editReply({ embeds: [embed] });
54
+ } else {
55
+ await interaction.reply({ embeds: [embed], ephemeral: true });
56
+ }
57
+ }
58
+ return;
59
+ }
60
+
61
+ switch (subcommand) {
62
+ case 'enable':
63
+ await this.collector.updateConfig(guildId, { enabled: true });
64
+ break;
65
+ case 'disable':
66
+ await this.collector.updateConfig(guildId, { enabled: false });
67
+ break;
68
+ case 'messages':
69
+ await this.collector.updateConfig(guildId, { track_messages: !config.track_messages });
70
+ break;
71
+ case 'voice':
72
+ await this.collector.updateConfig(guildId, { track_voice: !config.track_voice });
73
+ break;
74
+ case 'members':
75
+ await this.collector.updateConfig(guildId, { track_members: !config.track_members });
76
+ break;
77
+ case 'commands':
78
+ await this.collector.updateConfig(guildId, { track_commands: !config.track_commands });
79
+ break;
80
+ case 'performance':
81
+ await this.collector.updateConfig(guildId, { track_command_performance: !config.track_command_performance });
82
+ break;
83
+ case 'retention': {
84
+ const days = args instanceof Map ? args.get('days') : (typeof args === 'object' && 'days' in args ? args['days'] : null);
85
+ if (days && typeof days === 'number') {
86
+ await this.collector.updateConfig(guildId, { retention_days: days });
87
+ }
88
+ break;
89
+ }
90
+ default:
91
+ break;
92
+ }
93
+
94
+ const updated = await this.collector.getConfig(guildId);
95
+ const embed = new EmbedBuilder()
96
+ .setTitle(trans('configUpdated'))
97
+ .addFields(
98
+ { name: trans('enabled'), value: updated.enabled ? '✅ Yes' : '❌ No', inline: true },
99
+ { name: trans('trackMessages'), value: updated.track_messages ? '✅' : '❌', inline: true },
100
+ { name: trans('trackVoice'), value: updated.track_voice ? '✅' : '❌', inline: true },
101
+ { name: trans('trackMembers'), value: updated.track_members ? '✅' : '❌', inline: true },
102
+ { name: trans('trackCommands'), value: updated.track_commands ? '✅' : '❌', inline: true },
103
+ { name: trans('trackPerformance'), value: updated.track_command_performance ? '✅' : '❌', inline: true },
104
+ { name: trans('retentionDays'), value: (updated.retention_days || '90').toString(), inline: true },
105
+ )
106
+ .setColor(0x57F287);
107
+
108
+ if (message) {
109
+ await message.reply({ embeds: [embed] });
110
+ } else if (interaction) {
111
+ if (interaction.replied || interaction.deferred) {
112
+ await interaction.editReply({ embeds: [embed] });
113
+ } else {
114
+ await interaction.reply({ embeds: [embed], ephemeral: true });
115
+ }
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,11 @@
1
+ import { Command, CommandParameters } from 'zumito-framework';
2
+ import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
3
+ export declare class Stats extends Command {
4
+ private collector;
5
+ name: string;
6
+ categories: string[];
7
+ type: string;
8
+ dm: boolean;
9
+ constructor(collector?: AnalyticsCollector);
10
+ execute({ message, interaction, framework, trans }: CommandParameters): Promise<void>;
11
+ }
@@ -0,0 +1,52 @@
1
+ import { Command, CommandType, ServiceContainer } from 'zumito-framework';
2
+ import { EmbedBuilder } from 'zumito-framework/discord';
3
+ import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
4
+ export class Stats extends Command {
5
+ constructor(collector = ServiceContainer.getService(AnalyticsCollector)) {
6
+ super();
7
+ this.collector = collector;
8
+ this.name = 'stats';
9
+ this.categories = ['analytics', 'utility'];
10
+ this.type = CommandType.any;
11
+ this.dm = false;
12
+ }
13
+ async execute({ message, interaction, framework, trans }) {
14
+ const target = message || interaction;
15
+ if (!target)
16
+ return;
17
+ const guildId = message?.guildId || interaction?.guildId;
18
+ if (!guildId) {
19
+ await target.reply({ content: trans('noGuild'), ephemeral: true });
20
+ return;
21
+ }
22
+ const daysBack = 7;
23
+ const stats = await this.collector.getGuildStats(guildId, daysBack);
24
+ let totalMessages = 0, totalJoins = 0, totalLeaves = 0;
25
+ let totalVoice = 0, totalCommands = 0;
26
+ for (const s of stats) {
27
+ totalMessages += s.message_count;
28
+ totalJoins += s.join_count;
29
+ totalLeaves += s.leave_count;
30
+ totalVoice += s.voice_minutes;
31
+ totalCommands += s.command_count;
32
+ }
33
+ const embed = new EmbedBuilder()
34
+ .setTitle(trans('title'))
35
+ .setDescription(trans('description', { days: daysBack }))
36
+ .addFields({ name: trans('messages'), value: totalMessages.toString(), inline: true }, { name: trans('commands'), value: totalCommands.toString(), inline: true }, { name: trans('joins'), value: totalJoins.toString(), inline: true }, { name: trans('leaves'), value: totalLeaves.toString(), inline: true }, { name: trans('voice'), value: `${Math.round(totalVoice)} min`, inline: true })
37
+ .setColor(0x5865F2)
38
+ .setTimestamp();
39
+ const replyPayload = { embeds: [embed] };
40
+ if (message) {
41
+ await message.reply(replyPayload);
42
+ }
43
+ else if (interaction) {
44
+ if (interaction.replied || interaction.deferred) {
45
+ await interaction.editReply(replyPayload);
46
+ }
47
+ else {
48
+ await interaction.reply({ ...replyPayload, ephemeral: true });
49
+ }
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,66 @@
1
+ import { Command, CommandParameters, CommandType, ServiceContainer } from 'zumito-framework';
2
+ import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'zumito-framework/discord';
3
+ import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
4
+
5
+ export class Stats extends Command {
6
+ name = 'stats';
7
+ categories = ['analytics', 'utility'];
8
+ type = CommandType.any;
9
+ dm = false;
10
+
11
+ constructor(
12
+ private collector: AnalyticsCollector = ServiceContainer.getService(AnalyticsCollector) as AnalyticsCollector,
13
+ ) {
14
+ super();
15
+ }
16
+
17
+ async execute({ message, interaction, framework, trans }: CommandParameters): Promise<void> {
18
+ const target = message || interaction;
19
+ if (!target) return;
20
+
21
+ const guildId = message?.guildId || interaction?.guildId;
22
+ if (!guildId) {
23
+ await target.reply({ content: trans('noGuild'), ephemeral: true } as any);
24
+ return;
25
+ }
26
+
27
+ const daysBack = 7;
28
+ const stats = await this.collector.getGuildStats(guildId, daysBack);
29
+
30
+ let totalMessages = 0, totalJoins = 0, totalLeaves = 0;
31
+ let totalVoice = 0, totalCommands = 0;
32
+
33
+ for (const s of stats) {
34
+ totalMessages += s.message_count;
35
+ totalJoins += s.join_count;
36
+ totalLeaves += s.leave_count;
37
+ totalVoice += s.voice_minutes;
38
+ totalCommands += s.command_count;
39
+ }
40
+
41
+ const embed = new EmbedBuilder()
42
+ .setTitle(trans('title'))
43
+ .setDescription(trans('description', { days: daysBack }))
44
+ .addFields(
45
+ { name: trans('messages'), value: totalMessages.toString(), inline: true },
46
+ { name: trans('commands'), value: totalCommands.toString(), inline: true },
47
+ { name: trans('joins'), value: totalJoins.toString(), inline: true },
48
+ { name: trans('leaves'), value: totalLeaves.toString(), inline: true },
49
+ { name: trans('voice'), value: `${Math.round(totalVoice)} min`, inline: true },
50
+ )
51
+ .setColor(0x5865F2)
52
+ .setTimestamp();
53
+
54
+ const replyPayload = { embeds: [embed] } as any;
55
+
56
+ if (message) {
57
+ await message.reply(replyPayload);
58
+ } else if (interaction) {
59
+ if (interaction.replied || interaction.deferred) {
60
+ await interaction.editReply(replyPayload);
61
+ } else {
62
+ await interaction.reply({ ...replyPayload, ephemeral: true });
63
+ }
64
+ }
65
+ }
66
+ }
package/config.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export declare class AnalyticsModuleConfig {
2
+ static defaultRetentionDays: number;
3
+ static cleanupIntervalHours: number;
4
+ static defaultTrackMessages: boolean;
5
+ static defaultTrackVoice: boolean;
6
+ static defaultTrackMembers: boolean;
7
+ static defaultTrackCommands: boolean;
8
+ static defaultTrackCommandPerformance: boolean;
9
+ static defaultTrackPerChannelVoice: boolean;
10
+ static configure(opts: Partial<typeof AnalyticsModuleConfig>): void;
11
+ }
package/config.js ADDED
@@ -0,0 +1,13 @@
1
+ export class AnalyticsModuleConfig {
2
+ static configure(opts) {
3
+ Object.assign(this, opts);
4
+ }
5
+ }
6
+ AnalyticsModuleConfig.defaultRetentionDays = 90;
7
+ AnalyticsModuleConfig.cleanupIntervalHours = 24;
8
+ AnalyticsModuleConfig.defaultTrackMessages = true;
9
+ AnalyticsModuleConfig.defaultTrackVoice = true;
10
+ AnalyticsModuleConfig.defaultTrackMembers = true;
11
+ AnalyticsModuleConfig.defaultTrackCommands = true;
12
+ AnalyticsModuleConfig.defaultTrackCommandPerformance = false;
13
+ AnalyticsModuleConfig.defaultTrackPerChannelVoice = false;
package/config.ts ADDED
@@ -0,0 +1,14 @@
1
+ export class AnalyticsModuleConfig {
2
+ static defaultRetentionDays = 90;
3
+ static cleanupIntervalHours = 24;
4
+ static defaultTrackMessages = true;
5
+ static defaultTrackVoice = true;
6
+ static defaultTrackMembers = true;
7
+ static defaultTrackCommands = true;
8
+ static defaultTrackCommandPerformance = false;
9
+ static defaultTrackPerChannelVoice = false;
10
+
11
+ static configure(opts: Partial<typeof AnalyticsModuleConfig>) {
12
+ Object.assign(this, opts);
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ import { FrameworkEvent } from 'zumito-framework';
2
+ export declare class GuildMemberAdd extends FrameworkEvent {
3
+ once: boolean;
4
+ source: string;
5
+ private collector;
6
+ execute({ guildmember }: any): Promise<void>;
7
+ }
@@ -0,0 +1,24 @@
1
+ import { FrameworkEvent } from 'zumito-framework';
2
+ import { ServiceContainer } from 'zumito-framework';
3
+ import { AnalyticsCollector } from '../../services/AnalyticsCollector.js';
4
+ export class GuildMemberAdd extends FrameworkEvent {
5
+ constructor() {
6
+ super(...arguments);
7
+ this.once = false;
8
+ this.source = 'discord';
9
+ this.collector = ServiceContainer.getService(AnalyticsCollector);
10
+ }
11
+ async execute({ guildmember }) {
12
+ if (!guildmember?.guild?.id)
13
+ return;
14
+ const guildId = guildmember.guild.id;
15
+ await this.collector.recordMemberJoin(guildId);
16
+ try {
17
+ const memberCount = guildmember.guild.memberCount;
18
+ if (memberCount) {
19
+ await this.collector.recordMemberCount(guildId, memberCount);
20
+ }
21
+ }
22
+ catch (_) { /* ignore */ }
23
+ }
24
+ }