Aethel Bot OSS repository!
aethel.xyz
bot
fun
ai
discord
discord-bot
aethel
1import pgClient from './pgClient';
2import logger from './logger';
3
4export interface StrikeInfo {
5 strike_count: number;
6 banned_until: Date | null;
7}
8
9export class StrikeError extends Error {
10 constructor(
11 message: string,
12 public readonly userId?: string,
13 ) {
14 super(message);
15 this.name = 'StrikeError';
16 }
17}
18
19export async function getUserStrikeInfo(userId: string): Promise<StrikeInfo | null> {
20 if (!userId || typeof userId !== 'string') {
21 throw new StrikeError('Invalid user ID provided');
22 }
23
24 try {
25 const res = await pgClient.query(
26 'SELECT strike_count, banned_until FROM user_strikes WHERE user_id = $1',
27 [userId],
28 );
29
30 if (res.rows.length === 0) {
31 return null;
32 }
33
34 return {
35 strike_count: res.rows[0].strike_count,
36 banned_until: res.rows[0].banned_until ? new Date(res.rows[0].banned_until) : null,
37 };
38 } catch (error) {
39 logger.error('Failed to get user strike info', { userId, error });
40 throw new StrikeError('Failed to retrieve strike information', userId);
41 }
42}
43
44export async function incrementUserStrike(userId: string): Promise<StrikeInfo> {
45 if (!userId || typeof userId !== 'string') {
46 throw new StrikeError('Invalid user ID provided');
47 }
48
49 try {
50 const res = await pgClient.query(
51 `
52 INSERT INTO user_strikes (user_id, strike_count, last_strike_at)
53 VALUES ($1, 1, NOW())
54 ON CONFLICT (user_id) DO UPDATE SET
55 strike_count = user_strikes.strike_count + 1,
56 last_strike_at = NOW()
57 RETURNING strike_count, banned_until;
58 `,
59 [userId],
60 );
61
62 if (res.rows.length === 0) {
63 throw new StrikeError('Failed to increment strike - no rows returned', userId);
64 }
65
66 const { strike_count, banned_until } = res.rows[0];
67
68 if (strike_count >= 5 && (!banned_until || new Date(banned_until) < new Date())) {
69 try {
70 const banUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
71 await pgClient.query('UPDATE user_strikes SET banned_until = $2 WHERE user_id = $1', [
72 userId,
73 banUntil,
74 ]);
75
76 logger.warn('User auto-banned due to strike limit', {
77 userId,
78 strikeCount: strike_count,
79 banUntil: banUntil.toISOString(),
80 });
81
82 return {
83 strike_count,
84 banned_until: banUntil,
85 };
86 } catch (banError) {
87 logger.error('Failed to apply auto-ban', { userId, error: banError });
88 return {
89 strike_count,
90 banned_until: banned_until ? new Date(banned_until) : null,
91 };
92 }
93 }
94
95 logger.info('User strike incremented', { userId, strikeCount: strike_count });
96
97 return {
98 strike_count,
99 banned_until: banned_until ? new Date(banned_until) : null,
100 };
101 } catch (error) {
102 logger.error('Failed to increment user strike', { userId, error });
103 throw new StrikeError('Failed to increment strike count', userId);
104 }
105}
106
107export async function isUserBanned(userId: string): Promise<Date | null> {
108 if (!userId || typeof userId !== 'string') {
109 throw new StrikeError('Invalid user ID provided');
110 }
111
112 try {
113 const res = await pgClient.query('SELECT banned_until FROM user_strikes WHERE user_id = $1', [
114 userId,
115 ]);
116
117 if (res.rows.length === 0 || !res.rows[0].banned_until) {
118 return null;
119 }
120
121 const banUntil = new Date(res.rows[0].banned_until);
122
123 if (banUntil > new Date()) {
124 return banUntil;
125 }
126
127 return null;
128 } catch (error) {
129 logger.error('Failed to check user ban status', { userId, error });
130 throw new StrikeError('Failed to check ban status', userId);
131 }
132}
133
134export async function resetOldStrikes(): Promise<number> {
135 try {
136 const res = await pgClient.query(
137 `UPDATE user_strikes
138 SET strike_count = 0, banned_until = NULL
139 WHERE last_strike_at < NOW() - INTERVAL '3 days'
140 RETURNING user_id`,
141 );
142
143 const resetCount = res.rows.length;
144
145 if (resetCount > 0) {
146 logger.info('Reset old strikes', { resetCount });
147 }
148
149 return resetCount;
150 } catch (error) {
151 logger.error('Failed to reset old strikes', { error });
152 throw new StrikeError('Failed to reset old strikes');
153 }
154}
155
156export async function clearUserStrikes(userId: string): Promise<boolean> {
157 if (!userId || typeof userId !== 'string') {
158 throw new StrikeError('Invalid user ID provided');
159 }
160
161 try {
162 const res = await pgClient.query(
163 'UPDATE user_strikes SET strike_count = 0, banned_until = NULL WHERE user_id = $1',
164 [userId],
165 );
166
167 logger.info('Cleared user strikes', { userId });
168 return (res.rowCount ?? 0) > 0;
169 } catch (error) {
170 logger.error('Failed to clear user strikes', { userId, error });
171 throw new StrikeError('Failed to clear strikes', userId);
172 }
173}