Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Client, GatewayIntentBits } from "discord.js";

// Step 3 only needs the `Guilds` intent to log a list of connected guilds on READY.
// Reaction handlers (step 5) will add `GuildMessageReactions` and the privileged
// `GuildMembers` intent (which must be toggled in the Discord Developer Portal),
// along with the `Message`/`Channel`/`Reaction` partials needed for old messages.
export function createClient(): Client {
return new Client({
intents: [GatewayIntentBits.Guilds],
});
}
13 changes: 13 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from "zod";

const envSchema = z.object({
DISCORD_TOKEN: z.string().min(1, "DISCORD_TOKEN is required"),
LOG_LEVEL: z.enum(["trace", "debug", "info", "warn", "error", "fatal"]).default("info"),
CONFIG_PATH: z.string().default("config/verifications.yaml"),
});

export type Env = z.infer<typeof envSchema>;

export function loadEnv(input: NodeJS.ProcessEnv = process.env): Env {
return envSchema.parse(input);
}
18 changes: 18 additions & 0 deletions src/events/ready.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { type Client, Events } from "discord.js";
import type { Config } from "../config.js";
import { logger } from "../logger.js";

export function registerReady(client: Client, cfg: Config): void {
client.once(Events.ClientReady, (readyClient) => {
const guilds = readyClient.guilds.cache.map((g) => ({ id: g.id, name: g.name }));
logger.info(
{
bot: readyClient.user.tag,
guildCount: guilds.length,
guilds,
verificationsConfigured: cfg.verifications.length,
},
"ready",
);
});
}
44 changes: 32 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
import { createClient } from "./client.js";
import { loadConfig } from "./config.js";
import { loadEnv } from "./env.js";
import { registerReady } from "./events/ready.js";
import { logger } from "./logger.js";

function main(): void {
const path = process.env.CONFIG_PATH ?? "config/verifications.yaml";
const cfg = loadConfig(path);
console.log(`Loaded config from ${path}`);
console.log(` ${cfg.verifications.length} verification(s):`);
for (const v of cfg.verifications) {
console.log(
` - ${v.name}: guild=${v.guild_id} message=${v.message_id} emoji=${v.emoji} role=${v.role_id} on_remove=${v.on_remove}`,
);
}
console.log(` sweep: on_startup=${cfg.sweep.on_startup} cron='${cfg.sweep.cron}'`);
async function main(): Promise<void> {
const env = loadEnv();
const cfg = loadConfig(env.CONFIG_PATH);
logger.info(
{ configPath: env.CONFIG_PATH, verifications: cfg.verifications.length },
"config loaded",
);

const client = createClient();
registerReady(client, cfg);

client.on("error", (err) => {
logger.error({ err }, "client error");
});

const shutdown = async (signal: string): Promise<void> => {
logger.info({ signal }, "shutting down");
await client.destroy();
process.exit(0);
};
process.once("SIGINT", () => void shutdown("SIGINT"));
process.once("SIGTERM", () => void shutdown("SIGTERM"));

await client.login(env.DISCORD_TOKEN);
}

main();
main().catch((err: unknown) => {
logger.fatal({ err }, "fatal error during startup");
process.exit(1);
});
11 changes: 11 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { pino } from "pino";

export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
formatters: {
level: (label) => ({ level: label }),
},
timestamp: pino.stdTimeFunctions.isoTime,
});

export type Logger = typeof logger;
25 changes: 25 additions & 0 deletions test/env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { loadEnv } from "../src/env.js";

describe("loadEnv", () => {
it("parses a valid env", () => {
const env = loadEnv({ DISCORD_TOKEN: "tok", LOG_LEVEL: "debug", CONFIG_PATH: "x.yaml" });
expect(env.DISCORD_TOKEN).toBe("tok");
expect(env.LOG_LEVEL).toBe("debug");
expect(env.CONFIG_PATH).toBe("x.yaml");
});

it("defaults LOG_LEVEL and CONFIG_PATH", () => {
const env = loadEnv({ DISCORD_TOKEN: "tok" });
expect(env.LOG_LEVEL).toBe("info");
expect(env.CONFIG_PATH).toBe("config/verifications.yaml");
});

it("rejects a missing DISCORD_TOKEN", () => {
expect(() => loadEnv({})).toThrow(/DISCORD_TOKEN/);
});

it("rejects an invalid LOG_LEVEL", () => {
expect(() => loadEnv({ DISCORD_TOKEN: "tok", LOG_LEVEL: "verbose" })).toThrow();
});
});
Loading