Matrix protocol in OCaml, Eio specialised
1(** Send a direct message to another Matrix user.
2
3 This example demonstrates:
4 - Creating a client and logging in
5 - Finding or creating a direct message room with another user
6 - Sending a single text message
7 - Proper cleanup with logout
8
9 To run:
10 {[
11 dune exec examples/send_dm.exe -- \
12 --homeserver https://matrix.org \
13 --username @you:matrix.org \
14 --password secret \
15 --recipient @them:matrix.org \
16 "Hello!"
17 ]}
18
19 Or using an environment variable for the password:
20 {[
21 MATRIX_PASSWORD=secret dune exec examples/send_dm.exe -- \
22 --homeserver https://matrix.org \
23 --username @you:matrix.org \
24 --recipient @them:matrix.org \
25 "Hello!"
26 ]}
27
28 Enable E2E encryption on new rooms:
29 {[
30 dune exec examples/send_dm.exe -- --encrypted \
31 --homeserver https://matrix.org \
32 ...
33 ]}
34
35 Enable debug logging:
36 {[
37 dune exec examples/send_dm.exe -- -v -v \
38 --homeserver https://matrix.org \
39 ...
40 ]}
41
42 @see <https://spec.matrix.org/v1.11/client-server-api/#direct-messaging> Direct Messaging *)
43
44open Cmdliner
45open Matrix_eio
46
47(** Set up logging with fmt reporter. *)
48let setup_log style_renderer level =
49 Fmt_tty.setup_std_outputs ?style_renderer ();
50 Logs.set_level level;
51 Logs.set_reporter (Logs_fmt.reporter ());
52 ()
53
54(** Send a direct message to a user.
55
56 Finds an existing DM room or creates a new one, sends the message, and returns. *)
57let send_dm ~homeserver ~username ~password ~recipient ~message ~encrypted =
58 Eio_main.run @@ fun env ->
59 Eio.Switch.run @@ fun sw ->
60
61 Logs.info (fun m -> m "Connecting to %s" homeserver);
62
63 (* Create client and login *)
64 let client =
65 try
66 Matrix_eio.login_password ~sw ~env
67 ~homeserver:(Uri.of_string homeserver)
68 ~user:username ~password ()
69 with Eio.Io (Error.E err, _) ->
70 Logs.err (fun m -> m "Login failed: %a" Error.pp_err err);
71 exit 1
72 in
73
74 let my_user_id = Client.user_id client in
75 Logs.info (fun m -> m "Logged in as %s"
76 (Matrix_proto.Id.User_id.to_string my_user_id));
77
78 (* Parse recipient user ID *)
79 let recipient_id =
80 match Matrix_proto.Id.User_id.of_string recipient with
81 | Ok id -> id
82 | Error (`Invalid_user_id msg) ->
83 Logs.err (fun m -> m "Invalid recipient user ID '%s': %s" recipient msg);
84 exit 1
85 in
86
87 Logs.info (fun m -> m "Finding or creating DM room with %s" recipient);
88
89 (* Get existing DM room or create a new one.
90 This checks m.direct account data for existing rooms first. *)
91 let encrypted_opt = if encrypted then Some true else None in
92 let room_id =
93 try
94 Rooms.get_or_create_dm client ~user_id:recipient_id ?encrypted:encrypted_opt ()
95 with Eio.Io (Error.E err, _) ->
96 Logs.err (fun m -> m "Failed to get/create DM room: %a" Error.pp_err err);
97 exit 1
98 in
99
100 Logs.info (fun m -> m "Using room %s"
101 (Matrix_proto.Id.Room_id.to_string room_id));
102
103 (* Send the message *)
104 Logs.info (fun m -> m "Sending message...");
105
106 let event_id =
107 try
108 Messages.send_text client ~room_id ~body:message ()
109 with Eio.Io (Error.E err, _) ->
110 Logs.err (fun m -> m "Failed to send message: %a" Error.pp_err err);
111 exit 1
112 in
113
114 Logs.app (fun m -> m "Message sent (event ID: %s)"
115 (Matrix_proto.Id.Event_id.to_string event_id));
116
117 (* Logout *)
118 (try
119 Auth.logout client;
120 Logs.info (fun m -> m "Logged out")
121 with Eio.Io _ ->
122 Logs.warn (fun m -> m "Logout failed (session may still be active)"));
123
124 `Ok ()
125
126(* Command-line argument definitions *)
127
128let homeserver =
129 let doc = "Matrix homeserver URL (e.g., https://matrix.org)." in
130 let env = Cmd.Env.info "MATRIX_HOMESERVER" ~doc in
131 Arg.(required & opt (some string) None &
132 info ["homeserver"; "s"] ~env ~docv:"URL" ~doc)
133
134let username =
135 let doc = "Username (localpart or full @user:server)." in
136 let env = Cmd.Env.info "MATRIX_USERNAME" ~doc in
137 Arg.(required & opt (some string) None &
138 info ["username"; "u"] ~env ~docv:"USER" ~doc)
139
140let password =
141 let doc = "Password. For better security, use the $(b,MATRIX_PASSWORD) \
142 environment variable instead of this flag." in
143 let env = Cmd.Env.info "MATRIX_PASSWORD" ~doc in
144 Arg.(required & opt (some string) None &
145 info ["password"; "p"] ~env ~docv:"PASS" ~doc)
146
147let recipient =
148 let doc = "Recipient user ID (e.g., @user:matrix.org)." in
149 Arg.(required & opt (some string) None &
150 info ["recipient"; "r"] ~docv:"USER_ID" ~doc)
151
152let encrypted =
153 let doc = "Enable end-to-end encryption on newly created rooms. \
154 Note: Encryption requires key management which is not fully \
155 implemented; messages may not be decryptable by the recipient." in
156 Arg.(value & flag & info ["encrypted"; "e"] ~doc)
157
158let message =
159 let doc = "Message text to send." in
160 Arg.(required & pos 0 (some string) None & info [] ~docv:"MESSAGE" ~doc)
161
162let setup_log_term =
163 Term.(const setup_log $ Fmt_cli.style_renderer () $ Logs_cli.level ())
164
165let send_dm_term =
166 let run () homeserver username password recipient message encrypted =
167 send_dm ~homeserver ~username ~password ~recipient ~message ~encrypted
168 in
169 Term.(ret (const run $ setup_log_term $ homeserver $ username $
170 password $ recipient $ message $ encrypted))
171
172let cmd =
173 let doc = "Send a direct message to a Matrix user" in
174 let man = [
175 `S Manpage.s_description;
176 `P "Sends a one-off direct message to another Matrix user. Reuses an \
177 existing DM room if one exists, otherwise creates a new one. \
178 After sending, logs out.";
179 `S Manpage.s_examples;
180 `Pre " send_dm -s https://matrix.org -u @me:matrix.org \\\\";
181 `Pre " -p secret -r @them:matrix.org \"Hello!\"";
182 `P "Using environment variables:";
183 `Pre " export MATRIX_HOMESERVER=https://matrix.org";
184 `Pre " export MATRIX_USERNAME=@me:matrix.org";
185 `Pre " export MATRIX_PASSWORD=secret";
186 `Pre " send_dm -r @them:matrix.org \"Hello!\"";
187 `P "With E2E encryption (for new rooms):";
188 `Pre " send_dm --encrypted -s https://matrix.org ... \"Hello!\"";
189 `P "With debug logging:";
190 `Pre " send_dm -v -v -s https://matrix.org ... \"Hello!\"";
191 `S Manpage.s_environment;
192 `S Manpage.s_bugs;
193 `P "Report bugs at <https://github.com/matrix-org/ocaml-matrix/issues>.";
194 ] in
195 let info = Cmd.info "send_dm" ~version:"0.1.0" ~doc ~man in
196 Cmd.v info send_dm_term
197
198let () = exit (Cmd.eval cmd)