Mineral

Mineral is a Dart framework for building Discord bots. It handles the Discord gateway, REST API, and interaction routing so you can focus on your bot's logic.

Everything is organized around providers; isolated modules that register commands, events, and components. Each feature lives in its own class, making bots easy to grow and maintain across a team.

GitHub Discord


Install

dependencies:
  mineral: ^4.2.0

Quick start

1. Create a provider

// my_provider.dart
final class MyProvider extends Provider {
  final Client _client;

  MyProvider(this._client) {
    _client
      ..register<MyCommand>(MyCommand.new)
      ..register<OnMemberJoin>(OnMemberJoin.new);
  }
}

2. Register and start

// main.dart
void main() async {
  final client = ClientBuilder()
    .setIntent(Intent.allNonPrivileged)
    .registerProvider(MyProvider.new)
    .build();

  await client.init();
}

Features

Events

React to anything happening on your Discord server — member joins, message creation, voice state changes, and more. Each event is a dedicated class with a typed handle() method.

final class OnMemberJoin extends ServerMemberAddEvent {
  @override
  Future<void> handle(Member member, Server server) async {
    final channel = await server.channels.resolveSystemChannel();
    await channel?.send(MessageBuilder.text('Welcome, ${member.username}!'));
  }
}

Commands

Slash commands are declared as classes. Each command defines its name, description, and options via build(), and handles the interaction in handle(). Sub-commands are supported out of the box.

final class MyCommand implements CommandDeclaration {
  Future<void> handle(ServerCommandContext ctx, CommandOptions options) async {
    final target = options.require<String>('message');
    await ctx.interaction.reply(builder: MessageBuilder.text(target));
  }

  @override
  CommandDeclarationBuilder build() {
    return CommandDeclarationBuilder()
      ..setName('say')
      ..setDescription('Repeat a message')
      ..addOption(Option.string(name: 'message', description: 'Text to repeat', required: true))
      ..setHandle(handle);
  }
}

Buttons & Modals

Interactive components (buttons, modals, select menus) are bound to a customId. Mineral routes the interaction to the right handler automatically — no manual dispatch needed.

final class MyButton extends ServerButtonClickEvent {
  @override
  String? get customId => 'my_button';

  @override
  Future<void> handle(ServerButtonContext ctx) async {
    await ctx.interaction.reply(builder: MessageBuilder.text('Clicked!'));
  }
}

Global State

Share data across your entire bot without passing dependencies manually. Register a state once, read it anywhere via the State mixin.

abstract interface class CounterContract implements GlobalState<int> {
  void increment();
}

final class Counter implements CounterContract {
  int _count = 0;

  @override
  int get state => _count;

  @override
  void increment() => _count++;
}

// Register
client.register<CounterContract>(Counter.new);

// Use (in any handler with the State mixin)
state.read<CounterContract>().increment();

Examples

See the example/ directory for complete working bots:

  • welcome — member join event + button
  • feedback — slash command + button + modal flow
  • poll — sub-commands + vote buttons + global state

Libraries

api
container
contracts
events
mineral
The main entry-point for building bots with Mineral.
mineral_contracts
Contracts and interfaces for extension authors and DI customisation.
mineral_testing
Test helpers for bots built with Mineral.
services
utils