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 when event.content.msgtype matches (e.g. "m.text"). Events with a different msgtype are skipped before your block runs.
  • not_from: — pass :self to 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.


This site uses Just the Docs, a documentation theme for Jekyll.