Aethel Bot OSS repository!
aethel.xyz
bot
fun
ai
discord
discord-bot
aethel
1import pool from './pgClient';
2import logger from './logger';
3
4interface ReminderData {
5 reminder_id: string;
6 user_id: string;
7 user_tag: string;
8 channel_id: string;
9 guild_id: string | null;
10 message: string;
11 expires_at: Date;
12 created_at?: Date;
13 locale: string;
14 metadata?: {
15 source: string;
16 command_id?: string;
17 original_message_id?: string;
18 original_channel_id?: string;
19 message_url?: string;
20 };
21}
22
23class DatabaseError extends Error {
24 public readonly isDatabaseError = true;
25 public readonly isUserFriendly: boolean;
26 public readonly userMessage: string;
27
28 constructor(message: string, userMessage?: string, isUserFriendly = false) {
29 super(message);
30 this.name = 'DatabaseError';
31 this.userMessage = userMessage || 'A database error occurred. Please try again later.';
32 this.isUserFriendly = isUserFriendly;
33 }
34}
35
36function createDatabaseError(error: unknown, operation: string): DatabaseError {
37 const errorCode = (error as { code?: string }).code;
38 const errorMessage = (error as { message?: string }).message || 'Unknown database error';
39
40 logger.error(`Database error during ${operation}:`, error);
41
42 switch (errorCode) {
43 case 'ECONNREFUSED':
44 case 'ENOTFOUND':
45 return new DatabaseError(
46 errorMessage,
47 'Unable to connect to the database. Please try again later.',
48 true,
49 );
50 case '42703':
51 return new DatabaseError(
52 errorMessage,
53 'Database schema error. Please contact support.',
54 true,
55 );
56 case '23505':
57 return new DatabaseError(errorMessage, 'This reminder already exists.', true);
58 case '23502':
59 return new DatabaseError(
60 errorMessage,
61 'Missing required information. Please try again.',
62 true,
63 );
64 case '23503':
65 return new DatabaseError(errorMessage, 'Invalid reference. Please try again.', true);
66 default:
67 return new DatabaseError(
68 errorMessage,
69 'A database error occurred. Please try again later.',
70 false,
71 );
72 }
73}
74
75async function ensureUserRegistered(
76 userId: string,
77 userTag: string,
78 language = 'en',
79): Promise<void> {
80 const query = `
81 SELECT ensure_user_registered($1, $2, $3)
82 `;
83
84 try {
85 await pool.query(query, [userId, userTag, language]);
86 } catch (error) {
87 throw createDatabaseError(error, 'user registration');
88 }
89}
90
91async function saveReminder(reminderData: ReminderData): Promise<ReminderData> {
92 await ensureUserRegistered(
93 reminderData.user_id,
94 reminderData.user_tag,
95 reminderData.locale || 'en',
96 );
97
98 const query = `
99 INSERT INTO reminders (
100 reminder_id, user_id, user_tag, channel_id, guild_id,
101 message, expires_at, metadata
102 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
103 RETURNING *
104 `;
105
106 const values = [
107 reminderData.reminder_id,
108 reminderData.user_id,
109 reminderData.user_tag,
110 reminderData.channel_id,
111 reminderData.guild_id,
112 reminderData.message,
113 reminderData.expires_at,
114 reminderData.metadata || {},
115 ];
116
117 try {
118 const result = await pool.query<ReminderData>(query, values);
119 return result.rows[0];
120 } catch (error) {
121 throw createDatabaseError(error, 'saving reminder');
122 }
123}
124
125async function completeReminder(reminderId: string) {
126 const query = `
127 UPDATE reminders
128 SET is_completed = TRUE, completed_at = CURRENT_TIMESTAMP
129 WHERE reminder_id = $1
130 RETURNING *
131 `;
132
133 try {
134 const result = await pool.query(query, [reminderId]);
135 return result.rows[0];
136 } catch (error) {
137 throw createDatabaseError(error, 'completing reminder');
138 }
139}
140
141async function getActiveReminders() {
142 const query = `
143 SELECT * FROM reminders
144 WHERE is_completed = FALSE
145 AND expires_at > CURRENT_TIMESTAMP
146 ORDER BY expires_at ASC
147 `;
148
149 try {
150 const result = await pool.query(query);
151 return result.rows;
152 } catch (error) {
153 throw createDatabaseError(error, 'fetching active reminders');
154 }
155}
156
157async function getReminder(reminderId: string) {
158 const query = 'SELECT * FROM reminders WHERE reminder_id = $1';
159
160 try {
161 const result = await pool.query(query, [reminderId]);
162 return result.rows[0] || null;
163 } catch (error) {
164 throw createDatabaseError(error, 'fetching reminder');
165 }
166}
167
168async function getUserReminders(userId: string) {
169 const query = `
170 SELECT * FROM reminders
171 WHERE user_id = $1
172 ORDER BY created_at DESC
173 LIMIT 50
174 `;
175
176 try {
177 const result = await pool.query(query, [userId]);
178 return result.rows;
179 } catch (error) {
180 throw createDatabaseError(error, 'fetching user reminders');
181 }
182}
183
184async function cleanupReminders(days = 30) {
185 const query = `
186 DELETE FROM reminders
187 WHERE is_completed = TRUE
188 AND completed_at < CURRENT_TIMESTAMP - ($1 * INTERVAL '1 day')
189 RETURNING *
190 `;
191
192 try {
193 const result = await pool.query(query, [days]);
194 return result.rowCount;
195 } catch (error) {
196 throw createDatabaseError(error, 'cleaning up reminders');
197 }
198}
199
200async function clearCompletedReminders(userId: string) {
201 const query = `
202 DELETE FROM reminders
203 WHERE user_id = $1
204 AND is_completed = TRUE
205 RETURNING *
206 `;
207
208 try {
209 const result = await pool.query(query, [userId]);
210 return result.rowCount;
211 } catch (error) {
212 throw createDatabaseError(error, 'clearing completed reminders');
213 }
214}
215
216export {
217 saveReminder,
218 completeReminder,
219 getActiveReminders,
220 getReminder,
221 getUserReminders,
222 cleanupReminders,
223 clearCompletedReminders,
224 ensureUserRegistered,
225 DatabaseError,
226};