(** 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