Matrix protocol in OCaml, Eio specialised
at main 168 lines 6.5 kB view raw
1(** Simple Matrix bot example using the Eio-idiomatic Matrix SDK. 2 3 This example demonstrates: 4 - Creating a client and logging in with exception-based error handling 5 - Using Eio structured concurrency for the sync loop 6 - Responding to messages in rooms 7 - Proper cancellation via Eio switches 8 9 To run: 10 {[ 11 dune exec examples/simple_bot.exe -- \ 12 --homeserver https://matrix.org \ 13 --username @bot:matrix.org \ 14 --password secret 15 ]} 16 17 @see <https://spec.matrix.org/v1.11/client-server-api/> Matrix Client-Server API *) 18 19open Matrix_eio 20 21(** Helper to extract string from JSON. 22 Returns [Some value] if the key exists and is a string, [None] otherwise. *) 23let json_get_string key (json : Jsont.json) = 24 match json with 25 | Jsont.Object (mems, _) -> 26 List.find_map (fun ((name, _), value) -> 27 if name = key then 28 match value with 29 | Jsont.String (s, _) -> Some s 30 | _ -> None 31 else None 32 ) mems 33 | _ -> None 34 35(** Handle a single sync response, looking for messages to respond to. 36 37 This function processes timeline events from joined rooms and responds 38 to simple commands: 39 - [!echo <text>] - Echoes the text back 40 - [!ping] - Responds with "pong!" 41 42 @see <https://spec.matrix.org/v1.11/client-server-api/#syncing> Sync API *) 43let handle_sync client my_user_id (response : Matrix_proto.Sync.Response.t) = 44 match response.rooms with 45 | None -> () 46 | Some rooms -> 47 (* Process each joined room *) 48 List.iter (fun (room_id_str, joined_room) -> 49 match Matrix_proto.Id.Room_id.of_string room_id_str with 50 | Error _ -> () 51 | Ok room_id -> 52 (* Check timeline events *) 53 let events = match joined_room.Matrix_proto.Sync.Joined_room.timeline with 54 | None -> [] 55 | Some timeline -> timeline.events 56 in 57 List.iter (fun (event : Matrix_proto.Event.Raw_event.t) -> 58 (* Only respond to m.room.message events *) 59 let event_type = Matrix_proto.Event.Event_type.to_string event.type_ in 60 if event_type = "m.room.message" then begin 61 (* Get sender - don't respond to our own messages *) 62 let sender = event.sender in 63 let is_self = Matrix_proto.Id.User_id.to_string sender = 64 Matrix_proto.Id.User_id.to_string my_user_id in 65 if not is_self then begin 66 (* Extract message body from content *) 67 let body = json_get_string "body" event.content in 68 match body with 69 | Some msg when String.starts_with ~prefix:"!echo " msg -> 70 (* Echo command - repeat the message *) 71 let echo_text = String.sub msg 6 (String.length msg - 6) in 72 (try 73 let _ = Messages.send_text client ~room_id ~body:echo_text () in 74 Printf.printf "Echoed: %s\n%!" echo_text 75 with Eio.Io _ as e -> 76 Printf.eprintf "Failed to send echo: %s\n%!" (Printexc.to_string e)) 77 | Some msg when msg = "!ping" -> 78 (* Ping command *) 79 (try 80 let _ = Messages.send_text client ~room_id ~body:"pong!" () in 81 Printf.printf "Responded to ping\n%!" 82 with Eio.Io _ as e -> 83 Printf.eprintf "Failed to send pong: %s\n%!" (Printexc.to_string e)) 84 | Some msg -> 85 Printf.printf "Message from %s: %s\n%!" 86 (Matrix_proto.Id.User_id.to_string sender) msg 87 | None -> () 88 end 89 end 90 ) events 91 ) rooms.join 92 93(** Main bot loop using Eio structured concurrency. 94 95 The sync loop runs in a dedicated fibre that can be cancelled by 96 releasing the switch. This allows for clean shutdown. *) 97let run_bot ~homeserver ~username ~password = 98 Eio_main.run @@ fun env -> 99 Eio.Switch.run @@ fun sw -> 100 101 Printf.printf "Connecting to %s...\n%!" homeserver; 102 103 (* Create client and login using the Eio-idiomatic API. 104 This raises Eio.Io on failure instead of returning Result. *) 105 let client = 106 try 107 Matrix_eio.login_password ~sw ~env 108 ~homeserver:(Uri.of_string homeserver) 109 ~user:username ~password () 110 with Eio.Io (Error.E err, _) -> 111 Format.eprintf "Login failed: %a@." Error.pp_err err; 112 exit 1 113 in 114 115 let my_user_id = Client.user_id client in 116 Printf.printf "Logged in as %s\n%!" 117 (Matrix_proto.Id.User_id.to_string my_user_id); 118 119 (* Get joined rooms *) 120 (try 121 let rooms = Rooms.get_joined_rooms client in 122 Printf.printf "Joined %d rooms\n%!" (List.length rooms) 123 with Eio.Io _ -> 124 Printf.printf "Could not get joined rooms\n%!"); 125 126 (* Start sync loop in a fibre. 127 The loop can be cancelled by releasing the switch. *) 128 Printf.printf "Starting sync loop (Ctrl+C to stop)...\n%!"; 129 130 let clock = Eio.Stdenv.clock env in 131 132 (* Use the Eio-idiomatic sync loop with callback *) 133 Sync.sync_forever ~sw ~clock client 134 ~params:{ Sync.default_params with timeout = 30000 } 135 ~on_sync:(fun response -> 136 Printf.printf "Sync: next_batch=%s\n%!" response.next_batch; 137 handle_sync client my_user_id response; 138 Sync.Continue) 139 ~on_error:(fun err -> 140 Format.eprintf "Sync error: %a@." Error.pp_err err; 141 (* Retry after 5 seconds on error *) 142 Sync.Retry_after 5.0) 143 (); 144 145 (* Block the main fibre - sync runs in background *) 146 (* In a real application, you might want to handle signals here *) 147 Eio.Fiber.await_cancel () 148 149(** Parse command line and run the bot. *) 150let () = 151 let homeserver = ref "" in 152 let username = ref "" in 153 let password = ref "" in 154 155 let spec = [ 156 ("--homeserver", Arg.Set_string homeserver, "Homeserver URL (e.g., https://matrix.org)"); 157 ("--username", Arg.Set_string username, "Username (localpart or full @user:server)"); 158 ("--password", Arg.Set_string password, "Password"); 159 ] in 160 161 Arg.parse spec (fun _ -> ()) "Simple Matrix Bot using Eio\n\nUsage:"; 162 163 if !homeserver = "" || !username = "" || !password = "" then begin 164 Printf.eprintf "Usage: simple_bot --homeserver URL --username USER --password PASS\n"; 165 exit 1 166 end; 167 168 run_bot ~homeserver:!homeserver ~username:!username ~password:!password