1use crate::llm::actions::{
4 protocol_trait::{ActionResult, Protocol, Server},
5 ActionDefinition, Parameter,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::server::irc::wire::{cap_line, reject_line_breaks, reject_not_a_word};
10use crate::state::app_state::AppState;
11use anyhow::{Context, Result};
12use serde_json::json;
13use std::sync::LazyLock;
14use tracing::debug;
15
16pub struct IrcProtocol;
24
25impl IrcProtocol {
26 pub fn new() -> Self {
27 Self
28 }
29}
30
31impl Protocol for IrcProtocol {
33 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
34 Vec::new()
40 }
41 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
42 Vec::new()
44 }
45 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
46 vec![
47 send_irc_message_action(),
48 send_irc_welcome_action(),
49 send_irc_pong_action(),
50 send_irc_join_action(),
51 send_irc_part_action(),
52 send_irc_privmsg_action(),
53 send_irc_notice_action(),
54 send_irc_numeric_action(),
55 wait_for_more_action(),
56 close_connection_action(),
57 ]
58 }
59 fn protocol_name(&self) -> &'static str {
60 "IRC"
61 }
62 fn get_event_types(&self) -> Vec<EventType> {
63 get_irc_event_types()
64 }
65 fn stack_name(&self) -> &'static str {
66 "ETH>IP>TCP>IRC"
67 }
68 fn keywords(&self) -> Vec<&'static str> {
69 vec!["irc", "chat"]
70 }
71 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
72 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
73
74 ProtocolMetadataV2::builder()
75 .state(DevelopmentState::Experimental)
76 .well_known_port(6667)
77 .implementation("Manual line-based IRC parsing, plain TCP only")
78 .llm_control("Every inbound line; the model composes every reply")
79 .e2e_testing("Raw TCP client issuing NICK/USER/JOIN/PRIVMSG")
80 .notes(
81 "Single-client view: no channel membership, no nick registry, no broadcast \
82 between connections, no TLS. The model tracks all of that itself.",
83 )
84 .max_inbound_bytes(crate::server::irc::wire::MAX_IRC_READ_LINE)
85 .build()
86 }
87 fn description(&self) -> &'static str {
88 "IRC chat server"
89 }
90 fn example_prompt(&self) -> &'static str {
91 "Start an IRC server"
92 }
93 fn group_name(&self) -> &'static str {
94 "Application"
95 }
96 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
97 use crate::llm::actions::StartupExamples;
98 StartupExamples::new(
99 json!({
101 "type": "open_server",
102 "port": 6667,
103 "base_stack": "irc",
104 "instruction": "IRC chat server, send welcome on NICK/USER, echo messages back"
105 }),
106 json!({
108 "type": "open_server",
109 "port": 6667,
110 "base_stack": "irc",
111 "event_handlers": [{
112 "event_pattern": "irc_message_received",
113 "handler": {
114 "type": "script",
115 "language": "python",
116 "code": "# Handle IRC messages\nmsg = event.get('message', '').strip()\nif msg.startswith('NICK'):\n nick = msg.split()[1] if len(msg.split()) > 1 else 'guest'\n respond([{'type': 'send_irc_welcome', 'nickname': nick, 'server': 'irc.netget.local', 'message': 'Welcome to NetGet IRC'}])\nelif msg.startswith('PING'):\n token = msg.split(':')[1] if ':' in msg else ''\n respond([{'type': 'send_irc_pong', 'token': token}])\nelif msg.startswith('JOIN'):\n channel = msg.split()[1] if len(msg.split()) > 1 else '#general'\n respond([{'type': 'send_irc_join', 'nickname': 'guest', 'channel': channel}])\nelse:\n respond([{'type': 'wait_for_more'}])"
117 }
118 }]
119 }),
120 json!({
122 "type": "open_server",
123 "port": 6667,
124 "base_stack": "irc",
125 "event_handlers": [{
126 "event_pattern": "irc_message_received",
127 "handler": {
128 "type": "static",
129 "actions": [{
130 "type": "send_irc_welcome",
131 "nickname": "guest",
132 "server": "irc.netget.local",
133 "message": "Welcome to NetGet IRC"
134 }]
135 }
136 }]
137 }),
138 )
139 }
140}
141
142impl Server for IrcProtocol {
144 fn spawn(
145 &self,
146 ctx: crate::protocol::SpawnContext,
147 ) -> std::pin::Pin<
148 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
149 > {
150 Box::pin(async move {
151 use crate::server::irc::IrcServer;
152
153 IrcServer::spawn_with_llm_actions(
154 ctx.legacy_listen_addr(),
155 ctx.llm_client,
156 ctx.state,
157 ctx.status_tx,
158 ctx.server_id,
159 )
160 .await
161 })
162 }
163 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
164 let action_type = action
165 .get("type")
166 .and_then(|v| v.as_str())
167 .context("Missing 'type' field in action")?;
168
169 match action_type {
170 "send_irc_message" => self.execute_send_irc_message(action),
171 "send_irc_welcome" => self.execute_send_irc_welcome(action),
172 "send_irc_pong" => self.execute_send_irc_pong(action),
173 "send_irc_join" => self.execute_send_irc_join(action),
174 "send_irc_part" => self.execute_send_irc_part(action),
175 "send_irc_privmsg" => self.execute_send_irc_privmsg(action),
176 "send_irc_notice" => self.execute_send_irc_notice(action),
177 "send_irc_numeric" => self.execute_send_irc_numeric(action),
178 "wait_for_more" => Ok(ActionResult::WaitForMore),
179 "close_connection" => Ok(ActionResult::CloseConnection),
180 _ => Err(anyhow::anyhow!("Unknown IRC action: {}", action_type)),
181 }
182 }
183}
184
185impl IrcProtocol {
186 fn execute_send_irc_message(&self, action: serde_json::Value) -> Result<ActionResult> {
187 let message = action
188 .get("message")
189 .and_then(|v| v.as_str())
190 .context("Missing 'message' parameter")?;
191
192 let body = message.trim_end_matches('\n').trim_end_matches('\r');
197 reject_line_breaks("message", body)?;
198
199 let formatted = format!("{body}\r\n");
200
201 debug!("IRC sending message: {}", formatted.trim());
202 Ok(ActionResult::Output(cap_line(
203 "send_irc_message",
204 formatted,
205 )))
206 }
207
208 fn execute_send_irc_welcome(&self, action: serde_json::Value) -> Result<ActionResult> {
209 let nickname = action
210 .get("nickname")
211 .and_then(|v| v.as_str())
212 .context("Missing 'nickname' parameter")?;
213
214 let server = action
215 .get("server")
216 .and_then(|v| v.as_str())
217 .unwrap_or("irc.server");
218
219 let message = action
220 .get("message")
221 .and_then(|v| v.as_str())
222 .unwrap_or("Welcome to the IRC Network");
223
224 reject_not_a_word("nickname", nickname)?;
225 reject_not_a_word("server", server)?;
226 reject_line_breaks("message", message)?;
227
228 let response = format!(":{} 001 {} :{}\r\n", server, nickname, message);
230
231 debug!("IRC sending welcome: {}", response.trim());
232 Ok(ActionResult::Output(cap_line("send_irc_welcome", response)))
233 }
234
235 fn execute_send_irc_pong(&self, action: serde_json::Value) -> Result<ActionResult> {
236 let token = action
237 .get("token")
238 .and_then(|v| v.as_str())
239 .context("Missing 'token' parameter")?;
240
241 reject_line_breaks("token", token)?;
242
243 let response = format!("PONG :{}\r\n", token);
244
245 debug!("IRC sending PONG: {}", token);
246 Ok(ActionResult::Output(cap_line("send_irc_pong", response)))
247 }
248
249 fn execute_send_irc_join(&self, action: serde_json::Value) -> Result<ActionResult> {
250 let nickname = action
251 .get("nickname")
252 .and_then(|v| v.as_str())
253 .context("Missing 'nickname' parameter")?;
254
255 let channel = action
256 .get("channel")
257 .and_then(|v| v.as_str())
258 .context("Missing 'channel' parameter")?;
259
260 let user = action
261 .get("user")
262 .and_then(|v| v.as_str())
263 .unwrap_or("user");
264
265 let host = action
266 .get("host")
267 .and_then(|v| v.as_str())
268 .unwrap_or("localhost");
269
270 for (field, value) in [
271 ("nickname", nickname),
272 ("channel", channel),
273 ("user", user),
274 ("host", host),
275 ] {
276 reject_not_a_word(field, value)?;
277 }
278
279 let response = format!(":{nickname}!{user}@{host} JOIN {channel}\r\n");
281
282 debug!("IRC sending JOIN: {} to {}", nickname, channel);
283 Ok(ActionResult::Output(cap_line("send_irc_join", response)))
284 }
285
286 fn execute_send_irc_part(&self, action: serde_json::Value) -> Result<ActionResult> {
287 let nickname = action
288 .get("nickname")
289 .and_then(|v| v.as_str())
290 .context("Missing 'nickname' parameter")?;
291
292 let channel = action
293 .get("channel")
294 .and_then(|v| v.as_str())
295 .context("Missing 'channel' parameter")?;
296
297 let user = action
298 .get("user")
299 .and_then(|v| v.as_str())
300 .unwrap_or("user");
301
302 let host = action
303 .get("host")
304 .and_then(|v| v.as_str())
305 .unwrap_or("localhost");
306
307 let reason = action.get("reason").and_then(|v| v.as_str());
308
309 for (field, value) in [
310 ("nickname", nickname),
311 ("channel", channel),
312 ("user", user),
313 ("host", host),
314 ] {
315 reject_not_a_word(field, value)?;
316 }
317 if let Some(reason) = reason {
318 reject_line_breaks("reason", reason)?;
319 }
320
321 let response = if let Some(reason) = reason {
323 format!(":{nickname}!{user}@{host} PART {channel} :{reason}\r\n")
324 } else {
325 format!(":{nickname}!{user}@{host} PART {channel}\r\n")
326 };
327
328 debug!("IRC sending PART: {} from {}", nickname, channel);
329 Ok(ActionResult::Output(cap_line("send_irc_part", response)))
330 }
331
332 fn execute_send_irc_privmsg(&self, action: serde_json::Value) -> Result<ActionResult> {
333 let source = action
334 .get("source")
335 .and_then(|v| v.as_str())
336 .context("Missing 'source' parameter")?;
337
338 let target = action
339 .get("target")
340 .and_then(|v| v.as_str())
341 .context("Missing 'target' parameter")?;
342
343 let message = action
344 .get("message")
345 .and_then(|v| v.as_str())
346 .context("Missing 'message' parameter")?;
347
348 reject_not_a_word("source", source)?;
349 reject_not_a_word("target", target)?;
350 reject_line_breaks("message", message)?;
351
352 let response = format!(":{} PRIVMSG {} :{}\r\n", source, target, message);
354
355 debug!("IRC sending PRIVMSG from {} to {}", source, target);
356 Ok(ActionResult::Output(cap_line("send_irc_privmsg", response)))
357 }
358
359 fn execute_send_irc_notice(&self, action: serde_json::Value) -> Result<ActionResult> {
360 let source = action
361 .get("source")
362 .and_then(|v| v.as_str())
363 .context("Missing 'source' parameter")?;
364
365 let target = action
366 .get("target")
367 .and_then(|v| v.as_str())
368 .context("Missing 'target' parameter")?;
369
370 let message = action
371 .get("message")
372 .and_then(|v| v.as_str())
373 .context("Missing 'message' parameter")?;
374
375 reject_not_a_word("source", source)?;
376 reject_not_a_word("target", target)?;
377 reject_line_breaks("message", message)?;
378
379 let response = format!(":{} NOTICE {} :{}\r\n", source, target, message);
381
382 debug!("IRC sending NOTICE from {} to {}", source, target);
383 Ok(ActionResult::Output(cap_line("send_irc_notice", response)))
384 }
385
386 fn execute_send_irc_numeric(&self, action: serde_json::Value) -> Result<ActionResult> {
387 let server = action
388 .get("server")
389 .and_then(|v| v.as_str())
390 .unwrap_or("irc.server");
391
392 let code = action
393 .get("code")
394 .and_then(|v| v.as_u64())
395 .context("Missing 'code' parameter")?;
396
397 let target = action
398 .get("target")
399 .and_then(|v| v.as_str())
400 .context("Missing 'target' parameter")?;
401
402 let message = action
403 .get("message")
404 .and_then(|v| v.as_str())
405 .context("Missing 'message' parameter")?;
406
407 if !(1..=999).contains(&code) {
411 anyhow::bail!(
412 "Invalid 'code' {code}: an IRC numeric is a three-digit reply code between \
413 001 and 999 (e.g. 001 RPL_WELCOME, 332 RPL_TOPIC, 353 RPL_NAMREPLY)."
414 );
415 }
416 reject_not_a_word("server", server)?;
417 reject_not_a_word("target", target)?;
418 reject_line_breaks("message", message)?;
419
420 let response = format!(":{} {:03} {} :{}\r\n", server, code, target, message);
422
423 debug!("IRC sending numeric {}: {}", code, message);
424 Ok(ActionResult::Output(cap_line("send_irc_numeric", response)))
425 }
426}
427
428fn send_irc_message_action() -> ActionDefinition {
431 ActionDefinition {
432 name: "send_irc_message".to_string(),
433 description: "Send one raw IRC line, for a verb the other actions do not cover. \
434 Exactly one message: the CRLF is added for you and the text must not \
435 contain CR, LF or NUL. RFC 1459 caps a line at 512 bytes including that \
436 CRLF, and a longer one is truncated."
437 .to_string(),
438 parameters: vec![Parameter {
439 name: "message".to_string(),
440 type_hint: "string".to_string(),
441 description: "One IRC protocol line without its line ending, e.g. \
442 \":server NOTICE * :Looking up your hostname\". Must not contain CR, \
443 LF or NUL - use one action per message."
444 .to_string(),
445 required: true,
446 }],
447 example: json!({
448 "type": "send_irc_message",
449 "message": ":server NOTICE * :Looking up your hostname"
450 }),
451 log_template: Some(
452 LogTemplate::new()
453 .with_info("-> IRC: {preview(message,60)}")
454 .with_debug("IRC send_irc_message: {message}"),
455 ),
456 }
457}
458
459fn send_irc_welcome_action() -> ActionDefinition {
460 ActionDefinition {
461 name: "send_irc_welcome".to_string(),
462 description: "Send IRC welcome message (numeric 001 - RPL_WELCOME)".to_string(),
463 parameters: vec![
464 Parameter {
465 name: "nickname".to_string(),
466 type_hint: "string".to_string(),
467 description: "Client nickname".to_string(),
468 required: true,
469 },
470 Parameter {
471 name: "server".to_string(),
472 type_hint: "string".to_string(),
473 description: "Server name (default: irc.server)".to_string(),
474 required: false,
475 },
476 Parameter {
477 name: "message".to_string(),
478 type_hint: "string".to_string(),
479 description: "Welcome message (default: 'Welcome to the IRC Network')".to_string(),
480 required: false,
481 },
482 ],
483 example: json!({
484 "type": "send_irc_welcome",
485 "nickname": "alice",
486 "server": "irc.example.com",
487 "message": "Welcome to the IRC Network, alice!"
488 }),
489 log_template: Some(
490 LogTemplate::new()
491 .with_info("-> IRC 001 {nickname}: {message}")
492 .with_debug("IRC send_irc_welcome: nickname={nickname}, server={server}"),
493 ),
494 }
495}
496
497fn send_irc_pong_action() -> ActionDefinition {
498 ActionDefinition {
499 name: "send_irc_pong".to_string(),
500 description: "Send IRC PONG response to PING".to_string(),
501 parameters: vec![Parameter {
502 name: "token".to_string(),
503 type_hint: "string".to_string(),
504 description: "Token from PING command".to_string(),
505 required: true,
506 }],
507 example: json!({
508 "type": "send_irc_pong",
509 "token": "1234567890"
510 }),
511 log_template: Some(
512 LogTemplate::new()
513 .with_info("-> IRC PONG :{token}")
514 .with_debug("IRC send_irc_pong: token={token}"),
515 ),
516 }
517}
518
519fn send_irc_join_action() -> ActionDefinition {
520 ActionDefinition {
521 name: "send_irc_join".to_string(),
522 description: "Send IRC JOIN confirmation".to_string(),
523 parameters: vec![
524 Parameter {
525 name: "nickname".to_string(),
526 type_hint: "string".to_string(),
527 description: "Client nickname".to_string(),
528 required: true,
529 },
530 Parameter {
531 name: "channel".to_string(),
532 type_hint: "string".to_string(),
533 description: "Channel name (e.g., #general)".to_string(),
534 required: true,
535 },
536 Parameter {
537 name: "user".to_string(),
538 type_hint: "string".to_string(),
539 description: "Username (default: user)".to_string(),
540 required: false,
541 },
542 Parameter {
543 name: "host".to_string(),
544 type_hint: "string".to_string(),
545 description: "Hostname (default: localhost)".to_string(),
546 required: false,
547 },
548 ],
549 example: json!({
550 "type": "send_irc_join",
551 "nickname": "alice",
552 "channel": "#general"
553 }),
554 log_template: Some(
555 LogTemplate::new()
556 .with_info("-> IRC {nickname} JOIN {channel}")
557 .with_debug("IRC send_irc_join: nickname={nickname}, channel={channel}"),
558 ),
559 }
560}
561
562fn send_irc_part_action() -> ActionDefinition {
563 ActionDefinition {
564 name: "send_irc_part".to_string(),
565 description: "Send IRC PART confirmation (leaving channel)".to_string(),
566 parameters: vec![
567 Parameter {
568 name: "nickname".to_string(),
569 type_hint: "string".to_string(),
570 description: "Client nickname".to_string(),
571 required: true,
572 },
573 Parameter {
574 name: "channel".to_string(),
575 type_hint: "string".to_string(),
576 description: "Channel name (e.g., #general)".to_string(),
577 required: true,
578 },
579 Parameter {
580 name: "user".to_string(),
581 type_hint: "string".to_string(),
582 description: "Username (default: user)".to_string(),
583 required: false,
584 },
585 Parameter {
586 name: "host".to_string(),
587 type_hint: "string".to_string(),
588 description: "Hostname (default: localhost)".to_string(),
589 required: false,
590 },
591 Parameter {
592 name: "reason".to_string(),
593 type_hint: "string".to_string(),
594 description: "Part reason (optional)".to_string(),
595 required: false,
596 },
597 ],
598 example: json!({
599 "type": "send_irc_part",
600 "nickname": "alice",
601 "channel": "#general",
602 "reason": "Goodbye!"
603 }),
604 log_template: Some(
605 LogTemplate::new()
606 .with_info("-> IRC {nickname} PART {channel}")
607 .with_debug(
608 "IRC send_irc_part: nickname={nickname}, channel={channel}, reason={reason}",
609 ),
610 ),
611 }
612}
613
614fn send_irc_privmsg_action() -> ActionDefinition {
615 ActionDefinition {
616 name: "send_irc_privmsg".to_string(),
617 description: "Send IRC PRIVMSG (chat message)".to_string(),
618 parameters: vec![
619 Parameter {
620 name: "source".to_string(),
621 type_hint: "string".to_string(),
622 description: "Source (nickname or server). One word, no leading ':' - the colon \
623 is added for you."
624 .to_string(),
625 required: true,
626 },
627 Parameter {
628 name: "target".to_string(),
629 type_hint: "string".to_string(),
630 description: "Target (nickname or channel). One word - a space here would shift \
631 every later parameter."
632 .to_string(),
633 required: true,
634 },
635 Parameter {
636 name: "message".to_string(),
637 type_hint: "string".to_string(),
638 description: "Message text. Must not contain CR, LF or NUL: an embedded line \
639 break would forge a second IRC command from this source rather \
640 than continuing the message. Truncated at the RFC 1459 512-byte \
641 line limit."
642 .to_string(),
643 required: true,
644 },
645 ],
646 example: json!({
647 "type": "send_irc_privmsg",
648 "source": "bot",
649 "target": "alice",
650 "message": "Hello, alice!"
651 }),
652 log_template: Some(
653 LogTemplate::new()
654 .with_info("-> IRC {source} -> {target}: {preview(message,40)}")
655 .with_debug("IRC send_irc_privmsg: source={source}, target={target}"),
656 ),
657 }
658}
659
660fn send_irc_notice_action() -> ActionDefinition {
661 ActionDefinition {
662 name: "send_irc_notice".to_string(),
663 description: "Send IRC NOTICE (notification message)".to_string(),
664 parameters: vec![
665 Parameter {
666 name: "source".to_string(),
667 type_hint: "string".to_string(),
668 description: "Source (nickname or server). One word, no leading ':' - the colon \
669 is added for you."
670 .to_string(),
671 required: true,
672 },
673 Parameter {
674 name: "target".to_string(),
675 type_hint: "string".to_string(),
676 description: "Target (nickname or channel). One word - a space here would shift \
677 every later parameter."
678 .to_string(),
679 required: true,
680 },
681 Parameter {
682 name: "message".to_string(),
683 type_hint: "string".to_string(),
684 description: "Notice text. Must not contain CR, LF or NUL, and is truncated at \
685 the RFC 1459 512-byte line limit."
686 .to_string(),
687 required: true,
688 },
689 ],
690 example: json!({
691 "type": "send_irc_notice",
692 "source": "server",
693 "target": "alice",
694 "message": "Server maintenance in 5 minutes"
695 }),
696 log_template: Some(
697 LogTemplate::new()
698 .with_info("-> IRC NOTICE {source} -> {target}")
699 .with_debug(
700 "IRC send_irc_notice: source={source}, target={target}, message={message}",
701 ),
702 ),
703 }
704}
705
706fn send_irc_numeric_action() -> ActionDefinition {
707 ActionDefinition {
708 name: "send_irc_numeric".to_string(),
709 description: "Send IRC numeric response (e.g., 332 for topic, 353 for names)".to_string(),
710 parameters: vec![
711 Parameter {
712 name: "code".to_string(),
713 type_hint: "number".to_string(),
714 description: "Three-digit reply code between 001 and 999 (e.g. 332 RPL_TOPIC, \
715 353 RPL_NAMREPLY, 366 RPL_ENDOFNAMES)."
716 .to_string(),
717 required: true,
718 },
719 Parameter {
720 name: "target".to_string(),
721 type_hint: "string".to_string(),
722 description: "Target nickname".to_string(),
723 required: true,
724 },
725 Parameter {
726 name: "message".to_string(),
727 type_hint: "string".to_string(),
728 description: "Message text".to_string(),
729 required: true,
730 },
731 Parameter {
732 name: "server".to_string(),
733 type_hint: "string".to_string(),
734 description: "Server name (default: irc.server)".to_string(),
735 required: false,
736 },
737 ],
738 example: json!({
739 "type": "send_irc_numeric",
740 "code": 332,
741 "target": "alice",
742 "message": "#general Welcome to our channel!"
743 }),
744 log_template: Some(
745 LogTemplate::new()
746 .with_info("-> IRC {code} {target}: {preview(message,40)}")
747 .with_debug("IRC send_irc_numeric: code={code}, target={target}"),
748 ),
749 }
750}
751
752fn wait_for_more_action() -> ActionDefinition {
753 ActionDefinition {
754 name: "wait_for_more".to_string(),
755 description: "Wait for more data before responding".to_string(),
756 parameters: vec![],
757 example: json!({
758 "type": "wait_for_more"
759 }),
760 log_template: Some(
761 LogTemplate::new()
762 .with_info("IRC waiting for more data")
763 .with_debug("IRC wait_for_more"),
764 ),
765 }
766}
767
768fn close_connection_action() -> ActionDefinition {
769 ActionDefinition {
770 name: "close_connection".to_string(),
771 description: "Close the IRC connection".to_string(),
772 parameters: vec![],
773 example: json!({
774 "type": "close_connection"
775 }),
776 log_template: Some(
777 LogTemplate::new()
778 .with_info("IRC connection closed")
779 .with_debug("IRC close_connection"),
780 ),
781 }
782}
783
784pub static IRC_MESSAGE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
789 EventType::new(
790 "irc_message_received",
791 "One IRC protocol line received from a client (NICK, USER, JOIN, PRIVMSG, PING, QUIT, ...). \
792 The server does not speak first, so registration begins with the client's NICK/USER.",
793 json!({"type": "send_irc_welcome", "nickname": "alice", "server": "irc.example.com", "message": "Welcome to the IRC Network"}),
794 )
795 .with_parameters(vec![Parameter {
796 name: "message".to_string(),
797 type_hint: "string".to_string(),
798 description: "The IRC message line received, with the trailing CRLF stripped \
799 (e.g. 'NICK alice', 'PRIVMSG #general :hello')"
800 .to_string(),
801 required: true,
802 }])
803 .with_actions(vec![
804 send_irc_message_action(),
805 send_irc_welcome_action(),
806 send_irc_pong_action(),
807 send_irc_join_action(),
808 send_irc_part_action(),
809 send_irc_privmsg_action(),
810 send_irc_notice_action(),
811 send_irc_numeric_action(),
812 wait_for_more_action(),
813 close_connection_action(),
814 ])
815 .with_log_template(
816 LogTemplate::new()
817 .with_info("IRC {client_ip}: {preview(message,80)}")
818 .with_debug("IRC message from {client_ip}:{client_port}")
819 .with_trace("IRC: {json_pretty(.)}"),
820 )
821});
822
823pub fn get_irc_event_types() -> Vec<EventType> {
824 vec![IRC_MESSAGE_RECEIVED_EVENT.clone()]
825}