Skip to content

Commit bea8234

Browse files
committed
feat: add analytics and operations modules for user and moderation action management
1 parent f9fd99b commit bea8234

2 files changed

Lines changed: 281 additions & 0 deletions

File tree

src/database/analytics.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import {
2+
ActionStatus,
3+
type ActionType,
4+
type ModerationAction,
5+
} from "../../generated/prisma/index.js";
6+
import { prisma } from "./operations.js";
7+
8+
export async function getUserActionStats(discordId: string) {
9+
const user = await prisma.user.findUnique({
10+
where: { discordId },
11+
include: {
12+
actionsReceived: {
13+
where: {
14+
status: { in: [ActionStatus.ACTIVE, ActionStatus.EXPIRED] },
15+
},
16+
},
17+
},
18+
});
19+
20+
if (!user) {
21+
return null;
22+
}
23+
24+
const actionsByType = user.actionsReceived.reduce(
25+
(acc, action) => {
26+
acc[action.type] = (acc[action.type] || 0) + 1;
27+
return acc;
28+
},
29+
{} as Record<ActionType, number>
30+
);
31+
32+
return {
33+
discordId: user.discordId,
34+
totalActions: user.actionsReceived.length,
35+
actionsByType,
36+
};
37+
}
38+
39+
export async function getModeratorStats(discordId: string, days = 30) {
40+
const user = await prisma.user.findUnique({
41+
where: { discordId },
42+
});
43+
44+
if (!user) {
45+
return null;
46+
}
47+
48+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
49+
50+
const actions = await prisma.moderationAction.findMany({
51+
where: {
52+
moderatorId: user.id,
53+
createdAt: { gte: since },
54+
status: { not: ActionStatus.REMOVED_BY_ERROR },
55+
},
56+
});
57+
58+
const actionsByType = actions.reduce(
59+
(acc, action) => {
60+
acc[action.type] = (acc[action.type] || 0) + 1;
61+
return acc;
62+
},
63+
{} as Record<ActionType, number>
64+
);
65+
66+
return {
67+
discordId: user.discordId,
68+
totalActions: actions.length,
69+
actionsByType,
70+
period: `${days} days`,
71+
};
72+
}
73+
74+
export async function getRecentActions(limit = 20) {
75+
return await prisma.moderationAction.findMany({
76+
include: {
77+
target: true,
78+
moderator: true,
79+
},
80+
orderBy: { createdAt: "desc" },
81+
take: limit,
82+
});
83+
}
84+
85+
export async function findRepeatOffenders(minActions = 3) {
86+
const actions = await prisma.moderationAction.findMany({
87+
where: {
88+
status: { in: [ActionStatus.ACTIVE, ActionStatus.EXPIRED] },
89+
},
90+
include: {
91+
target: true,
92+
},
93+
});
94+
95+
const userActionCounts = actions.reduce(
96+
(acc, action) => {
97+
const discordId = action.target.discordId;
98+
if (!acc[discordId]) {
99+
acc[discordId] = { discordId, count: 0, actions: [] };
100+
}
101+
acc[discordId].count++;
102+
acc[discordId].actions.push(action);
103+
return acc;
104+
},
105+
{} as Record<string, { discordId: string; count: number; actions: ModerationAction[] }>
106+
);
107+
108+
return Object.values(userActionCounts)
109+
.filter((item) => item.count >= minActions)
110+
.sort((a, b) => b.count - a.count);
111+
}
112+
113+
export async function getActionsByType(actionType: ActionType, limit = 50) {
114+
return await prisma.moderationAction.findMany({
115+
where: { type: actionType },
116+
include: {
117+
target: true,
118+
moderator: true,
119+
},
120+
orderBy: { createdAt: "desc" },
121+
take: limit,
122+
});
123+
}
124+
125+
export async function getTotalActionCount() {
126+
return await prisma.moderationAction.count({
127+
where: {
128+
status: { not: ActionStatus.REMOVED_BY_ERROR },
129+
},
130+
});
131+
}

