@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.
- package/README.md +237 -0
- package/commands/AnalyticsConfig.d.ts +12 -0
- package/commands/AnalyticsConfig.js +93 -0
- package/commands/AnalyticsConfig.ts +118 -0
- package/commands/Stats.d.ts +11 -0
- package/commands/Stats.js +52 -0
- package/commands/Stats.ts +66 -0
- package/config.d.ts +11 -0
- package/config.js +13 -0
- package/config.ts +14 -0
- package/events/discord/GuildMemberAdd.d.ts +7 -0
- package/events/discord/GuildMemberAdd.js +24 -0
- package/events/discord/GuildMemberAdd.ts +23 -0
- package/events/discord/GuildMemberRemove.d.ts +7 -0
- package/events/discord/GuildMemberRemove.js +24 -0
- package/events/discord/GuildMemberRemove.ts +23 -0
- package/events/discord/MessageCreate.d.ts +7 -0
- package/events/discord/MessageCreate.js +18 -0
- package/events/discord/MessageCreate.ts +16 -0
- package/events/discord/VoiceStateUpdate.d.ts +7 -0
- package/events/discord/VoiceStateUpdate.js +29 -0
- package/events/discord/VoiceStateUpdate.ts +31 -0
- package/events/framework/CommandExecuted.d.ts +7 -0
- package/events/framework/CommandExecuted.js +17 -0
- package/events/framework/CommandExecuted.ts +16 -0
- package/index.d.ts +15 -0
- package/index.js +61 -0
- package/index.ts +69 -0
- package/models/CommandDailyStats.d.ts +9 -0
- package/models/CommandDailyStats.js +34 -0
- package/models/CommandDailyStats.ts +25 -0
- package/models/GuildAnalyticsConfig.d.ts +12 -0
- package/models/GuildAnalyticsConfig.js +43 -0
- package/models/GuildAnalyticsConfig.ts +34 -0
- package/models/GuildDailyStats.d.ts +11 -0
- package/models/GuildDailyStats.js +40 -0
- package/models/GuildDailyStats.ts +31 -0
- package/models/VoiceChannelDailyStats.d.ts +8 -0
- package/models/VoiceChannelDailyStats.js +31 -0
- package/models/VoiceChannelDailyStats.ts +22 -0
- package/package.json +21 -0
- package/routes/AdminAnalytics.d.ts +11 -0
- package/routes/AdminAnalytics.js +55 -0
- package/routes/AdminAnalytics.ts +69 -0
- package/routes/UserPanelAnalytics.d.ts +11 -0
- package/routes/UserPanelAnalytics.js +81 -0
- package/routes/UserPanelAnalytics.ts +101 -0
- package/services/AnalyticsCollector.d.ts +62 -0
- package/services/AnalyticsCollector.js +470 -0
- package/services/AnalyticsCollector.ts +537 -0
- package/translations/en.json +63 -0
- package/translations/es.json +63 -0
- package/tsconfig.json +18 -0
- package/views/admin-analytics.ejs +170 -0
- 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,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
|
+
}
|