Skip to content

A discord.js scafold for building Discord bots with less pain and more magic.

License

Notifications You must be signed in to change notification settings

UniquePixels/Unicorn

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Unicorn

A Discord bot framework built on Discord.js and TypeScript, designed to run on Bun.

Unicorn provides a structured, type-safe approach to building Discord bots using a Spark system for modular command/event handling and composable Guards for validation.

Features

  • Spark system -- modular handlers for commands, components, gateway events, and scheduled tasks
  • Guard composition -- chainable validation functions with TypeScript type narrowing
  • Type-safe configuration -- Zod-validated config with secret resolution and environment mapping
  • Scheduled events -- cron-based tasks with timezone support
  • Component pattern matching -- exact, prefix, wildcard, and regex matching for button/select/modal handlers
  • Health check server -- liveness and readiness probes for container orchestration
  • Structured logging -- Pino with Sentry integration in production
  • Graceful shutdown -- coordinated cleanup of jobs, servers, and the Discord client

Requirements

  • Bun v1.0+
  • Node.js 18+ (for Discord.js compatibility)

Quick Start

# Install dependencies
bun install

# Set up environment variables
# Bun loads .env automatically -- no dotenv needed
echo 'apiKey=your-bot-token-here' > .env

# Start the bot
bun start

Project Structure

src/
├── index.ts                    # Entry point and startup sequence
├── config.ts                   # Application configuration
├── sentry.ts                   # Sentry initialization (preloaded)
├── core/
│   ├── client/                 # UnicornClient interface and initialization
│   ├── configuration/          # Zod schemas, parseConfig(), type-safe IDs
│   ├── guards/                 # Guard infrastructure (runGuards, createGuard)
│   ├── logger/                 # Pino logger with Sentry transport
│   ├── sparks/                 # Spark definitions and loader
│   └── lib/                    # Shared utilities (attempt)
├── guards/
│   └── built-in/               # Built-in guard implementations
└── sparks/
    ├── built-in/               # Core event handlers (interaction routing, ready)
    └── ...                     # Your sparks go here

Creating Sparks

Commands

import { SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';

export const ping = defineCommand({
  command: new SlashCommandBuilder()
    .setName('ping')
    .setDescription('Check bot latency'),
  action: async (interaction, client) => {
    await interaction.reply(`Pong! ${client.ws.ping}ms`);
  },
});

Components

import { defineComponent } from '@/core/sparks';

export const confirmButton = defineComponent({
  id: 'confirm-action',
  action: async (interaction, client) => {
    await interaction.reply('Confirmed!');
  },
});

Gateway Events

import { Events } from 'discord.js';
import { defineGatewayEvent } from '@/core/sparks';

export const memberJoin = defineGatewayEvent({
  event: Events.GuildMemberAdd,
  action: async (member, client) => {
    client.logger.info({ userId: member.id }, 'New member joined');
  },
});

Scheduled Events

import { defineScheduledEvent } from '@/core/sparks';

export const dailyCleanup = defineScheduledEvent({
  id: 'daily-cleanup',
  schedule: '0 0 * * *',
  action: async (ctx) => {
    ctx.client.logger.info('Running daily cleanup...');
  },
});

Guards

Guards are composable validation functions that run before a spark's action:

import { PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import { inCachedGuild, hasPermission } from '@/guards';

export const kick = defineCommand({
  command: new SlashCommandBuilder()
    .setName('kick')
    .setDescription('Kick a member'),
  guards: [inCachedGuild, hasPermission(PermissionFlagsBits.KickMembers)],
  action: async (interaction, client) => {
    // interaction is typed with guild guaranteed
    await interaction.reply('Done.');
  },
});

See docs/guards.md for the full guard reference.

Scripts

bun start          # Run with Sentry preload
bun lint           # Format + check + typecheck
bun lint:format    # Biome format
bun lint:code      # Biome check
bun lint:tsc       # TypeScript typecheck
bun test           # Run tests

Documentation

  • Commands -- slash commands, autocomplete, subcommand groups
  • Components -- buttons, select menus, modals, pattern matching
  • Guards -- built-in guards, custom guards, composition
  • Gateway Events -- event listeners, once vs recurring
  • Scheduled Events -- cron tasks, timezones, lifecycle
  • Configuration -- config schema, secrets, envMap, health checks

License

MIT

About

A discord.js scafold for building Discord bots with less pain and more magic.

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Sponsor this project

Contributors 6