@zumito-team/analytics-module 0.6.0 → 0.8.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/dist/events/discord/MessageCreate.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +34 -4
- package/dist/models/ChannelMessageStats.d.ts +8 -0
- package/dist/models/ChannelMessageStats.js +31 -0
- package/dist/routes/AdminAnalytics.d.ts +1 -3
- package/dist/routes/AdminAnalytics.js +2 -23
- package/dist/routes/AdminAnalyticsCommands.d.ts +7 -0
- package/dist/routes/AdminAnalyticsCommands.js +34 -0
- package/dist/routes/AdminAnalyticsGrowth.d.ts +7 -0
- package/dist/routes/AdminAnalyticsGrowth.js +34 -0
- package/dist/routes/AdminAnalyticsMessages.d.ts +7 -0
- package/dist/routes/AdminAnalyticsMessages.js +32 -0
- package/dist/routes/UserPanelAnalytics.js +1 -17
- package/dist/routes/UserPanelAnalyticsCommands.d.ts +8 -0
- package/dist/routes/UserPanelAnalyticsCommands.js +48 -0
- package/dist/routes/UserPanelAnalyticsMessages.d.ts +8 -0
- package/dist/routes/UserPanelAnalyticsMessages.js +52 -0
- package/dist/routes/UserPanelAnalyticsVoice.d.ts +8 -0
- package/dist/routes/UserPanelAnalyticsVoice.js +53 -0
- package/dist/services/AnalyticsCollector.d.ts +5 -1
- package/dist/services/AnalyticsCollector.js +61 -3
- package/dist/views/admin-analytics-commands.ejs +55 -0
- package/dist/views/admin-analytics-growth.ejs +46 -0
- package/dist/views/admin-analytics-messages.ejs +30 -0
- package/dist/views/admin-analytics.ejs +38 -132
- package/dist/views/user-analytics-commands.ejs +55 -0
- package/dist/views/user-analytics-messages.ejs +47 -0
- package/dist/views/user-analytics-voice.ejs +47 -0
- package/dist/views/user-analytics.ejs +25 -166
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -14,4 +14,5 @@ export { AnalyticsModuleConfig } from './config.js';
|
|
|
14
14
|
export { GuildDailyStats } from './models/GuildDailyStats.js';
|
|
15
15
|
export { CommandDailyStats } from './models/CommandDailyStats.js';
|
|
16
16
|
export { VoiceChannelDailyStats } from './models/VoiceChannelDailyStats.js';
|
|
17
|
+
export { ChannelMessageStats } from './models/ChannelMessageStats.js';
|
|
17
18
|
export { GuildAnalyticsConfig } from './models/GuildAnalyticsConfig.js';
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,10 @@ export class AnalyticsModule extends Module {
|
|
|
24
24
|
{
|
|
25
25
|
label: 'Analiticas',
|
|
26
26
|
items: [
|
|
27
|
-
{ label: '
|
|
27
|
+
{ label: 'Resumen', url: '/admin/analytics' },
|
|
28
|
+
{ label: 'Mensajes', url: '/admin/analytics/messages' },
|
|
29
|
+
{ label: 'Comandos', url: '/admin/analytics/commands' },
|
|
30
|
+
{ label: 'Crecimiento', url: '/admin/analytics/growth' },
|
|
28
31
|
],
|
|
29
32
|
},
|
|
30
33
|
],
|
|
@@ -37,9 +40,35 @@ export class AnalyticsModule extends Module {
|
|
|
37
40
|
try {
|
|
38
41
|
const { UserPanelNavigationService } = await import('@zumito-team/user-panel-module/services/UserPanelNavigationService');
|
|
39
42
|
const nav = ServiceContainer.getService(UserPanelNavigationService);
|
|
40
|
-
nav.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
nav.registerItem({
|
|
44
|
+
id: 'analytics',
|
|
45
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-discord-white/60 group-hover:text-white" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 16l4-8 4 5 4-3"/></svg>`,
|
|
46
|
+
label: 'analytics.sidebarTitle',
|
|
47
|
+
url: '/panel/:guildId/analytics',
|
|
48
|
+
order: 3,
|
|
49
|
+
category: 'general',
|
|
50
|
+
sidebar: {
|
|
51
|
+
showDropdown: false,
|
|
52
|
+
sections: [
|
|
53
|
+
{
|
|
54
|
+
id: 'analytics-overview',
|
|
55
|
+
label: 'analytics.overview',
|
|
56
|
+
items: [
|
|
57
|
+
{ label: 'analytics.overview', url: '/panel/:guildId/analytics' },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'analytics-details',
|
|
62
|
+
label: 'analytics.detailedStats',
|
|
63
|
+
items: [
|
|
64
|
+
{ label: 'analytics.messages', url: '/panel/:guildId/analytics/messages' },
|
|
65
|
+
{ label: 'analytics.voice', url: '/panel/:guildId/analytics/voice' },
|
|
66
|
+
{ label: 'analytics.commands', url: '/panel/:guildId/analytics/commands' },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
});
|
|
43
72
|
}
|
|
44
73
|
catch (e) {
|
|
45
74
|
console.warn('[AnalyticsModule] User panel not available, skipping user panel integration');
|
|
@@ -60,4 +89,5 @@ export { AnalyticsModuleConfig } from './config.js';
|
|
|
60
89
|
export { GuildDailyStats } from './models/GuildDailyStats.js';
|
|
61
90
|
export { CommandDailyStats } from './models/CommandDailyStats.js';
|
|
62
91
|
export { VoiceChannelDailyStats } from './models/VoiceChannelDailyStats.js';
|
|
92
|
+
export { ChannelMessageStats } from './models/ChannelMessageStats.js';
|
|
63
93
|
export { GuildAnalyticsConfig } from './models/GuildAnalyticsConfig.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Collection, Field } from 'zumito-framework';
|
|
8
|
+
let ChannelMessageStats = class ChannelMessageStats {
|
|
9
|
+
};
|
|
10
|
+
__decorate([
|
|
11
|
+
Field({ type: 'string', primary: true, unique: true })
|
|
12
|
+
], ChannelMessageStats.prototype, "id", void 0);
|
|
13
|
+
__decorate([
|
|
14
|
+
Field({ type: 'string' })
|
|
15
|
+
], ChannelMessageStats.prototype, "guild_id", void 0);
|
|
16
|
+
__decorate([
|
|
17
|
+
Field({ type: 'string' })
|
|
18
|
+
], ChannelMessageStats.prototype, "channel_id", void 0);
|
|
19
|
+
__decorate([
|
|
20
|
+
Field({ type: 'string' })
|
|
21
|
+
], ChannelMessageStats.prototype, "date", void 0);
|
|
22
|
+
__decorate([
|
|
23
|
+
Field({ type: 'number', default: 0 })
|
|
24
|
+
], ChannelMessageStats.prototype, "message_count", void 0);
|
|
25
|
+
__decorate([
|
|
26
|
+
Field({ type: 'number', default: 0 })
|
|
27
|
+
], ChannelMessageStats.prototype, "unique_authors", void 0);
|
|
28
|
+
ChannelMessageStats = __decorate([
|
|
29
|
+
Collection({ name: 'analytics_channel_message_stats' })
|
|
30
|
+
], ChannelMessageStats);
|
|
31
|
+
export { ChannelMessageStats };
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { Route, RouteMethod } from 'zumito-framework';
|
|
2
|
-
import { Client } from 'zumito-framework/discord';
|
|
3
2
|
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
4
3
|
export declare class AdminAnalytics extends Route {
|
|
5
4
|
private collector;
|
|
6
|
-
private client;
|
|
7
5
|
method: RouteMethod;
|
|
8
6
|
path: string;
|
|
9
|
-
constructor(collector?: AnalyticsCollector
|
|
7
|
+
constructor(collector?: AnalyticsCollector);
|
|
10
8
|
execute(req: any, res: any): Promise<void>;
|
|
11
9
|
}
|
|
@@ -2,14 +2,12 @@ import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
|
2
2
|
import path, { dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import ejs from 'ejs';
|
|
5
|
-
import { Client } from 'zumito-framework/discord';
|
|
6
5
|
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
7
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
7
|
export class AdminAnalytics extends Route {
|
|
9
|
-
constructor(collector = ServiceContainer.getService(AnalyticsCollector)
|
|
8
|
+
constructor(collector = ServiceContainer.getService(AnalyticsCollector)) {
|
|
10
9
|
super();
|
|
11
10
|
this.collector = collector;
|
|
12
|
-
this.client = client;
|
|
13
11
|
this.method = RouteMethod.get;
|
|
14
12
|
this.path = '/admin/analytics';
|
|
15
13
|
}
|
|
@@ -22,26 +20,7 @@ export class AdminAnalytics extends Route {
|
|
|
22
20
|
const summary = await this.collector.getGlobalStatsSummary(daysBack);
|
|
23
21
|
const guildGrowth = await this.collector.getGuildGrowth(daysBack);
|
|
24
22
|
const messagesPerDay = await this.collector.getMessagesPerDay(daysBack);
|
|
25
|
-
const
|
|
26
|
-
const topCommands = await this.collector.getTopCommands(null, daysBack);
|
|
27
|
-
const slowestCommands = await this.collector.getSlowestCommands(null, daysBack);
|
|
28
|
-
const chartData = {
|
|
29
|
-
guildGrowth,
|
|
30
|
-
messagesPerDay,
|
|
31
|
-
commandsPerDay,
|
|
32
|
-
topCommands,
|
|
33
|
-
slowestCommands,
|
|
34
|
-
};
|
|
35
|
-
const { TranslationManager } = await import('zumito-framework');
|
|
36
|
-
const tm = ServiceContainer.getService(TranslationManager);
|
|
37
|
-
const t = (key, params) => tm.get(key, 'en', params);
|
|
38
|
-
const content = await ejs.renderFile(path.resolve(__dirname, '../views/admin-analytics.ejs'), {
|
|
39
|
-
summary,
|
|
40
|
-
chartData,
|
|
41
|
-
slowestCommands: slowestCommands.length > 0 ? slowestCommands : undefined,
|
|
42
|
-
t,
|
|
43
|
-
daysBack,
|
|
44
|
-
});
|
|
23
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/admin-analytics.ejs'), { summary, guildGrowth, messagesPerDay });
|
|
45
24
|
const { AdminViewService } = await import('@zumito-team/admin-module/services/AdminViewService.js');
|
|
46
25
|
const view = ServiceContainer.getService(AdminViewService);
|
|
47
26
|
const html = await view.render({
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
2
|
+
import path, { dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export class AdminAnalyticsCommands extends Route {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.method = RouteMethod.get;
|
|
11
|
+
this.path = '/admin/analytics/commands';
|
|
12
|
+
this.collector = ServiceContainer.getService(AnalyticsCollector);
|
|
13
|
+
}
|
|
14
|
+
async execute(req, res) {
|
|
15
|
+
const { AdminAuthService } = await import('@zumito-team/admin-module/services/AdminAuthService.js');
|
|
16
|
+
const auth = await ServiceContainer.getService(AdminAuthService).isLoginValid(req);
|
|
17
|
+
if (!auth?.isValid)
|
|
18
|
+
return res.redirect('/admin/login');
|
|
19
|
+
const daysBack = parseInt(req.query.days) || 7;
|
|
20
|
+
const commandsPerDay = await this.collector.getCommandsPerDay(null, daysBack);
|
|
21
|
+
const topCommands = await this.collector.getTopCommands(null, daysBack, 15);
|
|
22
|
+
const slowestCommands = await this.collector.getSlowestCommands(null, daysBack, 10);
|
|
23
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/admin-analytics-commands.ejs'), { commandsPerDay, topCommands, slowestCommands, daysBack });
|
|
24
|
+
const { AdminViewService } = await import('@zumito-team/admin-module/services/AdminViewService.js');
|
|
25
|
+
const view = ServiceContainer.getService(AdminViewService);
|
|
26
|
+
const html = await view.render({
|
|
27
|
+
title: 'Analiticas - Comandos',
|
|
28
|
+
content,
|
|
29
|
+
reqPath: this.path,
|
|
30
|
+
user: { name: 'Admin' },
|
|
31
|
+
});
|
|
32
|
+
res.send(html);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
2
|
+
import path, { dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export class AdminAnalyticsGrowth extends Route {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.method = RouteMethod.get;
|
|
11
|
+
this.path = '/admin/analytics/growth';
|
|
12
|
+
this.collector = ServiceContainer.getService(AnalyticsCollector);
|
|
13
|
+
}
|
|
14
|
+
async execute(req, res) {
|
|
15
|
+
const { AdminAuthService } = await import('@zumito-team/admin-module/services/AdminAuthService.js');
|
|
16
|
+
const auth = await ServiceContainer.getService(AdminAuthService).isLoginValid(req);
|
|
17
|
+
if (!auth?.isValid)
|
|
18
|
+
return res.redirect('/admin/login');
|
|
19
|
+
const daysBack = parseInt(req.query.days) || 30;
|
|
20
|
+
const guildGrowth = await this.collector.getGuildGrowth(daysBack);
|
|
21
|
+
const summary = await this.collector.getGlobalStatsSummary(daysBack);
|
|
22
|
+
const totalGuilds = Math.max(summary.totalGuilds, guildGrowth.length > 0 ? guildGrowth[guildGrowth.length - 1].guildCount : 0);
|
|
23
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/admin-analytics-growth.ejs'), { guildGrowth, totalGuilds, summary, daysBack });
|
|
24
|
+
const { AdminViewService } = await import('@zumito-team/admin-module/services/AdminViewService.js');
|
|
25
|
+
const view = ServiceContainer.getService(AdminViewService);
|
|
26
|
+
const html = await view.render({
|
|
27
|
+
title: 'Analiticas - Crecimiento',
|
|
28
|
+
content,
|
|
29
|
+
reqPath: this.path,
|
|
30
|
+
user: { name: 'Admin' },
|
|
31
|
+
});
|
|
32
|
+
res.send(html);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
2
|
+
import path, { dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export class AdminAnalyticsMessages extends Route {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.method = RouteMethod.get;
|
|
11
|
+
this.path = '/admin/analytics/messages';
|
|
12
|
+
this.collector = ServiceContainer.getService(AnalyticsCollector);
|
|
13
|
+
}
|
|
14
|
+
async execute(req, res) {
|
|
15
|
+
const { AdminAuthService } = await import('@zumito-team/admin-module/services/AdminAuthService.js');
|
|
16
|
+
const auth = await ServiceContainer.getService(AdminAuthService).isLoginValid(req);
|
|
17
|
+
if (!auth?.isValid)
|
|
18
|
+
return res.redirect('/admin/login');
|
|
19
|
+
const daysBack = parseInt(req.query.days) || 7;
|
|
20
|
+
const messagesPerDay = await this.collector.getMessagesPerDay(daysBack);
|
|
21
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/admin-analytics-messages.ejs'), { messagesPerDay, daysBack });
|
|
22
|
+
const { AdminViewService } = await import('@zumito-team/admin-module/services/AdminViewService.js');
|
|
23
|
+
const view = ServiceContainer.getService(AdminViewService);
|
|
24
|
+
const html = await view.render({
|
|
25
|
+
title: 'Analiticas - Mensajes',
|
|
26
|
+
content,
|
|
27
|
+
reqPath: this.path,
|
|
28
|
+
user: { name: 'Admin' },
|
|
29
|
+
});
|
|
30
|
+
res.send(html);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -57,23 +57,7 @@ export class UserPanelAnalytics extends Route {
|
|
|
57
57
|
leavesPerDay.push({ date: s.date, count: s.leave_count });
|
|
58
58
|
voicePerDay.push({ date: s.date, count: s.voice_minutes });
|
|
59
59
|
}
|
|
60
|
-
const
|
|
61
|
-
const topCommands = await this.collector.getTopCommands(guildId, daysBack);
|
|
62
|
-
const slowestCommands = await this.collector.getSlowestCommands(guildId, daysBack);
|
|
63
|
-
const channelVoice = await this.collector.getVoiceChannelStats(guildId, daysBack);
|
|
64
|
-
const chartData = {
|
|
65
|
-
messagesPerDay, joinsPerDay, leavesPerDay, voicePerDay,
|
|
66
|
-
commandsPerDay, topCommands, slowestCommands, channelVoice,
|
|
67
|
-
};
|
|
68
|
-
const config = await this.collector.getConfig(guildId);
|
|
69
|
-
const content = await ejs.renderFile(path.resolve(__dirname, '../views/user-analytics.ejs'), {
|
|
70
|
-
guildSummary,
|
|
71
|
-
chartData,
|
|
72
|
-
slowestCommands: (config.track_command_performance && slowestCommands.length > 0) ? slowestCommands : undefined,
|
|
73
|
-
channelVoice: (config.track_per_channel_voice && channelVoice.length > 0) ? channelVoice : undefined,
|
|
74
|
-
t,
|
|
75
|
-
daysBack,
|
|
76
|
-
});
|
|
60
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/user-analytics.ejs'), { guildSummary, messagesPerDay, joinsPerDay, leavesPerDay, t });
|
|
77
61
|
const view = ServiceContainer.getService(UserPanelViewService);
|
|
78
62
|
const html = await view.render({ content, reqPath: req.path, req, res });
|
|
79
63
|
res.send(html);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
2
|
+
import path, { dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { Client, PermissionFlagsBits } from 'zumito-framework/discord';
|
|
6
|
+
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
export class UserPanelAnalyticsCommands extends Route {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.method = RouteMethod.get;
|
|
12
|
+
this.path = '/panel/:guildId/analytics/commands';
|
|
13
|
+
this.collector = ServiceContainer.getService(AnalyticsCollector);
|
|
14
|
+
this.client = ServiceContainer.getService(Client);
|
|
15
|
+
}
|
|
16
|
+
async execute(req, res) {
|
|
17
|
+
const { UserPanelAuthService } = await import('@zumito-team/user-panel-module/services/UserPanelAuthService');
|
|
18
|
+
const { UserPanelViewService } = await import('@zumito-team/user-panel-module/services/UserPanelViewService');
|
|
19
|
+
const { UserPanelLanguageManager } = await import('@zumito-team/user-panel-module/services/UserPanelLanguageManager');
|
|
20
|
+
const authService = ServiceContainer.getService(UserPanelAuthService);
|
|
21
|
+
const authData = await authService.isLoginValid(req);
|
|
22
|
+
if (!authData.isValid)
|
|
23
|
+
return res.redirect('/panel/login');
|
|
24
|
+
const userId = authData.data.discordUserData.id;
|
|
25
|
+
const guildId = req.params.guildId;
|
|
26
|
+
const guild = this.client.guilds.cache.get(guildId);
|
|
27
|
+
if (!guild)
|
|
28
|
+
return res.status(404).send('Server not found');
|
|
29
|
+
let member = guild.members.cache.get(userId);
|
|
30
|
+
if (!member)
|
|
31
|
+
member = await guild.members.fetch(userId).catch(() => null);
|
|
32
|
+
if (!member || !(member.permissions.has(PermissionFlagsBits.Administrator) ||
|
|
33
|
+
member.permissions.has(PermissionFlagsBits.ManageGuild) || guild.ownerId === userId)) {
|
|
34
|
+
return res.status(403).send('No tienes permisos en este servidor');
|
|
35
|
+
}
|
|
36
|
+
const daysBack = parseInt(req.query.days) || 7;
|
|
37
|
+
const langMgr = ServiceContainer.getService(UserPanelLanguageManager);
|
|
38
|
+
const { t } = langMgr.getLanguageVariables(req, res);
|
|
39
|
+
const commandsPerDay = await this.collector.getCommandsPerDay(guildId, daysBack);
|
|
40
|
+
const topCommands = await this.collector.getTopCommands(guildId, daysBack, 15);
|
|
41
|
+
const slowestCommands = await this.collector.getSlowestCommands(guildId, daysBack, 10);
|
|
42
|
+
const config = await this.collector.getConfig(guildId);
|
|
43
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/user-analytics-commands.ejs'), { commandsPerDay, topCommands, slowestCommands: config.track_command_performance ? slowestCommands : null, t, daysBack });
|
|
44
|
+
const view = ServiceContainer.getService(UserPanelViewService);
|
|
45
|
+
const html = await view.render({ content, reqPath: req.path, req, res });
|
|
46
|
+
res.send(html);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
2
|
+
import path, { dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { Client, PermissionFlagsBits } from 'zumito-framework/discord';
|
|
6
|
+
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
export class UserPanelAnalyticsMessages extends Route {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.method = RouteMethod.get;
|
|
12
|
+
this.path = '/panel/:guildId/analytics/messages';
|
|
13
|
+
this.collector = ServiceContainer.getService(AnalyticsCollector);
|
|
14
|
+
this.client = ServiceContainer.getService(Client);
|
|
15
|
+
}
|
|
16
|
+
async execute(req, res) {
|
|
17
|
+
const { UserPanelAuthService } = await import('@zumito-team/user-panel-module/services/UserPanelAuthService');
|
|
18
|
+
const { UserPanelViewService } = await import('@zumito-team/user-panel-module/services/UserPanelViewService');
|
|
19
|
+
const { UserPanelLanguageManager } = await import('@zumito-team/user-panel-module/services/UserPanelLanguageManager');
|
|
20
|
+
const authService = ServiceContainer.getService(UserPanelAuthService);
|
|
21
|
+
const authData = await authService.isLoginValid(req);
|
|
22
|
+
if (!authData.isValid)
|
|
23
|
+
return res.redirect('/panel/login');
|
|
24
|
+
const userId = authData.data.discordUserData.id;
|
|
25
|
+
const guildId = req.params.guildId;
|
|
26
|
+
const guild = this.client.guilds.cache.get(guildId);
|
|
27
|
+
if (!guild)
|
|
28
|
+
return res.status(404).send('Server not found');
|
|
29
|
+
let member = guild.members.cache.get(userId);
|
|
30
|
+
if (!member)
|
|
31
|
+
member = await guild.members.fetch(userId).catch(() => null);
|
|
32
|
+
if (!member || !(member.permissions.has(PermissionFlagsBits.Administrator) ||
|
|
33
|
+
member.permissions.has(PermissionFlagsBits.ManageGuild) || guild.ownerId === userId)) {
|
|
34
|
+
return res.status(403).send('No tienes permisos en este servidor');
|
|
35
|
+
}
|
|
36
|
+
const daysBack = parseInt(req.query.days) || 7;
|
|
37
|
+
const langMgr = ServiceContainer.getService(UserPanelLanguageManager);
|
|
38
|
+
const { t } = langMgr.getLanguageVariables(req, res);
|
|
39
|
+
const stats = await this.collector.getGuildStats(guildId, daysBack);
|
|
40
|
+
let totalMessages = 0;
|
|
41
|
+
const messagesPerDay = [];
|
|
42
|
+
for (const s of stats) {
|
|
43
|
+
totalMessages += s.message_count;
|
|
44
|
+
messagesPerDay.push({ date: s.date, count: s.message_count });
|
|
45
|
+
}
|
|
46
|
+
const channelMessages = await this.collector.getChannelMessageStats(guildId, daysBack);
|
|
47
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/user-analytics-messages.ejs'), { totalMessages, messagesPerDay, channelMessages, t, daysBack });
|
|
48
|
+
const view = ServiceContainer.getService(UserPanelViewService);
|
|
49
|
+
const html = await view.render({ content, reqPath: req.path, req, res });
|
|
50
|
+
res.send(html);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
2
|
+
import path, { dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { Client, PermissionFlagsBits } from 'zumito-framework/discord';
|
|
6
|
+
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
export class UserPanelAnalyticsVoice extends Route {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.method = RouteMethod.get;
|
|
12
|
+
this.path = '/panel/:guildId/analytics/voice';
|
|
13
|
+
this.collector = ServiceContainer.getService(AnalyticsCollector);
|
|
14
|
+
this.client = ServiceContainer.getService(Client);
|
|
15
|
+
}
|
|
16
|
+
async execute(req, res) {
|
|
17
|
+
const { UserPanelAuthService } = await import('@zumito-team/user-panel-module/services/UserPanelAuthService');
|
|
18
|
+
const { UserPanelViewService } = await import('@zumito-team/user-panel-module/services/UserPanelViewService');
|
|
19
|
+
const { UserPanelLanguageManager } = await import('@zumito-team/user-panel-module/services/UserPanelLanguageManager');
|
|
20
|
+
const authService = ServiceContainer.getService(UserPanelAuthService);
|
|
21
|
+
const authData = await authService.isLoginValid(req);
|
|
22
|
+
if (!authData.isValid)
|
|
23
|
+
return res.redirect('/panel/login');
|
|
24
|
+
const userId = authData.data.discordUserData.id;
|
|
25
|
+
const guildId = req.params.guildId;
|
|
26
|
+
const guild = this.client.guilds.cache.get(guildId);
|
|
27
|
+
if (!guild)
|
|
28
|
+
return res.status(404).send('Server not found');
|
|
29
|
+
let member = guild.members.cache.get(userId);
|
|
30
|
+
if (!member)
|
|
31
|
+
member = await guild.members.fetch(userId).catch(() => null);
|
|
32
|
+
if (!member || !(member.permissions.has(PermissionFlagsBits.Administrator) ||
|
|
33
|
+
member.permissions.has(PermissionFlagsBits.ManageGuild) || guild.ownerId === userId)) {
|
|
34
|
+
return res.status(403).send('No tienes permisos en este servidor');
|
|
35
|
+
}
|
|
36
|
+
const daysBack = parseInt(req.query.days) || 7;
|
|
37
|
+
const langMgr = ServiceContainer.getService(UserPanelLanguageManager);
|
|
38
|
+
const { t } = langMgr.getLanguageVariables(req, res);
|
|
39
|
+
const stats = await this.collector.getGuildStats(guildId, daysBack);
|
|
40
|
+
const channelVoice = await this.collector.getVoiceChannelStats(guildId, daysBack);
|
|
41
|
+
let totalVoice = 0;
|
|
42
|
+
const voicePerDay = [];
|
|
43
|
+
for (const s of stats) {
|
|
44
|
+
totalVoice += s.voice_minutes;
|
|
45
|
+
voicePerDay.push({ date: s.date, count: s.voice_minutes });
|
|
46
|
+
}
|
|
47
|
+
const config = await this.collector.getConfig(guildId);
|
|
48
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/user-analytics-voice.ejs'), { totalVoice, voicePerDay, channelVoice: config.track_per_channel_voice ? channelVoice : null, t, daysBack });
|
|
49
|
+
const view = ServiceContainer.getService(UserPanelViewService);
|
|
50
|
+
const html = await view.render({ content, reqPath: req.path, req, res });
|
|
51
|
+
res.send(html);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -2,6 +2,7 @@ import { GuildDailyStats } from '../models/GuildDailyStats.js';
|
|
|
2
2
|
import { CommandDailyStats } from '../models/CommandDailyStats.js';
|
|
3
3
|
import { VoiceChannelDailyStats } from '../models/VoiceChannelDailyStats.js';
|
|
4
4
|
import { GuildAnalyticsConfig } from '../models/GuildAnalyticsConfig.js';
|
|
5
|
+
import { ChannelMessageStats } from '../models/ChannelMessageStats.js';
|
|
5
6
|
export interface CommandExecutedPayload {
|
|
6
7
|
guildId: string;
|
|
7
8
|
commandName: string;
|
|
@@ -13,11 +14,13 @@ export declare class AnalyticsCollector {
|
|
|
13
14
|
private db;
|
|
14
15
|
private voiceSessions;
|
|
15
16
|
private dailyVoiceUsers;
|
|
17
|
+
private dailyMessageAuthors;
|
|
18
|
+
private dailyChannelMessageAuthors;
|
|
16
19
|
private cleanupTimer;
|
|
17
20
|
constructor(db?: any);
|
|
18
21
|
private repo;
|
|
19
22
|
private ensuredConfig;
|
|
20
|
-
recordMessage(guildId: string): Promise<void>;
|
|
23
|
+
recordMessage(guildId: string, channelId?: string, authorId?: string): Promise<void>;
|
|
21
24
|
recordMemberJoin(guildId: string): Promise<void>;
|
|
22
25
|
recordMemberLeave(guildId: string): Promise<void>;
|
|
23
26
|
recordVoiceJoin(guildId: string, channelId: string, userId: string): Promise<void>;
|
|
@@ -52,6 +55,7 @@ export declare class AnalyticsCollector {
|
|
|
52
55
|
avgExecutionTimeMs: number;
|
|
53
56
|
})[]>;
|
|
54
57
|
getVoiceChannelStats(guildId: string, daysBack: number): Promise<VoiceChannelDailyStats[]>;
|
|
58
|
+
getChannelMessageStats(guildId: string, daysBack: number): Promise<ChannelMessageStats[]>;
|
|
55
59
|
getConfig(guildId: string): Promise<GuildAnalyticsConfig>;
|
|
56
60
|
updateConfig(guildId: string, partial: Partial<GuildAnalyticsConfig>): Promise<void>;
|
|
57
61
|
runCleanup(): Promise<void>;
|
|
@@ -3,6 +3,7 @@ import { GuildDailyStats } from '../models/GuildDailyStats.js';
|
|
|
3
3
|
import { CommandDailyStats } from '../models/CommandDailyStats.js';
|
|
4
4
|
import { VoiceChannelDailyStats } from '../models/VoiceChannelDailyStats.js';
|
|
5
5
|
import { GuildAnalyticsConfig } from '../models/GuildAnalyticsConfig.js';
|
|
6
|
+
import { ChannelMessageStats } from '../models/ChannelMessageStats.js';
|
|
6
7
|
import { AnalyticsModuleConfig } from '../config.js';
|
|
7
8
|
function today() {
|
|
8
9
|
return new Date().toISOString().slice(0, 10);
|
|
@@ -12,6 +13,8 @@ export class AnalyticsCollector {
|
|
|
12
13
|
this.db = db;
|
|
13
14
|
this.voiceSessions = new Map();
|
|
14
15
|
this.dailyVoiceUsers = new Map();
|
|
16
|
+
this.dailyMessageAuthors = new Map();
|
|
17
|
+
this.dailyChannelMessageAuthors = new Map();
|
|
15
18
|
this.cleanupTimer = null;
|
|
16
19
|
}
|
|
17
20
|
repo(name) {
|
|
@@ -38,11 +41,48 @@ export class AnalyticsCollector {
|
|
|
38
41
|
await repo.insert(defaults);
|
|
39
42
|
return defaults;
|
|
40
43
|
}
|
|
41
|
-
async recordMessage(guildId) {
|
|
44
|
+
async recordMessage(guildId, channelId, authorId) {
|
|
42
45
|
const config = await this.ensuredConfig(guildId);
|
|
43
46
|
if (!config.enabled || !config.track_messages)
|
|
44
47
|
return;
|
|
45
48
|
await this.upsertGuildStats(guildId, { message_count: 1 });
|
|
49
|
+
// Track unique authors per day
|
|
50
|
+
if (authorId) {
|
|
51
|
+
const authorKey = `${guildId}_${today()}`;
|
|
52
|
+
if (!this.dailyMessageAuthors.has(authorKey)) {
|
|
53
|
+
this.dailyMessageAuthors.set(authorKey, new Set());
|
|
54
|
+
}
|
|
55
|
+
this.dailyMessageAuthors.get(authorKey).add(authorId);
|
|
56
|
+
}
|
|
57
|
+
// Track per-channel stats
|
|
58
|
+
if (channelId && config.track_per_channel_voice) {
|
|
59
|
+
const date = today();
|
|
60
|
+
const chanKey = `${guildId}_${channelId}_${date}`;
|
|
61
|
+
const repo = this.repo(ChannelMessageStats);
|
|
62
|
+
const existing = await repo.findOne({ id: chanKey });
|
|
63
|
+
if (authorId) {
|
|
64
|
+
if (!this.dailyChannelMessageAuthors.has(chanKey)) {
|
|
65
|
+
this.dailyChannelMessageAuthors.set(chanKey, new Set());
|
|
66
|
+
}
|
|
67
|
+
this.dailyChannelMessageAuthors.get(chanKey).add(authorId);
|
|
68
|
+
}
|
|
69
|
+
if (existing) {
|
|
70
|
+
await repo.update({ id: chanKey }, {
|
|
71
|
+
message_count: existing.message_count + 1,
|
|
72
|
+
unique_authors: this.dailyChannelMessageAuthors.get(chanKey)?.size || 0,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
await repo.insert({
|
|
77
|
+
id: chanKey,
|
|
78
|
+
guild_id: guildId,
|
|
79
|
+
channel_id: channelId,
|
|
80
|
+
date,
|
|
81
|
+
message_count: 1,
|
|
82
|
+
unique_authors: 0,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
46
86
|
}
|
|
47
87
|
async recordMemberJoin(guildId) {
|
|
48
88
|
const config = await this.ensuredConfig(guildId);
|
|
@@ -374,8 +414,17 @@ export class AnalyticsCollector {
|
|
|
374
414
|
error_count: data.errors,
|
|
375
415
|
avgExecutionTimeMs: data.usage_count > 0 ? Math.round(data.totalTime / data.usage_count) : 0,
|
|
376
416
|
}))
|
|
377
|
-
.
|
|
378
|
-
|
|
417
|
+
.sort((a, b) => {
|
|
418
|
+
const aHasPerf = a.total_execution_time_ms > 0;
|
|
419
|
+
const bHasPerf = b.total_execution_time_ms > 0;
|
|
420
|
+
if (aHasPerf && bHasPerf)
|
|
421
|
+
return b.avgExecutionTimeMs - a.avgExecutionTimeMs;
|
|
422
|
+
if (aHasPerf)
|
|
423
|
+
return -1;
|
|
424
|
+
if (bHasPerf)
|
|
425
|
+
return 1;
|
|
426
|
+
return b.usage_count - a.usage_count;
|
|
427
|
+
})
|
|
379
428
|
.slice(0, limit);
|
|
380
429
|
}
|
|
381
430
|
async getVoiceChannelStats(guildId, daysBack) {
|
|
@@ -387,6 +436,15 @@ export class AnalyticsCollector {
|
|
|
387
436
|
.sort('date', 'asc')
|
|
388
437
|
.exec();
|
|
389
438
|
}
|
|
439
|
+
async getChannelMessageStats(guildId, daysBack) {
|
|
440
|
+
const repo = this.repo(ChannelMessageStats);
|
|
441
|
+
const dateMin = this.dateDaysAgo(daysBack);
|
|
442
|
+
return repo.query()
|
|
443
|
+
.where('guild_id', 'eq', guildId)
|
|
444
|
+
.where('date', 'gte', dateMin)
|
|
445
|
+
.sort('date', 'asc')
|
|
446
|
+
.exec();
|
|
447
|
+
}
|
|
390
448
|
// ── Config ───────────────────────────────────────────────────
|
|
391
449
|
async getConfig(guildId) {
|
|
392
450
|
return this.ensuredConfig(guildId);
|