/* * Slopbot client lifecycle. Separate from the Vesktop core by design. * Setup and security: docs/slopbot.md. */ import { Client, Events, GatewayIntentBits } from "discord.js"; import type { SlopbotConfig } from "./config.js"; import type { SlopbotFetcher } from "./backend.js"; export interface SlopbotStatus { connected: boolean; user: string | null; guilds: string[]; } /** * Log in and return a ready client. The bot needs no message events, so it * declares only the Guilds intent; REST history does not use the gateway. */ export async function startSlopbot(config: SlopbotConfig): Promise { if (!config.enabled || !config.token) { throw new Error("slopbot is disabled or has no token"); } const client = new Client({ intents: [GatewayIntentBits.Guilds] }); // login() resolves before the READY dispatch, so wait for it: the guild cache // and client.user are only populated then. const ready = new Promise((resolve, reject) => { client.once(Events.ClientReady, () => resolve()); client.once(Events.Error, reject); }); await client.login(config.token); await ready; return client; } export async function stopSlopbot(client: Client | null): Promise { if (!client) return; await client.destroy(); } /** The Bot tab reads this. Safe with a null client. */ export function slopbotStatus(client: Client | null): SlopbotStatus { if (!client || !client.isReady()) { return { connected: false, user: null, guilds: [] }; } return { connected: true, user: client.user?.tag ?? null, guilds: [...client.guilds.cache.values()].map(guild => guild.name) }; } /** The ScrapeBackend fetcher over a live client. */ export function slopbotFetcher(client: Client): SlopbotFetcher { return { async fetchMessages(channelId, options) { const channel = await client.channels.fetch(channelId); if (!channel || !channel.isTextBased()) { throw new Error(`channel ${channelId} is not text based`); } const messages = await channel.messages.fetch({ limit: options.limit, ...(options.before ? { before: options.before } : {}) }); return [...messages.values()]; } }; }