Skip to main content

netget/client/xmpp/
actions.rs

1//! XMPP client protocol actions implementation
2
3use crate::llm::actions::{
4    client_trait::{Client, ClientActionResult},
5    protocol_trait::Protocol,
6    ActionDefinition, Parameter,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14/// XMPP client connected event
15pub static XMPP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16    EventType::new(
17        "xmpp_connected",
18        "XMPP client successfully connected and authenticated",
19        json!({
20            "type": "send_message",
21            "to": "friend@example.com",
22            "body": "Hello!"
23        }),
24    )
25    .with_parameters(vec![Parameter {
26        name: "jid".to_string(),
27        type_hint: "string".to_string(),
28        description: "The JID (Jabber ID) of the connected client".to_string(),
29        required: true,
30    }])
31});
32
33/// XMPP client message received event
34pub static XMPP_CLIENT_MESSAGE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
35    EventType::new(
36        "xmpp_message_received",
37        "XMPP message received from another user",
38        json!({
39            "type": "send_message",
40            "to": "friend@example.com",
41            "body": "Thanks for your message!"
42        }),
43    )
44    .with_parameters(vec![
45        Parameter {
46            name: "from".to_string(),
47            type_hint: "string".to_string(),
48            description: "JID of the message sender".to_string(),
49            required: true,
50        },
51        Parameter {
52            name: "to".to_string(),
53            type_hint: "string".to_string(),
54            description: "JID of the message recipient".to_string(),
55            required: true,
56        },
57        Parameter {
58            name: "body".to_string(),
59            type_hint: "string".to_string(),
60            description: "Message body text".to_string(),
61            required: true,
62        },
63        Parameter {
64            name: "message_type".to_string(),
65            type_hint: "string".to_string(),
66            description: "Type of message (Chat, Groupchat, etc.)".to_string(),
67            required: true,
68        },
69    ])
70});
71
72/// XMPP client presence received event
73pub static XMPP_CLIENT_PRESENCE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
74    EventType::new(
75        "xmpp_presence_received",
76        "XMPP presence update received from a contact",
77        json!({
78            "type": "wait_for_more"
79        }),
80    )
81    .with_parameters(vec![
82        Parameter {
83            name: "from".to_string(),
84            type_hint: "string".to_string(),
85            description: "JID of the contact".to_string(),
86            required: true,
87        },
88        Parameter {
89            name: "presence_type".to_string(),
90            type_hint: "string".to_string(),
91            description: "Type of presence (Available, Unavailable, etc.)".to_string(),
92            required: true,
93        },
94        Parameter {
95            name: "show".to_string(),
96            type_hint: "string".to_string(),
97            description: "Availability indicator (away, chat, dnd, xa)".to_string(),
98            required: false,
99        },
100        Parameter {
101            name: "status".to_string(),
102            type_hint: "string".to_string(),
103            description: "Status message".to_string(),
104            required: false,
105        },
106    ])
107});
108
109/// XMPP client protocol action handler
110pub struct XmppClientProtocol;
111
112impl XmppClientProtocol {
113    pub fn new() -> Self {
114        Self
115    }
116}
117
118// Implement Protocol trait (common functionality)
119impl Protocol for XmppClientProtocol {
120    fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
121        vec![
122            crate::llm::actions::ParameterDefinition {
123                name: "jid".to_string(),
124                type_hint: "string".to_string(),
125                description: "JID (Jabber ID) to connect as (e.g., user@example.com). Names \
126                              the account only: the client connects to remote_addr, never to \
127                              a server looked up from the JID's domain"
128                    .to_string(),
129                required: true,
130                example: serde_json::json!("alice@example.com"),
131                default: None,
132            },
133            crate::llm::actions::ParameterDefinition {
134                name: "password".to_string(),
135                type_hint: "string".to_string(),
136                description: "Password for authentication".to_string(),
137                required: true,
138                example: serde_json::json!("secret"),
139                default: None,
140            },
141            crate::llm::actions::ParameterDefinition {
142                name: "session_timeout_secs".to_string(),
143                type_hint: "integer".to_string(),
144                description: format!(
145                    "Seconds to wait for the server to accept the session (TCP connect, \
146                     STARTTLS, SASL authentication, resource binding) before the client fails, \
147                     1..{}",
148                    super::MAX_SESSION_TIMEOUT_SECS
149                ),
150                required: false,
151                example: serde_json::json!(30),
152                default: Some(serde_json::json!(super::SESSION_TIMEOUT.as_secs())),
153            },
154        ]
155    }
156    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
157        vec![
158            ActionDefinition {
159                name: "send_message".to_string(),
160                description: "Send a message to a JID".to_string(),
161                parameters: vec![
162                    Parameter {
163                        name: "to".to_string(),
164                        type_hint: "string".to_string(),
165                        description: "JID of the recipient".to_string(),
166                        required: true,
167                    },
168                    Parameter {
169                        name: "body".to_string(),
170                        type_hint: "string".to_string(),
171                        description: "Message body text".to_string(),
172                        required: true,
173                    },
174                ],
175                example: json!({
176                    "type": "send_message",
177                    "to": "friend@example.com",
178                    "body": "Hello from NetGet!"
179                }),
180                log_template: None,
181            },
182            ActionDefinition {
183                name: "send_presence".to_string(),
184                description: "Send presence update".to_string(),
185                parameters: vec![
186                    Parameter {
187                        name: "show".to_string(),
188                        type_hint: "string".to_string(),
189                        description: "Availability (away, chat, dnd, xa)".to_string(),
190                        required: false,
191                    },
192                    Parameter {
193                        name: "status".to_string(),
194                        type_hint: "string".to_string(),
195                        description: "Status message".to_string(),
196                        required: false,
197                    },
198                ],
199                example: json!({
200                    "type": "send_presence",
201                    "show": "away",
202                    "status": "Out for lunch"
203                }),
204                log_template: None,
205            },
206            ActionDefinition {
207                name: "disconnect".to_string(),
208                description: "Disconnect from the XMPP server".to_string(),
209                parameters: vec![],
210                example: json!({
211                    "type": "disconnect"
212                }),
213                log_template: None,
214            },
215        ]
216    }
217    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
218        vec![
219            ActionDefinition {
220                name: "send_message".to_string(),
221                description: "Send a message in response to a received message or event"
222                    .to_string(),
223                parameters: vec![
224                    Parameter {
225                        name: "to".to_string(),
226                        type_hint: "string".to_string(),
227                        description: "JID of the recipient".to_string(),
228                        required: true,
229                    },
230                    Parameter {
231                        name: "body".to_string(),
232                        type_hint: "string".to_string(),
233                        description: "Message body text".to_string(),
234                        required: true,
235                    },
236                ],
237                example: json!({
238                    "type": "send_message",
239                    "to": "friend@example.com",
240                    "body": "Thanks for your message!"
241                }),
242                log_template: None,
243            },
244            ActionDefinition {
245                name: "wait_for_more".to_string(),
246                description: "Wait for more events before responding".to_string(),
247                parameters: vec![],
248                example: json!({
249                    "type": "wait_for_more"
250                }),
251                log_template: None,
252            },
253        ]
254    }
255    fn protocol_name(&self) -> &'static str {
256        "XMPP"
257    }
258    fn get_event_types(&self) -> Vec<EventType> {
259        vec![
260            EventType::new(
261                "xmpp_connected",
262                "Triggered when XMPP client connects and authenticates",
263                json!({"type": "placeholder", "event_id": "xmpp_connected"}),
264            ),
265            EventType::new(
266                "xmpp_message_received",
267                "Triggered when XMPP message is received",
268                json!({"type": "placeholder", "event_id": "xmpp_message_received"}),
269            ),
270            EventType::new(
271                "xmpp_presence_received",
272                "Triggered when presence update is received",
273                json!({"type": "placeholder", "event_id": "xmpp_presence_received"}),
274            ),
275        ]
276    }
277    fn stack_name(&self) -> &'static str {
278        "ETH>IP>TCP>TLS>XMPP"
279    }
280    fn keywords(&self) -> Vec<&'static str> {
281        vec![
282            "xmpp",
283            "xmpp client",
284            "jabber",
285            "connect to xmpp",
286            "connect to jabber",
287        ]
288    }
289    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
290        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
291
292        ProtocolMetadataV2::builder()
293            .state(DevelopmentState::Experimental)
294            .implementation("tokio-xmpp library for async XMPP client")
295            .llm_control("Send messages, presence updates, respond to incoming stanzas")
296            .e2e_testing(
297                "Loopback listeners pin the connect target, the pending-session status and the \
298                 command channel; a session against a real prosody/ejabberd exists only in \
299                 #[ignore]d tests",
300            )
301            .build()
302    }
303    fn description(&self) -> &'static str {
304        "XMPP/Jabber client for instant messaging"
305    }
306    fn example_prompt(&self) -> &'static str {
307        "Connect to XMPP as user@example.com and respond to incoming messages"
308    }
309    fn group_name(&self) -> &'static str {
310        "Application"
311    }
312
313    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
314        use crate::llm::actions::StartupExamples;
315        use serde_json::json;
316
317        StartupExamples::new(
318            // LLM mode: LLM controls XMPP messaging
319            json!({
320                "type": "open_client",
321                "remote_addr": "xmpp.example.com:5222",
322                "base_stack": "xmpp",
323                "startup_params": {"jid": "alice@example.com", "password": "secret"},
324                "instruction": "Send presence and auto-reply to all incoming messages"
325            }),
326            // Script mode: Code-based deterministic responses
327            json!({
328                "type": "open_client",
329                "remote_addr": "xmpp.example.com:5222",
330                "base_stack": "xmpp",
331                "startup_params": {"jid": "alice@example.com", "password": "secret"},
332                "event_handlers": [{
333                    "event_pattern": "xmpp_message_received",
334                    "handler": {
335                        "type": "script",
336                        "language": "python",
337                        "code": "<xmpp_client_handler>"
338                    }
339                }]
340            }),
341            // Static mode: Fixed XMPP presence on connect
342            json!({
343                "type": "open_client",
344                "remote_addr": "xmpp.example.com:5222",
345                "base_stack": "xmpp",
346                "startup_params": {"jid": "alice@example.com", "password": "secret"},
347                "event_handlers": [
348                    {
349                        "event_pattern": "xmpp_connected",
350                        "handler": {
351                            "type": "static",
352                            "actions": [{
353                                "type": "send_presence",
354                                "show": "chat",
355                                "status": "Available"
356                            }]
357                        }
358                    },
359                    {
360                        "event_pattern": "xmpp_message_received",
361                        "handler": {
362                            "type": "static",
363                            "actions": [{
364                                "type": "wait_for_more"
365                            }]
366                        }
367                    }
368                ]
369            }),
370        )
371    }
372}
373
374// Implement Client trait (client-specific functionality)
375impl Client for XmppClientProtocol {
376    fn connect(
377        &self,
378        ctx: crate::protocol::ConnectContext,
379    ) -> std::pin::Pin<
380        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
381    > {
382        Box::pin(async move {
383            use crate::client::xmpp::XmppClientConnection;
384            XmppClientConnection::connect_with_llm_actions(
385                ctx.remote_addr,
386                ctx.llm_client,
387                ctx.state,
388                ctx.status_tx,
389                ctx.client_id,
390                ctx.startup_params,
391            )
392            .await
393        })
394    }
395    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
396        let action_type = action
397            .get("type")
398            .and_then(|v| v.as_str())
399            .context("Missing 'type' field in action")?;
400
401        match action_type {
402            "send_message" => {
403                let to = action
404                    .get("to")
405                    .and_then(|v| v.as_str())
406                    .context("Missing 'to' field")?;
407
408                let body = action
409                    .get("body")
410                    .and_then(|v| v.as_str())
411                    .context("Missing 'body' field")?;
412
413                Ok(ClientActionResult::Custom {
414                    name: "send_message".to_string(),
415                    data: json!({
416                        "to": to,
417                        "body": body,
418                    }),
419                })
420            }
421            "send_presence" => {
422                let show = action.get("show").and_then(|v| v.as_str());
423                let status = action.get("status").and_then(|v| v.as_str());
424
425                Ok(ClientActionResult::Custom {
426                    name: "send_presence".to_string(),
427                    data: json!({
428                        "show": show,
429                        "status": status,
430                    }),
431                })
432            }
433            "disconnect" => Ok(ClientActionResult::Disconnect),
434            "wait_for_more" => Ok(ClientActionResult::WaitForMore),
435            _ => Err(anyhow::anyhow!(
436                "Unknown XMPP client action: {}",
437                action_type
438            )),
439        }
440    }
441}