Bots & Handlers
Everything you register with dispatch is a handler: any object that responds to #event_types -> Array<String> and #call(event). There are two ways to produce one — the Bot DSL for the common case, and plain objects when you want full control.
The Bot DSL
Bot.new(client) { ... } pairs a Client with handler blocks declared via on:
bot = Async::Matrix::ApplicationService::Bot.new(client) do
on "m.room.member" do |event|
join_room(event.room_id) if event.content.membership == "invite"
end
on "m.room.message", msgtype: "m.text", not_from: :self do |event|
send_notice event.room_id, "Echo: #{event.content.body}"
end
end
A Bot responds to #handlers, so dispatch bot (or handler.register bot) expands it into its individual handlers.
on(*event_types, msgtype:, not_from:, &block)
Register a block for one or more event types. Both filters are optional:
msgtype:— only dispatch whenevent.content.msgtypematches (e.g."m.text"). Events with a different msgtype are skipped before your block runs.not_from:— pass:selfto skip events sent by this bot’s own MXID. Essential for avoiding echo loops.
You can pass several event types to one block:
on "m.room.message", "m.room.encrypted" do |event|
# ...
end
Context helpers
Handler blocks execute in a Context bound to the client, so they can call these directly without touching the client:
| Helper | Delegates to |
|---|---|
send_text(room_id, text) |
plain text message |
send_html(room_id, html, plaintext = nil) |
formatted message |
send_notice(room_id, text) |
m.notice message |
join_room(room_id) |
join |
leave_room(room_id) |
leave |
set_display_name(name, user_id = nil) |
profile update |
client |
the underlying Client, for anything else |
Reach for client when you need an API call the context doesn’t wrap:
on "m.room.message" do |event|
client.api.rooms(event.room_id).read_markers.post("m.fully_read" => event.event_id)
end
Plain handlers
When you want more structure than a block — state, injected collaborators, unit tests — implement the duck-type directly. This is exactly what a Bot produces under the hood.
class Echo
def initialize(client) = @client = client
def event_types = ["m.room.message"]
def call(event)
return unless event.content&.msgtype == "m.text"
return if event.sender == @client.config.bot_mxid
@client.send_notice(event.room_id, "Echo: #{event.content.body}")
end
end
dispatch Echo.new(client)
A handler is registered under each type in its #event_types; an event is delivered to every handler that matches its type.
Fault tolerance
Dispatch is deliberately fault-tolerant: an exception raised in one handler is caught and logged, and the remaining handlers for that event still run. A single misbehaving handler cannot stop the homeserver’s transaction from being acknowledged, nor block its siblings. Handle expected failures inside your own call where you want specific recovery; rely on the dispatcher only as a backstop.
Events
The event yielded to a handler wraps the raw Matrix event JSON with convenient accessors — event.room_id, event.sender, event.type, event.state_key, event.event_id, and event.content (itself dot-accessible, e.g. event.content.body, event.content.membership). Content access is nil-safe with &. where a field may be absent. See Events & Schemas for validation.