Matrix protocol in OCaml, Eio specialised
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