(** Simple Matrix bot example using the Eio-idiomatic Matrix SDK.
This example demonstrates:
- Creating a client and logging in with exception-based error handling
- Using Eio structured concurrency for the sync loop
- Responding to messages in rooms
- Proper cancellation via Eio switches
To run:
{[
dune exec examples/simple_bot.exe -- \
--homeserver https://matrix.org \
--username @bot:matrix.org \
--password secret
]}
@see Matrix Client-Server API *)
open Matrix_eio
(** Helper to extract string from JSON.
Returns [Some value] if the key exists and is a string, [None] otherwise. *)
let json_get_string key (json : Jsont.json) =
match json with
| Jsont.Object (mems, _) ->
List.find_map (fun ((name, _), value) ->
if name = key then
match value with
| Jsont.String (s, _) -> Some s
| _ -> None
else None
) mems
| _ -> None
(** Handle a single sync response, looking for messages to respond to.
This function processes timeline events from joined rooms and responds
to simple commands:
- [!echo ] - Echoes the text back
- [!ping] - Responds with "pong!"
@see Sync API *)
let handle_sync client my_user_id (response : Matrix_proto.Sync.Response.t) =
match response.rooms with
| None -> ()
| Some rooms ->
(* Process each joined room *)
List.iter (fun (room_id_str, joined_room) ->
match Matrix_proto.Id.Room_id.of_string room_id_str with
| Error _ -> ()
| Ok room_id ->
(* Check timeline events *)
let events = match joined_room.Matrix_proto.Sync.Joined_room.timeline with
| None -> []
| Some timeline -> timeline.events
in
List.iter (fun (event : Matrix_proto.Event.Raw_event.t) ->
(* Only respond to m.room.message events *)
let event_type = Matrix_proto.Event.Event_type.to_string event.type_ in
if event_type = "m.room.message" then begin
(* Get sender - don't respond to our own messages *)
let sender = event.sender in
let is_self = Matrix_proto.Id.User_id.to_string sender =
Matrix_proto.Id.User_id.to_string my_user_id in
if not is_self then begin
(* Extract message body from content *)
let body = json_get_string "body" event.content in
match body with
| Some msg when String.starts_with ~prefix:"!echo " msg ->
(* Echo command - repeat the message *)
let echo_text = String.sub msg 6 (String.length msg - 6) in
(try
let _ = Messages.send_text client ~room_id ~body:echo_text () in
Printf.printf "Echoed: %s\n%!" echo_text
with Eio.Io _ as e ->
Printf.eprintf "Failed to send echo: %s\n%!" (Printexc.to_string e))
| Some msg when msg = "!ping" ->
(* Ping command *)
(try
let _ = Messages.send_text client ~room_id ~body:"pong!" () in
Printf.printf "Responded to ping\n%!"
with Eio.Io _ as e ->
Printf.eprintf "Failed to send pong: %s\n%!" (Printexc.to_string e))
| Some msg ->
Printf.printf "Message from %s: %s\n%!"
(Matrix_proto.Id.User_id.to_string sender) msg
| None -> ()
end
end
) events
) rooms.join
(** Main bot loop using Eio structured concurrency.
The sync loop runs in a dedicated fibre that can be cancelled by
releasing the switch. This allows for clean shutdown. *)
let run_bot ~homeserver ~username ~password =
Eio_main.run @@ fun env ->
Eio.Switch.run @@ fun sw ->
Printf.printf "Connecting to %s...\n%!" homeserver;
(* Create client and login using the Eio-idiomatic API.
This raises Eio.Io on failure instead of returning Result. *)
let client =
try
Matrix_eio.login_password ~sw ~env
~homeserver:(Uri.of_string homeserver)
~user:username ~password ()
with Eio.Io (Error.E err, _) ->
Format.eprintf "Login failed: %a@." Error.pp_err err;
exit 1
in
let my_user_id = Client.user_id client in
Printf.printf "Logged in as %s\n%!"
(Matrix_proto.Id.User_id.to_string my_user_id);
(* Get joined rooms *)
(try
let rooms = Rooms.get_joined_rooms client in
Printf.printf "Joined %d rooms\n%!" (List.length rooms)
with Eio.Io _ ->
Printf.printf "Could not get joined rooms\n%!");
(* Start sync loop in a fibre.
The loop can be cancelled by releasing the switch. *)
Printf.printf "Starting sync loop (Ctrl+C to stop)...\n%!";
let clock = Eio.Stdenv.clock env in
(* Use the Eio-idiomatic sync loop with callback *)
Sync.sync_forever ~sw ~clock client
~params:{ Sync.default_params with timeout = 30000 }
~on_sync:(fun response ->
Printf.printf "Sync: next_batch=%s\n%!" response.next_batch;
handle_sync client my_user_id response;
Sync.Continue)
~on_error:(fun err ->
Format.eprintf "Sync error: %a@." Error.pp_err err;
(* Retry after 5 seconds on error *)
Sync.Retry_after 5.0)
();
(* Block the main fibre - sync runs in background *)
(* In a real application, you might want to handle signals here *)
Eio.Fiber.await_cancel ()
(** Parse command line and run the bot. *)
let () =
let homeserver = ref "" in
let username = ref "" in
let password = ref "" in
let spec = [
("--homeserver", Arg.Set_string homeserver, "Homeserver URL (e.g., https://matrix.org)");
("--username", Arg.Set_string username, "Username (localpart or full @user:server)");
("--password", Arg.Set_string password, "Password");
] in
Arg.parse spec (fun _ -> ()) "Simple Matrix Bot using Eio\n\nUsage:";
if !homeserver = "" || !username = "" || !password = "" then begin
Printf.eprintf "Usage: simple_bot --homeserver URL --username USER --password PASS\n";
exit 1
end;
run_bot ~homeserver:!homeserver ~username:!username ~password:!password