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:
- Sourcing::Plugin::Memory for event storage
- Projections for read-optimized views
- IRC::Client integration
- Command handling via event emission
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
| Command | Description |
|---|---|
++username | Increase user's karma by 1 |
--username | Decrease user's karma by 1 |
!karma [user] | Show karma for a user |
!help | Show 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:
- Define new event classes in
irc-bot.raku - Create projections to build read models
- Add command handlers in the plugin
See Also
- Projections - Read-optimized views
- Writing a Plugin - Custom storage backends
- Projection API