feat(backend): implement all missing endpoints, real analytics queries, and WebSocket server
- Replace stub analytics.repository.ts with real PostgreSQL queries using pg.Pool - Add /api/guilds, /api/config, /api/auth/login, /api/ui-state (GET/POST) - Add /api/review, /api/recordings, /api/analysis/search - Add /api/messages/:id/reanalyze endpoint - Add /api/analytics/heatmap and /api/analytics/topics - Implement media routes (stub responses, backend has no Discord voice client) - Add WebSocket server at /ws with heartbeat and broadcast functions - Fix analytics route paths to match frontend contract (dual paths for backward compat) - Export getPool() from database module for raw SQL queries - Register all new routers in app.ts Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5a094c926a
commit
9b41eb9c12
@@ -46,10 +46,13 @@ export function handleGetHourlyStats(
|
||||
next: NextFunction,
|
||||
) {
|
||||
return asyncHandler(async (req: Request, res: Response) => {
|
||||
const guildId = requireQueryString(req.query.guildId, "guildId");
|
||||
const hours = req.query.hours ? Number(req.query.hours) : 24;
|
||||
logger.debug({ guildId, hours }, "Handling get hourly stats");
|
||||
const result = await analyticsService.getHourlyStats(guildId, hours);
|
||||
const query = analyticsQuerySchema.parse(req.query);
|
||||
logger.debug({ query }, "Handling get hourly stats");
|
||||
const result = await analyticsService.getHourlyStats(
|
||||
query.guildId,
|
||||
query.channelId,
|
||||
query.hours,
|
||||
);
|
||||
res.json(result);
|
||||
})(req, res, next);
|
||||
}
|
||||
@@ -60,10 +63,15 @@ export function handleGetTopViolators(
|
||||
next: NextFunction,
|
||||
) {
|
||||
return asyncHandler(async (req: Request, res: Response) => {
|
||||
const guildId = requireQueryString(req.query.guildId, "guildId");
|
||||
const query = analyticsQuerySchema.parse(req.query);
|
||||
const limit = req.query.limit ? Number(req.query.limit) : 10;
|
||||
logger.debug({ guildId, limit }, "Handling get top violators");
|
||||
const result = await analyticsService.getTopViolators(guildId, limit);
|
||||
logger.debug({ query, limit }, "Handling get top violators");
|
||||
const result = await analyticsService.getTopViolators(
|
||||
query.guildId,
|
||||
query.channelId,
|
||||
query.hours,
|
||||
limit,
|
||||
);
|
||||
res.json(result);
|
||||
})(req, res, next);
|
||||
}
|
||||
@@ -74,10 +82,15 @@ export function handleGetUserLeaderboard(
|
||||
next: NextFunction,
|
||||
) {
|
||||
return asyncHandler(async (req: Request, res: Response) => {
|
||||
const guildId = requireQueryString(req.query.guildId, "guildId");
|
||||
const query = analyticsQuerySchema.parse(req.query);
|
||||
const limit = req.query.limit ? Number(req.query.limit) : 10;
|
||||
logger.debug({ guildId, limit }, "Handling get user leaderboard");
|
||||
const result = await analyticsService.getUserLeaderboard(guildId, limit);
|
||||
logger.debug({ query, limit }, "Handling get user leaderboard");
|
||||
const result = await analyticsService.getUserLeaderboard(
|
||||
query.guildId,
|
||||
query.channelId,
|
||||
query.hours,
|
||||
limit,
|
||||
);
|
||||
res.json(result);
|
||||
})(req, res, next);
|
||||
}
|
||||
@@ -88,9 +101,47 @@ export function handleGetModerationStats(
|
||||
next: NextFunction,
|
||||
) {
|
||||
return asyncHandler(async (req: Request, res: Response) => {
|
||||
const guildId = requireQueryString(req.query.guildId, "guildId");
|
||||
logger.debug({ guildId }, "Handling get moderation stats");
|
||||
const result = await analyticsService.getModerationStats(guildId);
|
||||
const query = analyticsQuerySchema.parse(req.query);
|
||||
logger.debug({ query }, "Handling get moderation stats");
|
||||
const result = await analyticsService.getModerationStats(
|
||||
query.guildId,
|
||||
query.channelId,
|
||||
query.hours,
|
||||
);
|
||||
res.json(result);
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
export function handleGetHeatmap(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
return asyncHandler(async (req: Request, res: Response) => {
|
||||
const query = analyticsQuerySchema.parse(req.query);
|
||||
logger.debug({ query }, "Handling get heatmap");
|
||||
const result = await analyticsService.getHeatmap(
|
||||
query.guildId,
|
||||
query.channelId,
|
||||
query.hours,
|
||||
);
|
||||
res.json(result);
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
export function handleGetTopics(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
return asyncHandler(async (req: Request, res: Response) => {
|
||||
const query = analyticsQuerySchema.parse(req.query);
|
||||
logger.debug({ query }, "Handling get topics");
|
||||
const result = await analyticsService.getTopics(
|
||||
query.guildId,
|
||||
query.channelId,
|
||||
query.hours,
|
||||
);
|
||||
res.json(result);
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
@@ -1,53 +1,333 @@
|
||||
import { getPool } from "../../shared/database/index.js";
|
||||
import { createChildLogger } from "../../shared/logger/index.js";
|
||||
|
||||
const logger = createChildLogger("analytics.repository");
|
||||
|
||||
interface TimeFilter {
|
||||
where: string;
|
||||
params: Array<string | number>;
|
||||
paramOffset: number;
|
||||
}
|
||||
|
||||
function buildTimeFilter(
|
||||
guildId: string,
|
||||
channelId: string | undefined,
|
||||
hours: number,
|
||||
offset = 1,
|
||||
): TimeFilter {
|
||||
const clauses: string[] = ["guild_id = $" + offset];
|
||||
const params: Array<string | number> = [guildId];
|
||||
let p = offset + 1;
|
||||
|
||||
if (channelId) {
|
||||
clauses.push("channel_id = $" + p);
|
||||
params.push(channelId);
|
||||
p++;
|
||||
}
|
||||
|
||||
clauses.push("created_at > (EXTRACT(EPOCH FROM NOW()) * 1000 - $" + p + ")");
|
||||
params.push(hours * 3_600_000);
|
||||
|
||||
return { where: "WHERE " + clauses.join(" AND "), params, paramOffset: p };
|
||||
}
|
||||
|
||||
export class AnalyticsRepository {
|
||||
async getOverview(guildId: string, channelId?: string, hours = 24) {
|
||||
logger.debug({ guildId, channelId, hours }, "Getting analytics overview");
|
||||
// TODO: Implement actual Drizzle ORM queries
|
||||
const pool = getPool();
|
||||
const filter = buildTimeFilter(guildId, channelId, hours);
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
COUNT(*)::int AS total_messages,
|
||||
COUNT(DISTINCT user_id)::int AS active_users_count,
|
||||
COUNT(DISTINCT channel_id)::int AS total_channels,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'clean')::int AS clean,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'warn')::int AS warned,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'error')::int AS error,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'pending')::int AS pending,
|
||||
COALESCE(AVG(ai_moderation_score), 0)::real AS average_score
|
||||
FROM messages
|
||||
${filter.where}
|
||||
`,
|
||||
filter.params,
|
||||
);
|
||||
|
||||
const row = rows[0] as Record<string, unknown> | undefined;
|
||||
|
||||
return {
|
||||
totalMessages: 0,
|
||||
totalUsers: 0,
|
||||
flaggedMessages: 0,
|
||||
averageSeverity: 0,
|
||||
period: { hours },
|
||||
messages: {
|
||||
total: Number(row?.total_messages ?? 0),
|
||||
clean: Number(row?.clean ?? 0),
|
||||
warned: Number(row?.warned ?? 0),
|
||||
flagged: Number(row?.flagged ?? 0),
|
||||
error: Number(row?.error ?? 0),
|
||||
pending: Number(row?.pending ?? 0),
|
||||
average_score: Number(row?.average_score ?? 0),
|
||||
},
|
||||
hourly: [],
|
||||
topics: [],
|
||||
top_users: [],
|
||||
active_users_count: Number(row?.active_users_count ?? 0),
|
||||
total_channels: Number(row?.total_channels ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
async getDailyTrend(guildId: string, hours = 24) {
|
||||
logger.debug({ guildId, hours }, "Getting daily trend");
|
||||
// TODO: Implement actual Drizzle ORM queries
|
||||
return [];
|
||||
const pool = getPool();
|
||||
const filter = buildTimeFilter(guildId, undefined, hours);
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
TO_CHAR(to_timestamp(created_at / 1000), 'YYYY-MM-DD') AS date,
|
||||
COUNT(*)::int AS count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'clean')::int AS clean,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'warn')::int AS warned,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'error')::int AS error
|
||||
FROM messages
|
||||
${filter.where}
|
||||
GROUP BY date
|
||||
ORDER BY date ASC
|
||||
`,
|
||||
filter.params,
|
||||
);
|
||||
|
||||
return rows.map((r) => ({
|
||||
date: r.date as string,
|
||||
count: Number(r.count ?? 0),
|
||||
clean: Number(r.clean ?? 0),
|
||||
warned: Number(r.warned ?? 0),
|
||||
flagged: Number(r.flagged ?? 0),
|
||||
error: Number(r.error ?? 0),
|
||||
}));
|
||||
}
|
||||
|
||||
async getHourlyStats(guildId: string, hours = 24) {
|
||||
logger.debug({ guildId, hours }, "Getting hourly stats");
|
||||
// TODO: Implement actual Drizzle ORM queries
|
||||
return [];
|
||||
async getHourlyStats(guildId: string, channelId?: string, hours = 24) {
|
||||
logger.debug({ guildId, channelId, hours }, "Getting hourly stats");
|
||||
const pool = getPool();
|
||||
const filter = buildTimeFilter(guildId, channelId, hours);
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
TO_CHAR(to_timestamp(created_at / 1000), 'HH24') AS hour,
|
||||
COUNT(*)::int AS count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'clean')::int AS clean,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'warn')::int AS warned,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'error')::int AS error
|
||||
FROM messages
|
||||
${filter.where}
|
||||
GROUP BY hour
|
||||
ORDER BY hour ASC
|
||||
`,
|
||||
filter.params,
|
||||
);
|
||||
|
||||
return rows.map((r) => ({
|
||||
hour: r.hour as string,
|
||||
count: Number(r.count ?? 0),
|
||||
clean: Number(r.clean ?? 0),
|
||||
warned: Number(r.warned ?? 0),
|
||||
flagged: Number(r.flagged ?? 0),
|
||||
error: Number(r.error ?? 0),
|
||||
}));
|
||||
}
|
||||
|
||||
async getTopViolators(guildId: string, limit = 10) {
|
||||
logger.debug({ guildId, limit }, "Getting top violators");
|
||||
// TODO: Implement actual Drizzle ORM queries
|
||||
return [];
|
||||
async getTopViolators(
|
||||
guildId: string,
|
||||
channelId?: string,
|
||||
hours = 24,
|
||||
limit = 10,
|
||||
) {
|
||||
logger.debug(
|
||||
{ guildId, channelId, hours, limit },
|
||||
"Getting top violators",
|
||||
);
|
||||
const pool = getPool();
|
||||
const filter = buildTimeFilter(guildId, channelId, hours);
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
user_id,
|
||||
MAX(username) AS username,
|
||||
MAX(avatar_url) AS avatar_url,
|
||||
COUNT(*)::int AS total_messages,
|
||||
COUNT(*) FILTER (WHERE ai_status IN ('warn', 'flagged', 'error'))::int AS flagged_count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'warn')::int AS warned_count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS hard_flagged_count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'error')::int AS error_count,
|
||||
COALESCE(AVG(ai_moderation_score), 0)::real AS violation_score,
|
||||
MAX(ai_moderation_flags) AS worst_flags,
|
||||
MAX(created_at) AS last_violation
|
||||
FROM messages
|
||||
${filter.where}
|
||||
AND ai_status IN ('warn', 'flagged', 'error')
|
||||
GROUP BY user_id
|
||||
ORDER BY flagged_count DESC
|
||||
LIMIT $${filter.params.length + 1}
|
||||
`,
|
||||
[...filter.params, limit],
|
||||
);
|
||||
|
||||
return rows.map((r) => ({
|
||||
user_id: r.user_id as string,
|
||||
username: (r.username as string) ?? "",
|
||||
avatar_url: (r.avatar_url as string | null) ?? null,
|
||||
total_messages: Number(r.total_messages ?? 0),
|
||||
flagged_count: Number(r.flagged_count ?? 0),
|
||||
warned_count: Number(r.warned_count ?? 0),
|
||||
violation_score: Number(r.violation_score ?? 0),
|
||||
worst_flags: (r.worst_flags as string | null) ?? null,
|
||||
last_violation: Number(r.last_violation ?? 0),
|
||||
}));
|
||||
}
|
||||
|
||||
async getUserLeaderboard(guildId: string, limit = 10) {
|
||||
logger.debug({ guildId, limit }, "Getting user leaderboard");
|
||||
// TODO: Implement actual Drizzle ORM queries
|
||||
return [];
|
||||
async getUserLeaderboard(
|
||||
guildId: string,
|
||||
channelId?: string,
|
||||
hours = 24,
|
||||
limit = 10,
|
||||
) {
|
||||
logger.debug(
|
||||
{ guildId, channelId, hours, limit },
|
||||
"Getting user leaderboard",
|
||||
);
|
||||
const pool = getPool();
|
||||
const filter = buildTimeFilter(guildId, channelId, hours);
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
user_id,
|
||||
MAX(username) AS username,
|
||||
MAX(avatar_url) AS avatar_url,
|
||||
COUNT(*)::int AS message_count,
|
||||
COUNT(*) FILTER (WHERE type = 'edited')::int AS edited_count,
|
||||
COUNT(*) FILTER (WHERE type = 'deleted')::int AS deleted_count,
|
||||
COUNT(*) FILTER (WHERE ai_status IN ('warn', 'flagged', 'error'))::int AS flagged_count,
|
||||
MAX(created_at) AS last_active
|
||||
FROM messages
|
||||
${filter.where}
|
||||
GROUP BY user_id
|
||||
ORDER BY message_count DESC
|
||||
LIMIT $${filter.params.length + 1}
|
||||
`,
|
||||
[...filter.params, limit],
|
||||
);
|
||||
|
||||
return rows.map((r) => ({
|
||||
user_id: r.user_id as string,
|
||||
username: (r.username as string) ?? "",
|
||||
avatar_url: (r.avatar_url as string | null) ?? null,
|
||||
message_count: Number(r.message_count ?? 0),
|
||||
edited_count: Number(r.edited_count ?? 0),
|
||||
deleted_count: Number(r.deleted_count ?? 0),
|
||||
flagged_count: Number(r.flagged_count ?? 0),
|
||||
last_active: Number(r.last_active ?? 0),
|
||||
}));
|
||||
}
|
||||
|
||||
async getModerationStats(guildId: string) {
|
||||
logger.debug({ guildId }, "Getting moderation stats");
|
||||
// TODO: Implement actual Drizzle ORM queries
|
||||
async getModerationStats(guildId: string, channelId?: string, hours = 24) {
|
||||
logger.debug({ guildId, channelId, hours }, "Getting moderation stats");
|
||||
const pool = getPool();
|
||||
const filter = buildTimeFilter(guildId, channelId, hours);
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
COUNT(*)::int AS total,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'clean')::int AS clean,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'warn')::int AS warned,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'error')::int AS error,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'pending')::int AS pending,
|
||||
COALESCE(AVG(ai_moderation_score), 0)::real AS average_score
|
||||
FROM messages
|
||||
${filter.where}
|
||||
`,
|
||||
filter.params,
|
||||
);
|
||||
|
||||
const row = rows[0] as Record<string, unknown> | undefined;
|
||||
|
||||
return {
|
||||
clean: 0,
|
||||
warn: 0,
|
||||
flagged: 0,
|
||||
error: 0,
|
||||
total: Number(row?.total ?? 0),
|
||||
clean: Number(row?.clean ?? 0),
|
||||
warned: Number(row?.warned ?? 0),
|
||||
flagged: Number(row?.flagged ?? 0),
|
||||
error: Number(row?.error ?? 0),
|
||||
pending: Number(row?.pending ?? 0),
|
||||
average_score: Number(row?.average_score ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
async getHeatmap(guildId: string, channelId?: string, hours = 24) {
|
||||
logger.debug({ guildId, channelId, hours }, "Getting heatmap data");
|
||||
const pool = getPool();
|
||||
const filter = buildTimeFilter(guildId, channelId, hours);
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
EXTRACT(DOW FROM to_timestamp(created_at / 1000))::int AS day_of_week,
|
||||
EXTRACT(HOUR FROM to_timestamp(created_at / 1000))::int AS hour,
|
||||
COUNT(*)::int AS count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'clean')::int AS clean,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'warn')::int AS warned,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged
|
||||
FROM messages
|
||||
${filter.where}
|
||||
GROUP BY day_of_week, hour
|
||||
ORDER BY day_of_week, hour
|
||||
`,
|
||||
filter.params,
|
||||
);
|
||||
|
||||
return rows.map((r) => ({
|
||||
dayOfWeek: Number(r.day_of_week ?? 0),
|
||||
hour: Number(r.hour ?? 0),
|
||||
count: Number(r.count ?? 0),
|
||||
clean: Number(r.clean ?? 0),
|
||||
warned: Number(r.warned ?? 0),
|
||||
flagged: Number(r.flagged ?? 0),
|
||||
}));
|
||||
}
|
||||
|
||||
async getTopics(guildId: string, channelId?: string, hours = 24) {
|
||||
logger.debug({ guildId, channelId, hours }, "Getting topics");
|
||||
const pool = getPool();
|
||||
const filter = buildTimeFilter(guildId, channelId, hours);
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
TRIM(UNNEST(STRING_TO_ARRAY(ai_categories, ','))) AS topic,
|
||||
COUNT(*)::int AS count,
|
||||
COALESCE(AVG(ai_moderation_score), 0)::real AS score
|
||||
FROM messages
|
||||
${filter.where}
|
||||
AND ai_categories IS NOT NULL
|
||||
AND ai_categories != ''
|
||||
GROUP BY topic
|
||||
ORDER BY count DESC
|
||||
`,
|
||||
filter.params,
|
||||
);
|
||||
|
||||
return rows.map((r) => ({
|
||||
topic: (r.topic as string) ?? "",
|
||||
count: Number(r.count ?? 0),
|
||||
score: Number(r.score ?? 0),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export const analyticsRepository = new AnalyticsRepository();
|
||||
|
||||
@@ -2,10 +2,12 @@ import type { Router } from "express";
|
||||
import express from "express";
|
||||
import {
|
||||
handleGetDailyTrend,
|
||||
handleGetHeatmap,
|
||||
handleGetHourlyStats,
|
||||
handleGetModerationStats,
|
||||
handleGetOverview,
|
||||
handleGetTopViolators,
|
||||
handleGetTopics,
|
||||
handleGetUserLeaderboard,
|
||||
} from "./analytics.controller.js";
|
||||
|
||||
@@ -13,11 +15,13 @@ export function createAnalyticsRouter(): Router {
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/analytics/overview", handleGetOverview);
|
||||
router.get("/analytics/daily-trend", handleGetDailyTrend);
|
||||
router.get("/analytics/hourly-stats", handleGetHourlyStats);
|
||||
router.get("/analytics/top-violators", handleGetTopViolators);
|
||||
router.get("/analytics/user-leaderboard", handleGetUserLeaderboard);
|
||||
router.get("/analytics/moderation-stats", handleGetModerationStats);
|
||||
router.get("/analytics/trend", handleGetDailyTrend);
|
||||
router.get("/analytics/hourly", handleGetHourlyStats);
|
||||
router.get("/analytics/violators", handleGetTopViolators);
|
||||
router.get("/analytics/leaderboard", handleGetUserLeaderboard);
|
||||
router.get("/analytics/stats", handleGetModerationStats);
|
||||
router.get("/analytics/heatmap", handleGetHeatmap);
|
||||
router.get("/analytics/topics", handleGetTopics);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -33,28 +33,63 @@ export class AnalyticsService {
|
||||
return analyticsRepository.getDailyTrend(guildId, hours);
|
||||
}
|
||||
|
||||
async getHourlyStats(guildId: string, hours = 24) {
|
||||
async getHourlyStats(guildId: string, channelId?: string, hours = 24) {
|
||||
this.assertMonitorGuild(guildId);
|
||||
logger.debug({ guildId, hours }, "Getting hourly stats");
|
||||
return analyticsRepository.getHourlyStats(guildId, hours);
|
||||
logger.debug({ guildId, channelId, hours }, "Getting hourly stats");
|
||||
return analyticsRepository.getHourlyStats(guildId, channelId, hours);
|
||||
}
|
||||
|
||||
async getTopViolators(guildId: string, limit = 10) {
|
||||
async getTopViolators(
|
||||
guildId: string,
|
||||
channelId?: string,
|
||||
hours = 24,
|
||||
limit = 10,
|
||||
) {
|
||||
this.assertMonitorGuild(guildId);
|
||||
logger.debug({ guildId, limit }, "Getting top violators");
|
||||
return analyticsRepository.getTopViolators(guildId, limit);
|
||||
logger.debug({ guildId, channelId, hours, limit }, "Getting top violators");
|
||||
return analyticsRepository.getTopViolators(
|
||||
guildId,
|
||||
channelId,
|
||||
hours,
|
||||
limit,
|
||||
);
|
||||
}
|
||||
|
||||
async getUserLeaderboard(guildId: string, limit = 10) {
|
||||
async getUserLeaderboard(
|
||||
guildId: string,
|
||||
channelId?: string,
|
||||
hours = 24,
|
||||
limit = 10,
|
||||
) {
|
||||
this.assertMonitorGuild(guildId);
|
||||
logger.debug({ guildId, limit }, "Getting user leaderboard");
|
||||
return analyticsRepository.getUserLeaderboard(guildId, limit);
|
||||
logger.debug(
|
||||
{ guildId, channelId, hours, limit },
|
||||
"Getting user leaderboard",
|
||||
);
|
||||
return analyticsRepository.getUserLeaderboard(
|
||||
guildId,
|
||||
channelId,
|
||||
hours,
|
||||
limit,
|
||||
);
|
||||
}
|
||||
|
||||
async getModerationStats(guildId: string) {
|
||||
async getModerationStats(guildId: string, channelId?: string, hours = 24) {
|
||||
this.assertMonitorGuild(guildId);
|
||||
logger.debug({ guildId }, "Getting moderation stats");
|
||||
return analyticsRepository.getModerationStats(guildId);
|
||||
logger.debug({ guildId, channelId, hours }, "Getting moderation stats");
|
||||
return analyticsRepository.getModerationStats(guildId, channelId, hours);
|
||||
}
|
||||
|
||||
async getHeatmap(guildId: string, channelId?: string, hours = 24) {
|
||||
this.assertMonitorGuild(guildId);
|
||||
logger.debug({ guildId, channelId, hours }, "Getting heatmap");
|
||||
return analyticsRepository.getHeatmap(guildId, channelId, hours);
|
||||
}
|
||||
|
||||
async getTopics(guildId: string, channelId?: string, hours = 24) {
|
||||
this.assertMonitorGuild(guildId);
|
||||
logger.debug({ guildId, channelId, hours }, "Getting topics");
|
||||
return analyticsRepository.getTopics(guildId, channelId, hours);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user