src/database/operations.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { ActionStatus, type ActionType, PrismaClient } from "../../generated/prisma/index.js";
2+
3+
export const prisma = new PrismaClient();
4+
5+
// Connect to database and verify connection
6+
export async function connectDatabase() {
7+
try {
8+
await prisma.$connect();
9+
// Test query to verify connection
10+
await prisma.user.findFirst();
11+
console.log("✅ Database connected successfully");
12+
} catch (error) {
13+
console.error("❌ Database connection failed:", error);
14+
throw error;
15+
}
16+
}
17+
18+
// Disconnect from database
19+
export async function disconnectDatabase() {
20+
await prisma.$disconnect();
21+
console.log("Database disconnected");
22+
}
23+
24+
// User operations - only Discord ID needed
25+
export async function upsertUser(discordId: string) {
26+
return await prisma.user.upsert({
27+
where: { discordId },
28+
update: {},
29+
create: { discordId },
30+
});
31+
}
32+
33+
export async function getUserByDiscordId(discordId: string) {
34+
return await prisma.user.findUnique({
35+
where: { discordId },
36+
});
37+
}
38+
39+
// Moderation action operations
40+
type CreateActionParams = {
41+
type: ActionType;
42+
targetDiscordId: string;
43+
moderatorDiscordId: string;
44+
reason?: string;
45+
duration?: number;
46+
};
47+
48+
export async function createModerationAction(params: CreateActionParams) {
49+
// Ensure both users exist
50+
const target = await upsertUser(params.targetDiscordId);
51+
const moderator = await upsertUser(params.moderatorDiscordId);
52+
53+
// Calculate expiry for timed actions
54+
const expiresAt = params.duration ? new Date(Date.now() + params.duration * 1000) : null;
55+
56+
return await prisma.moderationAction.create({
57+
data: {
58+
type: params.type,
59+
reason: params.reason,
60+
duration: params.duration,
61+
targetId: target.id,
62+
moderatorId: moderator.id,
63+
expiresAt,
64+
},
65+
include: {
66+
target: true,
67+
moderator: true,
68+
},
69+
});
70+
}
71+
72+
export async function removeActionByError(
73+
actionId: number,
74+
removedByDiscordId: string,
75+
reason: string
76+
) {
77+
const removedBy = await upsertUser(removedByDiscordId);
78+
79+
// Mark original action as removed
80+
const originalAction = await prisma.moderationAction.update({
81+
where: { id: actionId },
82+
data: { status: ActionStatus.REMOVED_BY_ERROR },
83+
});
84+
85+
// Create correction record
86+
return await prisma.moderationAction.create({
87+
data: {
88+
type: originalAction.type,
89+
status: ActionStatus.REMOVED_BY_ERROR,
90+
reason: `Correction: ${reason}`,
91+
targetId: originalAction.targetId,
92+
moderatorId: removedBy.id,
93+
parentActionId: originalAction.id,
94+
},
95+
include: {
96+
parentAction: true,
97+
},
98+
});
99+
}
100+
101+
export async function getUserActions(discordId: string, limit = 50) {
102+
const user = await getUserByDiscordId(discordId);
103+
if (!user) {
104+
return [];
105+
}
106+
107+
return await prisma.moderationAction.findMany({
108+
where: { targetId: user.id },
109+
include: {
110+
moderator: true,
111+
},
112+
orderBy: { createdAt: "desc" },
113+
take: limit,
114+
});
115+
}
116+
117+
export async function getActiveActions(discordId: string) {
118+
const user = await getUserByDiscordId(discordId);
119+
if (!user) {
120+
return [];
121+
}
122+
123+
return await prisma.moderationAction.findMany({
124+
where: {
125+
targetId: user.id,
126+
status: ActionStatus.ACTIVE,
127+
},
128+
include: {
129+
moderator: true,
130+
},
131+
});
132+
}
133+
134+
export async function getActionById(actionId: number) {
135+
return await prisma.moderationAction.findUnique({
136+
where: { id: actionId },
137+
include: {
138+
target: true,
139+
moderator: true,
140+
parentAction: true,
141+
},
142+
});
143+
}
144+
145+
export async function updateActionStatus(actionId: number, status: ActionStatus) {
146+
return await prisma.moderationAction.update({
147+
where: { id: actionId },
148+
data: { status },
149+
});
150+
}

0 commit comments

Comments
 (0)