IRC Bot Example

A real-world example demonstrating event sourcing with an IRC bot using karma tracking, projections, and the Memory plugin.

Overview

This example shows how to build an IRC bot using Sourcing's event sourcing patterns. The bot connects to an IRC server, joins channels, and tracks user karma using projections.

Features demonstrated:

Project Structure

📁 examples/irc-bot/
├── 📄 bin/
│   └── 📄 irc-bot.raku          # Main executable
├── 📁 lib/
│   └── 📁 IRC/
│       └── 📁 Bot/
│           ├── 📁 Alias/
│           │   ├── 📄 Aggregation.rakumod
│           │   ├── 📄 Events.rakumod
│           │   └── 📄 Projection.rakumod
│           ├── 📁 Channel/
│           │   ├── 📄 Aggregation.rakumod
│           │   └── 📄 Events.rakumod
│           ├── 📁 Karma/
│           │   ├── 📄 Aggregation.rakumod
│           │   └── 📄 Events.rakumod
│           ├── 📁 Projections/
│           │   ├── 📄 KarmaProjection.rakumod
│           │   └── 📄 ChannelLogProjection.rakumod
│           ├── 📁 Saga/
│           │   ├── 📄 KarmaHandler.rakumod
│           │   ├── 📄 AliasHandler.rakumod
│           │   └── 📄 Supply.rakumod
│           └── 📄 Plugin/
│               └── 📄 Karma.rakumod
├── 📄 config.toml                # Bot configuration
└── 📁 t/
    └── 📄 01-irc-bot.rakutest   # Tests

Configuration

The bot is configured via config.toml:

nickname = sourcing-bot
server = irc.libera.chat
port = 6697
channels = #raku,#sourcing

karma-enabled = true
karma-min = -1000
karma-max = 1000

Running the Bot

raku -I. examples/irc-bot/bin/irc-bot.raku

Commands

CommandDescription
++usernameIncrease user's karma by 1
--usernameDecrease user's karma by 1
!karma [user]Show karma for a user
!helpShow available commands

How It Works

Events

When a user types ++username, the bot emits a KarmaIncreased event:

my $e = KarmaIncreased.new(
    :$target,
    :changed-by($event.nick),
    :amount(1),
    :changed-at(DateTime.now)
);
$*SourcingConfig.emit: $e, :type(KarmaProjection), :ids({target => $target});

Projections

The KarmaProjection builds a read-optimized view of user karma:

projection KarmaProjection {
    has Str $.target is projection-id;
    has Int $.karma = 0;
    
    multi method apply(KarmaIncreased $e) {
        $!karma += $e.amount;
    }
    
    multi method apply(KarmaDecreased $e) {
        $!karma -= $e.amount;
    }
}

Querying Karma

To get a user's current karma, use the sourcing function:

my $karma = sourcing KarmaProjection, :target($username);
say $karma.karma;  # Current karma score

Event Flow

%%{init: {"theme": "dark", "themeVariables": { "darkMode": true }}}%%
sequenceDiagram
    participant IRC as IRC Server
    participant Plugin as Bot Plugin
    participant CA as ChannelAggregation
    participant PS as ProjectionStorage
    participant KH as KarmaHandler Saga
    participant AH as AliasHandler Saga
    participant KA as KarmaAggregation
    participant AA as AliasAggregation
    participant KP as KarmaProjection
    participant AP as AliasProjection

    Note over IRC, Plugin: IRC Message → Events Flow
    IRC->>Plugin: PRIVMSG #channel: user++

    Plugin->>CA: ChannelAggregation.receive-message(nick, message)
    CA->>CA: emit MessageReceived event
    CA->>PS: store(MessageReceived)

    PS->>KH: routing to sagas
    KH->>KH: apply(MessageReceived) → processing
    KH->>KH: parse /(\S+)++$/ pattern

    KH->>KA: KarmaAggregation.increment-karma(target)
    KA->>KA: emit KarmaIncreased event
    KA->>PS: store(KarmaIncreased)

    KP->>KP: apply(KarmaIncreased) → update score
    KP->>KP: apply(KarmaIncreased) → update increases

    Note over IRC, Plugin: Query Flow
    IRC->>Plugin: PRIVMSG #channel: !karma user
    Plugin->>KP: KarmaProjection.get-score(nick)
    KP-->>Plugin: score: 5
    Plugin-->>IRC: PRIVMSG #channel: "user has karma 5"

    Note over IRC, Plugin: Alias Flow
    IRC->>Plugin: PRIVMSG #channel: user=> newname
    Plugin->>AA: AliasAggregation.set-alias(user, newname)
    AA->>AA: emit AliasSet event
    AA->>PS: store(AliasSet)

    AP->>AP: apply(AliasSet) → update mapping

    IRC->>Plugin: PRIVMSG #channel: !aliases user
    Plugin->>AP: AliasProjection.get-aliases(user)
    AP-->>Plugin: aliases: [newname]
    Plugin-->>IRC: PRIVMSG #channel: "user aliases: newname"

Testing

Run the tests:

mi6 test t/01-irc-bot.rakutest

Extending the Bot

To add new features:

  1. Define new event classes in irc-bot.raku
  2. Create projections to build read models
  3. Add command handlers in the plugin

See Also