feat: implement reminder command with Appwrite integration and polling mechanism
This commit is contained in:
29
src/core/api/appwrite.ts
Normal file
29
src/core/api/appwrite.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// Simple Appwrite client wrapper
|
||||
// @ts-ignore
|
||||
import { Client, Databases } from 'node-appwrite';
|
||||
|
||||
const endpoint = process.env.APPWRITE_ENDPOINT || '';
|
||||
const projectId = process.env.APPWRITE_PROJECT_ID || '';
|
||||
const apiKey = process.env.APPWRITE_API_KEY || '';
|
||||
|
||||
export const APPWRITE_DATABASE_ID = process.env.APPWRITE_DATABASE_ID || '';
|
||||
export const APPWRITE_COLLECTION_REMINDERS_ID = process.env.APPWRITE_COLLECTION_REMINDERS_ID || '';
|
||||
|
||||
let client: Client | null = null;
|
||||
let databases: Databases | null = null;
|
||||
|
||||
function ensureClient() {
|
||||
if (!endpoint || !projectId || !apiKey) return null;
|
||||
if (client) return client;
|
||||
client = new Client().setEndpoint(endpoint).setProject(projectId).setKey(apiKey);
|
||||
databases = new Databases(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
export function getDatabases(): Databases | null {
|
||||
return ensureClient() ? (databases as Databases) : null;
|
||||
}
|
||||
|
||||
export function isAppwriteConfigured(): boolean {
|
||||
return Boolean(endpoint && projectId && apiKey && APPWRITE_DATABASE_ID && APPWRITE_COLLECTION_REMINDERS_ID);
|
||||
}
|
||||
121
src/core/api/reminders.ts
Normal file
121
src/core/api/reminders.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const sdk: any = require('node-appwrite');
|
||||
import type Amayo from '../client';
|
||||
import { getDatabases, isAppwriteConfigured, APPWRITE_COLLECTION_REMINDERS_ID, APPWRITE_DATABASE_ID } from './appwrite';
|
||||
|
||||
export type ReminderDoc = {
|
||||
$id?: string;
|
||||
userId: string;
|
||||
guildId?: string | null;
|
||||
channelId?: string | null;
|
||||
message: string;
|
||||
executeAt: string; // ISO string
|
||||
createdAt?: string;
|
||||
};
|
||||
|
||||
// Row type returned by Appwrite for our reminders
|
||||
export type ReminderRow = ReminderDoc & {
|
||||
$id: string;
|
||||
$createdAt?: string;
|
||||
$updatedAt?: string;
|
||||
$permissions?: string[];
|
||||
$collectionId?: string;
|
||||
$databaseId?: string;
|
||||
};
|
||||
|
||||
export async function scheduleReminder(doc: ReminderDoc): Promise<string> {
|
||||
const db = getDatabases();
|
||||
if (!db || !isAppwriteConfigured()) throw new Error('Appwrite no está configurado');
|
||||
const data = {
|
||||
userId: doc.userId,
|
||||
guildId: doc.guildId ?? null,
|
||||
channelId: doc.channelId ?? null,
|
||||
message: doc.message,
|
||||
executeAt: doc.executeAt,
|
||||
createdAt: doc.createdAt ?? new Date().toISOString()
|
||||
} as Record<string, any>;
|
||||
const res = await db.createDocument(APPWRITE_DATABASE_ID, APPWRITE_COLLECTION_REMINDERS_ID, sdk.ID.unique(), data) as unknown as { $id: string };
|
||||
return res.$id;
|
||||
}
|
||||
|
||||
async function fetchDueReminders(limit = 25): Promise<ReminderRow[]> {
|
||||
const db = getDatabases();
|
||||
if (!db || !isAppwriteConfigured()) return [];
|
||||
const nowIso = new Date().toISOString();
|
||||
try {
|
||||
const list = await db.listDocuments(APPWRITE_DATABASE_ID, APPWRITE_COLLECTION_REMINDERS_ID, [
|
||||
sdk.Query.lessThanEqual('executeAt', nowIso),
|
||||
sdk.Query.limit(limit)
|
||||
]) as unknown as { documents?: ReminderRow[] };
|
||||
return (list.documents || []) as ReminderRow[];
|
||||
} catch (e) {
|
||||
console.error('Error listando recordatorios vencidos:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function deliverReminder(bot: Amayo, doc: ReminderRow) {
|
||||
const userId: string = doc.userId;
|
||||
const channelId: string | null = (doc.channelId as string | null) || null;
|
||||
const message: string = doc.message || '';
|
||||
|
||||
let delivered = false;
|
||||
// 1) Intentar en el canal original si existe y es de texto
|
||||
if (channelId) {
|
||||
try {
|
||||
const ch: any = await bot.channels.fetch(channelId).catch(() => null);
|
||||
if (ch && typeof ch.send === 'function') {
|
||||
await ch.send({ content: `⏰ <@${userId}> Recordatorio: ${message}` });
|
||||
delivered = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('No se pudo enviar al canal original:', e);
|
||||
}
|
||||
}
|
||||
// 2) Fallback: DM al usuario
|
||||
if (!delivered) {
|
||||
try {
|
||||
const user = await bot.users.fetch(userId);
|
||||
await user.send({ content: `⏰ Recordatorio: ${message}` });
|
||||
delivered = true;
|
||||
} catch (e) {
|
||||
console.warn('No se pudo enviar DM al usuario:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return delivered;
|
||||
}
|
||||
|
||||
export function startReminderPoller(bot: Amayo) {
|
||||
if (!isAppwriteConfigured()) {
|
||||
console.warn('Appwrite no configurado: el poller de recordatorios no se iniciará.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const intervalSec = parseInt(process.env.REMINDERS_POLL_INTERVAL_SECONDS || '30', 10);
|
||||
console.log(`⏱️ Iniciando poller de recordatorios cada ${intervalSec}s`);
|
||||
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
const due = await fetchDueReminders(50);
|
||||
if (!due.length) return;
|
||||
for (const d of due) {
|
||||
const ok = await deliverReminder(bot, d);
|
||||
if (!ok) continue; // Dejar para reintento futuro
|
||||
try {
|
||||
const db = getDatabases();
|
||||
if (db) await db.deleteDocument(APPWRITE_DATABASE_ID, APPWRITE_COLLECTION_REMINDERS_ID, d.$id);
|
||||
} catch (e) {
|
||||
console.warn('No se pudo eliminar recordatorio entregado:', e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error en ciclo de recordatorios:', e);
|
||||
}
|
||||
}, Math.max(10, intervalSec) * 1000);
|
||||
|
||||
// Node no debería impedir salida por timers; por si acaso, unref
|
||||
// @ts-ignore
|
||||
if (typeof timer.unref === 'function') timer.unref();
|
||||
return timer;
|
||||
}
|
||||
@@ -35,8 +35,13 @@ const getInviteObject = (invite?: Invite) => invite?.guild ? {
|
||||
icon: invite.guild.icon ? `https://cdn.discordapp.com/icons/${invite.guild.id}/${invite.guild.icon}.webp?size=256` : ''
|
||||
} : null;
|
||||
|
||||
// Helper: calcula el rank dentro del servidor para un campo (weeklyPoints / monthlyPoints)
|
||||
async function computeRankInGuild(guildId: string, userId: string, field: 'weeklyPoints' | 'monthlyPoints', knownPoints?: number): Promise<number> {
|
||||
// Helper: calcula el rank dentro del servidor para un campo (weeklyPoints / monthlyPoints / totalPoints)
|
||||
async function computeRankInGuild(
|
||||
guildId: string,
|
||||
userId: string,
|
||||
field: 'weeklyPoints' | 'monthlyPoints' | 'totalPoints',
|
||||
knownPoints?: number
|
||||
): Promise<number> {
|
||||
try {
|
||||
let points = knownPoints;
|
||||
if (typeof points !== 'number') {
|
||||
@@ -85,6 +90,13 @@ export const VARIABLES: Record<string, VarResolver> = {
|
||||
const rank = await computeRankInGuild(guildId, userId, 'monthlyPoints', stats?.monthlyPoints);
|
||||
return String(rank || 0);
|
||||
},
|
||||
'user.rankTotal': async ({ user, guild, stats }) => {
|
||||
const userId = getUserId(user);
|
||||
const guildId = guild?.id;
|
||||
if (!userId || !guildId) return '0';
|
||||
const rank = await computeRankInGuild(guildId, userId, 'totalPoints', stats?.totalPoints);
|
||||
return String(rank || 0);
|
||||
},
|
||||
|
||||
// GUILD INFO
|
||||
'guild.name': ({ guild }) => guild?.name ?? '',
|
||||
|
||||
Reference in New Issue
Block a user