···
7
-
let () = Mirage_crypto_rng_unix.use_default ()
let src = Logs.Src.create "echo_bot" ~doc:"Zulip Echo Bot"
module Log = (val Logs.src_log src : Logs.LOG)
···
let handle_message ~config:_ ~storage ~identity ~message ~env:_ =
Log.debug (fun m -> m "Received message for processing");
29
-
(* Parse the incoming message *)
30
-
let extract_field fields name =
31
-
match List.assoc_opt name fields with
32
-
| Some (`String s) ->
33
-
Log.debug (fun m -> m "Extracted field %s: %s" name s);
36
-
Log.debug (fun m -> m "Field %s not found or not a string" name);
40
-
let extract_int_field fields name =
41
-
match List.assoc_opt name fields with
42
-
| Some (`Float f) ->
43
-
let i = int_of_float f in
44
-
Log.debug (fun m -> m "Extracted field %s: %d" name i);
47
-
Log.debug (fun m -> m "Field %s not found or not a number" name);
27
+
(* Use the new Message type for cleaner handling *)
53
-
Log.info (fun m -> m "Processing message with %d fields" (List.length fields));
55
-
(* Extract message details *)
56
-
let content = extract_field fields "content" in
57
-
let sender_email = extract_field fields "sender_email" in
58
-
let sender_full_name = extract_field fields "sender_full_name" in
59
-
let message_type = extract_field fields "type" in
60
-
let sender_id = extract_int_field fields "sender_id" in
61
-
let message_id = extract_int_field fields "id" in
63
-
(* Log message metadata *)
65
-
m "Message metadata: type=%s, sender=%s (%s), id=%s"
66
-
(Option.value message_type ~default:"unknown")
67
-
(Option.value sender_full_name ~default:"unknown")
68
-
(Option.value sender_email ~default:"unknown")
69
-
(Option.fold ~none:"none" ~some:string_of_int message_id));
29
+
| Message.Private { common; display_recipient = _ } ->
30
+
Log.info (fun m -> m "Processing private message from %s (ID: %d)"
31
+
common.sender_email common.id);
(* Check if this is our own message to avoid loops *)
let bot_email = Bot_handler.Identity.email identity in
73
-
let is_own_message = match sender_email with
75
-
let is_own = email = bot_email in
77
-
Log.debug (fun m -> m "Ignoring own message from %s" email);
82
-
if is_own_message then (
83
-
Log.debug (fun m -> m "Skipping response to own message");
35
+
if common.sender_email = bot_email then (
36
+
Log.debug (fun m -> m "Ignoring own message");
Ok Bot_handler.Response.None
(* Process the message content *)
87
-
let response_content = match content, sender_full_name, sender_id with
88
-
| Some msg, Some name, Some id ->
89
-
Log.info (fun m -> m "Processing message from %s (ID: %d): %s" name id msg);
40
+
let sender_name = common.sender_full_name in
41
+
let sender_id = common.sender_id in
42
+
let msg = common.content in
91
-
(* Remove bot mention if present *)
92
-
let mention = "@**" ^ Bot_handler.Identity.mention_name identity ^ "**" in
94
-
if String.starts_with ~prefix:mention msg then (
95
-
let cleaned = String.sub msg
96
-
(String.length mention)
97
-
(String.length msg - String.length mention) in
98
-
Log.debug (fun m -> m "Removed bot mention, cleaned message: %s" cleaned);
101
-
Log.debug (fun m -> m "No bot mention to remove");
44
+
Log.info (fun m -> m "Processing message from %s (ID: %d): %s" sender_name sender_id msg);
106
-
(* Create echo response *)
108
-
let lower_msg = String.lowercase_ascii cleaned_msg in
109
-
if cleaned_msg = "" then
110
-
Printf.sprintf "Hello %s! Send me a message and I'll echo it back!" name
111
-
else if lower_msg = "help" then
112
-
Printf.sprintf "Hi %s! I'm an echo bot with storage. Commands:\n\
113
-
• `help` - Show this help\n\
114
-
• `ping` - Test if I'm alive\n\
115
-
• `store <key> <value>` - Store a value\n\
116
-
• `get <key>` - Retrieve a value\n\
117
-
• `delete <key>` - Delete a stored value\n\
118
-
• `list` - List all stored keys\n\
119
-
• Any other message - I'll echo it back!" name
120
-
else if lower_msg = "ping" then (
121
-
Log.info (fun m -> m "Responding to ping from %s" name);
122
-
Printf.sprintf "Pong! 🏓 (from %s)" name
124
-
else if String.starts_with ~prefix:"store " lower_msg then (
125
-
(* Parse store command: store <key> <value> *)
126
-
let parts = String.sub cleaned_msg 6 (String.length cleaned_msg - 6) |> String.trim in
127
-
match String.index_opt parts ' ' with
129
-
let key = String.sub parts 0 idx |> String.trim in
130
-
let value = String.sub parts (idx + 1) (String.length parts - idx - 1) |> String.trim in
131
-
(match Bot_storage.put storage ~key ~value with
133
-
Log.info (fun m -> m "Stored key=%s value=%s for user %s" key value name);
134
-
Printf.sprintf "✅ Stored: `%s` = `%s`" key value
136
-
Log.err (fun m -> m "Failed to store key=%s: %s" key (Zulip.Error.message e));
137
-
Printf.sprintf "❌ Failed to store: %s" (Zulip.Error.message e))
139
-
"Usage: `store <key> <value>` - Example: `store name John`"
141
-
else if String.starts_with ~prefix:"get " lower_msg then (
142
-
(* Parse get command: get <key> *)
143
-
let key = String.sub cleaned_msg 4 (String.length cleaned_msg - 4) |> String.trim in
144
-
match Bot_storage.get storage ~key with
146
-
Log.info (fun m -> m "Retrieved key=%s value=%s for user %s" key value name);
147
-
Printf.sprintf "📦 `%s` = `%s`" key value
149
-
Log.info (fun m -> m "Key not found: %s" key);
150
-
Printf.sprintf "❓ Key not found: `%s`" key
152
-
else if String.starts_with ~prefix:"delete " lower_msg then (
153
-
(* Parse delete command: delete <key> *)
154
-
let key = String.sub cleaned_msg 7 (String.length cleaned_msg - 7) |> String.trim in
155
-
match Bot_storage.remove storage ~key with
157
-
Log.info (fun m -> m "Deleted key=%s for user %s" key name);
158
-
Printf.sprintf "🗑️ Deleted key: `%s`" key
160
-
Log.err (fun m -> m "Failed to delete key=%s: %s" key (Zulip.Error.message e));
161
-
Printf.sprintf "❌ Failed to delete: %s" (Zulip.Error.message e)
163
-
else if lower_msg = "list" then (
164
-
(* List all stored keys *)
165
-
match Bot_storage.keys storage with
166
-
| Ok keys when keys = [] ->
167
-
"📭 No keys stored yet. Use `store <key> <value>` to add data!"
169
-
let key_list = String.concat "\n" (List.map (fun k -> "• `" ^ k ^ "`") keys) in
170
-
Printf.sprintf "📋 Stored keys:\n%s\n\nUse `get <key>` to retrieve values." key_list
172
-
Printf.sprintf "❌ Failed to list keys: %s" (Zulip.Error.message e)
175
-
Printf.sprintf "Echo from %s: %s" name cleaned_msg
46
+
(* Remove bot mention using Message utility *)
47
+
let bot_email = Bot_handler.Identity.email identity in
48
+
let cleaned_msg = Message.strip_mention message ~user_email:bot_email in
49
+
Log.debug (fun m -> m "Cleaned message: %s" cleaned_msg);
178
-
Log.debug (fun m -> m "Generated response: %s" response);
51
+
(* Create echo response *)
52
+
let response_content =
53
+
let lower_msg = String.lowercase_ascii cleaned_msg in
54
+
if cleaned_msg = "" then
55
+
Printf.sprintf "Hello %s! Send me a message and I'll echo it back!" sender_name
56
+
else if lower_msg = "help" then
57
+
Printf.sprintf "Hi %s! I'm an echo bot with storage. Commands:\n\
58
+
• `help` - Show this help\n\
59
+
• `ping` - Test if I'm alive\n\
60
+
• `store <key> <value>` - Store a value\n\
61
+
• `get <key>` - Retrieve a value\n\
62
+
• `delete <key>` - Delete a stored value\n\
63
+
• `list` - List all stored keys\n\
64
+
• Any other message - I'll echo it back!" sender_name
65
+
else if lower_msg = "ping" then (
66
+
Log.info (fun m -> m "Responding to ping from %s" sender_name);
67
+
Printf.sprintf "Pong! 🏓 (from %s)" sender_name
69
+
else if String.starts_with ~prefix:"store " lower_msg then (
70
+
(* Parse store command: store <key> <value> *)
71
+
let parts = String.sub cleaned_msg 6 (String.length cleaned_msg - 6) |> String.trim in
72
+
match String.index_opt parts ' ' with
74
+
let key = String.sub parts 0 idx |> String.trim in
75
+
let value = String.sub parts (idx + 1) (String.length parts - idx - 1) |> String.trim in
76
+
(match Bot_storage.put storage ~key ~value with
78
+
Log.info (fun m -> m "Stored key=%s value=%s for user %s" key value sender_name);
79
+
Printf.sprintf "✅ Stored: `%s` = `%s`" key value
81
+
Log.err (fun m -> m "Failed to store key=%s: %s" key (Zulip.Error.message e));
82
+
Printf.sprintf "❌ Failed to store: %s" (Zulip.Error.message e))
84
+
"Usage: `store <key> <value>` - Example: `store name John`"
86
+
else if String.starts_with ~prefix:"get " lower_msg then (
87
+
(* Parse get command: get <key> *)
88
+
let key = String.sub cleaned_msg 4 (String.length cleaned_msg - 4) |> String.trim in
89
+
match Bot_storage.get storage ~key with
91
+
Log.info (fun m -> m "Retrieved key=%s value=%s for user %s" key value sender_name);
92
+
Printf.sprintf "📦 `%s` = `%s`" key value
94
+
Log.info (fun m -> m "Key not found: %s" key);
95
+
Printf.sprintf "❓ Key not found: `%s`" key
97
+
else if String.starts_with ~prefix:"delete " lower_msg then (
98
+
(* Parse delete command: delete <key> *)
99
+
let key = String.sub cleaned_msg 7 (String.length cleaned_msg - 7) |> String.trim in
100
+
match Bot_storage.remove storage ~key with
102
+
Log.info (fun m -> m "Deleted key=%s for user %s" key sender_name);
103
+
Printf.sprintf "🗑️ Deleted key: `%s`" key
105
+
Log.err (fun m -> m "Failed to delete key=%s: %s" key (Zulip.Error.message e));
106
+
Printf.sprintf "❌ Failed to delete: %s" (Zulip.Error.message e)
108
+
else if lower_msg = "list" then (
109
+
(* List all stored keys *)
110
+
match Bot_storage.keys storage with
111
+
| Ok keys when keys = [] ->
112
+
"📭 No keys stored yet. Use `store <key> <value>` to add data!"
114
+
let key_list = String.concat "\n" (List.map (fun k -> "• `" ^ k ^ "`") keys) in
115
+
Printf.sprintf "📋 Stored keys:\n%s\n\nUse `get <key>` to retrieve values." key_list
117
+
Printf.sprintf "❌ Failed to list keys: %s" (Zulip.Error.message e)
120
+
Printf.sprintf "Echo from %s: %s" sender_name cleaned_msg
181
-
| Some msg, None, _ ->
182
-
Log.warn (fun m -> m "Message without sender name: %s" msg);
183
-
Printf.sprintf "Echo: %s" msg
123
+
Log.debug (fun m -> m "Generated response: %s" response_content);
124
+
Log.info (fun m -> m "Sending private reply");
125
+
Ok (Bot_handler.Response.Reply response_content)
186
-
Log.warn (fun m -> m "Received message without content");
187
-
"I couldn't understand that message."
127
+
| Message.Stream { common; display_recipient; subject; _ } ->
128
+
Log.info (fun m -> m "Processing stream message from %s in %s/%s"
129
+
common.sender_email display_recipient subject);
190
-
Log.warn (fun m -> m "Incomplete message data");
191
-
"I couldn't process that message properly."
131
+
(* Check if this is our own message to avoid loops *)
132
+
let bot_email = Bot_handler.Identity.email identity in
133
+
if common.sender_email = bot_email then (
134
+
Log.debug (fun m -> m "Ignoring own message");
135
+
Ok Bot_handler.Response.None
137
+
(* Process the message content *)
138
+
let sender_name = common.sender_full_name in
194
-
(* Determine response type based on original message type *)
195
-
let response = match message_type with
196
-
| Some "private" ->
197
-
Log.info (fun m -> m "Sending private reply: %s" response_content);
198
-
Bot_handler.Response.Reply response_content
200
-
Log.info (fun m -> m "Sending stream reply: %s" response_content);
201
-
Bot_handler.Response.Reply response_content
203
-
Log.warn (fun m -> m "Unknown message type: %s" other);
204
-
Bot_handler.Response.None
206
-
Log.warn (fun m -> m "No message type specified");
207
-
Bot_handler.Response.None
140
+
(* Remove bot mention using Message utility *)
141
+
let cleaned_msg = Message.strip_mention message ~user_email:bot_email in
143
+
(* Create echo response *)
144
+
let response_content =
145
+
let lower_msg = String.lowercase_ascii cleaned_msg in
146
+
if cleaned_msg = "" then
147
+
Printf.sprintf "Hello %s! Send me a message and I'll echo it back!" sender_name
148
+
else if lower_msg = "help" then
149
+
Printf.sprintf "Hi %s! I'm an echo bot with storage. Commands:\n\
150
+
• `help` - Show this help\n\
151
+
• `ping` - Test if I'm alive\n\
152
+
• `store <key> <value>` - Store a value\n\
153
+
• `get <key>` - Retrieve a value\n\
154
+
• `delete <key>` - Delete a stored value\n\
155
+
• `list` - List all stored keys\n\
156
+
• Any other message - I'll echo it back!" sender_name
157
+
else if lower_msg = "ping" then (
158
+
Log.info (fun m -> m "Responding to ping from %s" sender_name);
159
+
Printf.sprintf "Pong! 🏓 (from %s)" sender_name
161
+
else if String.starts_with ~prefix:"store " lower_msg then (
162
+
(* Parse store command: store <key> <value> *)
163
+
let parts = String.sub cleaned_msg 6 (String.length cleaned_msg - 6) |> String.trim in
164
+
match String.index_opt parts ' ' with
166
+
let key = String.sub parts 0 idx |> String.trim in
167
+
let value = String.sub parts (idx + 1) (String.length parts - idx - 1) |> String.trim in
168
+
(match Bot_storage.put storage ~key ~value with
170
+
Log.info (fun m -> m "Stored key=%s value=%s for user %s" key value sender_name);
171
+
Printf.sprintf "✅ Stored: `%s` = `%s`" key value
173
+
Log.err (fun m -> m "Failed to store key=%s: %s" key (Zulip.Error.message e));
174
+
Printf.sprintf "❌ Failed to store: %s" (Zulip.Error.message e))
176
+
"Usage: `store <key> <value>` - Example: `store name John`"
178
+
else if String.starts_with ~prefix:"get " lower_msg then (
179
+
(* Parse get command: get <key> *)
180
+
let key = String.sub cleaned_msg 4 (String.length cleaned_msg - 4) |> String.trim in
181
+
match Bot_storage.get storage ~key with
183
+
Log.info (fun m -> m "Retrieved key=%s value=%s for user %s" key value sender_name);
184
+
Printf.sprintf "📦 `%s` = `%s`" key value
186
+
Log.info (fun m -> m "Key not found: %s" key);
187
+
Printf.sprintf "❓ Key not found: `%s`" key
189
+
else if String.starts_with ~prefix:"delete " lower_msg then (
190
+
(* Parse delete command: delete <key> *)
191
+
let key = String.sub cleaned_msg 7 (String.length cleaned_msg - 7) |> String.trim in
192
+
match Bot_storage.remove storage ~key with
194
+
Log.info (fun m -> m "Deleted key=%s for user %s" key sender_name);
195
+
Printf.sprintf "🗑️ Deleted key: `%s`" key
197
+
Log.err (fun m -> m "Failed to delete key=%s: %s" key (Zulip.Error.message e));
198
+
Printf.sprintf "❌ Failed to delete: %s" (Zulip.Error.message e)
200
+
else if lower_msg = "list" then (
201
+
(* List all stored keys *)
202
+
match Bot_storage.keys storage with
203
+
| Ok keys when keys = [] ->
204
+
"📭 No keys stored yet. Use `store <key> <value>` to add data!"
206
+
let key_list = String.concat "\n" (List.map (fun k -> "• `" ^ k ^ "`") keys) in
207
+
Printf.sprintf "📋 Stored keys:\n%s\n\nUse `get <key>` to retrieve values." key_list
209
+
Printf.sprintf "❌ Failed to list keys: %s" (Zulip.Error.message e)
212
+
Printf.sprintf "Echo from %s: %s" sender_name cleaned_msg
215
+
Log.debug (fun m -> m "Generated response: %s" response_content);
216
+
Log.info (fun m -> m "Sending stream reply");
217
+
Ok (Bot_handler.Response.Reply response_content)
213
-
Log.err (fun m -> m "Received non-object message: %s"
216
-
| `Bool _ -> "bool"
217
-
| `Float _ -> "float"
218
-
| `String _ -> "string"
220
-
| _ -> "unknown"));
219
+
| Message.Unknown _ ->
220
+
Log.err (fun m -> m "Received unknown message format");
Ok Bot_handler.Response.None
···
Cmd.v info Term.(const (run_echo_bot) $ config_file $ verbosity_term $ const env)
365
+
(* Initialize the cryptographic RNG for the application *)
366
+
Mirage_crypto_rng_unix.use_default ();
Eio_main.run @@ fun env ->
exit (Cmd.eval (bot_cmd env))