Skip to content

Latest commit

 

History

52 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dischorus

An opinionated, minimalistic Discord bot orchestration library for sending and receiving messages across multiple bot accounts sharing common channels.

Built on serenity and tokio.

Features

  • Multi-bot orchestration — run multiple bot accounts in a single session, automatically intersecting their accessible channels
  • Event-driven message matching — register filters (contains, starts_with, or custom closures) and receive matching messages through typed receivers
  • Message replies and reactions — send messages, reply to specific messages, and attach emoji reactions (Unicode or custom guild emojis)
  • Resilient pipeline — configurable backoff delays, send timeouts, retry jitter, and automatic reconnection handling on disconnection
  • Python bindings — full API exposed via PyO3/maturin with async support

Architecture

flowchart TB
    User["User Code"]

    subgraph Session["Session"]
        SB["SessionBuilder / Session"]
        BE["BotEvents<br/>(filters → flume channels)"]
        subgraph Bots["BotMap"]
            B0["Bot 0<br/>(receives events)"]
            B1["Bot 1"]
            BN["Bot N"]
        end
        PL["Pipeline task<br/>(bounded queue, backoff,<br/>retry jitter, timeouts)"]
    end

    RX["MessageEventReceiver"]

    subgraph GatewayLayer["Gateway (trait)"]
        SG["SerenityGateway"]
        EC["EmojiCache"]
    end

    subgraph DiscordAPI["Discord"]
        WS["WebSocket (receive)"]
        REST["REST API (send)"]
    end

    User -->|"register_event()"| BE
    BE --> RX
    RX -->|"recv_async() / recv()"| User
    User -->|"schedule_message() / schedule_reaction()"| PL

    SB -->|"build(): intersects<br/>accessible channels"| Bots

    WS --> SG -->|"dispatch"| BE
    PL -->|"send_message() / send_reaction()"| SG
    SG --- EC
    SG --> REST
Loading

Inbound path — the first bot in the session receives messages over the WebSocket gateway, dispatches them through registered MessageEvent filters, and fans them out to per-event flume channels exposed as MessageEventReceivers (bounded, with a configurable push timeout). The receiver loops back into user code — responses and reactions are produced by the user's event loop and fed back into the pipeline.

Outbound path — all sends go through a single pipeline task consuming a bounded queue. Each item is dispatched to the sender bot's gateway with a send timeout, retry-on-disconnect with random jitter, and a random backoff between items.

Design choices

  • Single serialized pipeline instead of per-bot concurrency. A swarm of bots posting concurrently from shared channels is a natural trigger for Discord's rate limits and spam heuristics. Serializing all outbound traffic through one queue with random inter-item delays (250–500 ms by default) trades a little throughput for predictable, human-like pacing and a single point where rate-limit policy lives. This also aligns with the use-case that motivated this project: simulating a human-like discord discussion with bots.

  • Bounded channels everywhere. Both the pipeline queue and event receiver channels are bounded (flume), so a fast producer or slow consumer applies backpressure instead of silently accumulating unbounded memory. Event delivery additionally has a timeout, so a stalled handler can never wedge dispatch.

  • Gateway as a trait over serenity. All Discord interaction is hidden behind a small async trait (send_message, send_reaction, accessible_channels, ...). The mock gateway used in the test suite plugs in at exactly this seam, which keeps tests off the network.

  • Channel intersection at session construction. With multiple accounts sharing channels, sending from a channel only some bots can see would be a source of confusing partial failures. Instead, SessionBuilder::build computes the intersection of all bots' accessible channels once, and dispatch simply filters on it — an event can never be received from, and a message never scheduled to, a channel the swarm doesn't fully share.

  • One shared pipeline rather than one per bot. Sessions are assumed to be long-lived and the pipeline is a lightweight background task, so centralizing retries, timeouts, and backoff there means user code never has to think about reconnection handling. GatewayError::Disconnected retries are transparent; only fatal errors and timeouts propagate to the caller.

  • ArcSwap for event registration. Filters are registered at runtime (possibly after bots are connected) without blocking the dispatch hot path: readers load a snapshot lock-free, and writers serialize on a mutex only when adding a new event.

Quick start (Rust)

Add the dependency:

[dependencies]
dischorus = { git = "https://github.com/oteffahi/dischorus" }

Set your bot token in a .env file:

DISCORD_TOKEN=your_bot_token_here
use dischorus::{
    PipelineConfig,
    message::{Message, MessageEvent, Reaction, ReactionEmoji},
    session::SessionBuilder,
};

#[tokio::main]
async fn main() {
    dischorus::logging::init_logs_from_env_or("info");

    let mut builder = SessionBuilder::new();
    let bot = builder.add_user("DISCORD_TOKEN");

    let session = builder
        .build(PipelineConfig::new(100).delays(500, 1000))
        .await
        .unwrap();

    let receiver = session
        .register_event(MessageEvent::contains("ping"))
        .capacity(50)
        .channel_timeout(10_000)
        .await;

    let wave = Reaction::new(bot, ReactionEmoji::unicode("👋"));
    while let Ok(event) = receiver.recv_async().await {
        session
            .schedule_message(
                Message::reply(event.message_id(), bot, "pong")
                    .reactions(vec![wave.clone()]),
            )
            .await
            .unwrap();
    }
}

Quick start (Python)

uv add "dischorus @ git+https://github.com/oteffahi/dischorus#subdirectory=python"

Or install manually from a clone:

git clone https://github.com/oteffahi/dischorus
cd dischorus/python
uv run maturin develop

Set your bot token in a .env file:

DISCORD_TOKEN=your_bot_token_here
import asyncio
import dischorus

async def main():
    dischorus.init_logs("info")
    builder = dischorus.SessionBuilder()
    bot = builder.add_user("DISCORD_TOKEN")

    session = await builder.build(
        dischorus.PipelineConfig.new(100, delays=(500, 1000))
    )

    receiver = await session.register_event(
        dischorus.MessageEvent.contains("ping"),
        capacity=50,
        channel_timeout_ms=10000,
    )

    wave = dischorus.Reaction.new(bot, dischorus.ReactionEmoji.unicode("👋"))
    while True:
        event = await receiver.recv()
        if event is None:
            break
        msg = dischorus.Message.reply(event.message_id(), bot, "pong", reactions=[wave])
        await session.schedule_message(msg)

asyncio.run(main())

License

This project is licensed under the MIT License.

About

An opinionated, minimalistic Discord bot orchestration library for sending and receiving messages across multiple bot accounts sharing common channels.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages