Skip to main content

chat_system/
lib.rs

1//! # chat-system
2//!
3//! A multi-protocol async chat crate for Rust.  Provides a **single unified
4//! [`Messenger`] trait** for IRC, Matrix, Discord, Telegram, Slack, Signal,
5//! WhatsApp, Microsoft Teams, Google Chat, iMessage, Webhook, and Console —
6//! with full rich-text support for every platform's native format.
7//!
8//! The primary way to use this crate is through the **generic interface**:
9//! [`MessengerConfig`] is a serde-tagged enum that selects the backend at
10//! runtime, so the protocol is just a field in your config file rather than a
11//! compile-time choice.
12//!
13//! ---
14//!
15//! ## Quick start — generic interface
16//!
17//! ```rust,no_run
18//! use chat_system::{GenericMessenger, Messenger, MessengerConfig, PresenceStatus};
19//! use chat_system::config::IrcConfig;
20//!
21//! #[tokio::main]
22//! async fn main() -> anyhow::Result<()> {
23//!     // Build (or deserialize) a config — the protocol is just a field.
24//!     let config = MessengerConfig::Irc(IrcConfig {
25//!         name: "my-bot".into(),
26//!         server: "irc.libera.chat".into(),
27//!         port: 6697,
28//!         nick: "my-bot".into(),
29//!         channels: vec!["#rust".into()],
30//!         tls: true,
31//!     });
32//!
33//!     // GenericMessenger implements Messenger — swap the config to change protocol.
34//!     let mut client = GenericMessenger::new(config);
35//!     client.initialize().await?;
36//!
37//!     // Presence status (no-op on platforms that don't support it)
38//!     client.set_status(PresenceStatus::Online).await?;
39//!
40//!     // Text status / custom status message
41//!     client.set_text_status("Building something with Rust 🦀").await?;
42//!
43//!     client.send_message("#rust", "Hello from chat-system!").await?;
44//!
45//!     // Receive messages
46//!     for msg in client.receive_messages().await? {
47//!         println!("[{}] {}: {}", msg.channel.as_deref().unwrap_or("?"), msg.sender, msg.content);
48//!         // Each message may carry reactions (populated on platforms that support them)
49//!         if let Some(reactions) = &msg.reactions {
50//!             for r in reactions { println!("  {} × {}", r.emoji, r.count); }
51//!         }
52//!     }
53//!
54//!     client.disconnect().await?;
55//!     Ok(())
56//! }
57//! ```
58//!
59//! ### Loading the config from a file
60//!
61//! Because [`MessengerConfig`] derives `serde::Deserialize`, any
62//! serde-compatible source works.
63//!
64//! **TOML** (`config.toml`):
65//!
66//! ```toml
67//! protocol = "discord"
68//! name     = "my-bot"
69//! token    = "Bot TOKEN_HERE"
70//! ```
71//!
72//! ```rust,ignore
73//! # use chat_system::{GenericMessenger, Messenger, MessengerConfig};
74//! # #[tokio::main] async fn main() -> anyhow::Result<()> {
75//! let toml_str = std::fs::read_to_string("config.toml")?;
76//! let config: MessengerConfig = toml::from_str(&toml_str)?;
77//! let mut client = GenericMessenger::new(config);
78//! client.initialize().await?;
79//! # Ok(()) }
80//! ```
81//!
82//! **JSON** (`config.json`):
83//!
84//! ```json
85//! {"protocol":"telegram","name":"my-bot","token":"BOT_TOKEN"}
86//! ```
87//!
88//! ```rust,no_run
89//! # use chat_system::{GenericMessenger, Messenger, MessengerConfig};
90//! # #[tokio::main] async fn main() -> anyhow::Result<()> {
91//! let json_str = std::fs::read_to_string("config.json")?;
92//! let config: MessengerConfig = serde_json::from_str(&json_str)?;
93//! let mut client = GenericMessenger::new(config);
94//! client.initialize().await?;
95//! # Ok(()) }
96//! ```
97//!
98//! ---
99//!
100//! ## Multi-platform with `MessengerManager`
101//!
102//! [`MessengerManager`] holds a collection of [`Messenger`] instances and
103//! dispatches to all of them at once.
104//!
105//! ```rust,no_run
106//! use chat_system::{GenericMessenger, Messenger, MessengerConfig, MessengerManager};
107//! use chat_system::config::{DiscordConfig, TelegramConfig};
108//!
109//! #[tokio::main]
110//! async fn main() -> anyhow::Result<()> {
111//!     let mut mgr = MessengerManager::new()
112//!         .add(GenericMessenger::new(MessengerConfig::Discord(DiscordConfig {
113//!             name: "discord".into(),
114//!             token: std::env::var("DISCORD_TOKEN")?,
115//!         })))
116//!         .add(GenericMessenger::new(MessengerConfig::Telegram(TelegramConfig {
117//!             name: "telegram".into(),
118//!             token: std::env::var("TELEGRAM_TOKEN")?,
119//!         })));
120//!     mgr.initialize_all().await?;
121//!
122//!     // Broadcast to every connected platform
123//!     mgr.broadcast("#general", "Hello from all platforms!").await;
124//!
125//!     // Receive from every platform in one call
126//!     for msg in mgr.receive_all().await? {
127//!         println!("[{}] {}: {}", msg.channel.as_deref().unwrap_or("?"), msg.sender, msg.content);
128//!     }
129//!
130//!     mgr.disconnect_all().await?;
131//!     Ok(())
132//! }
133//! ```
134//!
135//! ---
136//!
137//! ## Reactions
138//!
139//! ```rust,no_run
140//! # use chat_system::{GenericMessenger, Messenger, MessengerConfig};
141//! # use chat_system::config::SlackConfig;
142//! # #[tokio::main] async fn main() -> anyhow::Result<()> {
143//! # let mut client = GenericMessenger::new(MessengerConfig::Slack(SlackConfig { name: "s".into(), token: "t".into() }));
144//! # client.initialize().await?;
145//! // Add a reaction (no-op on platforms that don't support it)
146//! client.add_reaction("msg-id-123", "#general", "👍").await?;
147//! client.remove_reaction("msg-id-123", "#general", "👍").await?;
148//! # Ok(()) }
149//! ```
150//!
151//! Incoming messages expose reactions via [`Message::reactions`]:
152//!
153//! ```rust,no_run
154//! # use chat_system::{GenericMessenger, Messenger, MessengerConfig};
155//! # use chat_system::config::SlackConfig;
156//! # #[tokio::main] async fn main() -> anyhow::Result<()> {
157//! # let client = GenericMessenger::new(MessengerConfig::Slack(SlackConfig { name: "s".into(), token: "t".into() }));
158//! for msg in client.receive_messages().await? {
159//!     if let Some(reactions) = &msg.reactions {
160//!         for r in reactions {
161//!             println!("{}: {} ({})", msg.id, r.emoji, r.count);
162//!         }
163//!     }
164//! }
165//! # Ok(()) }
166//! ```
167//!
168//! ---
169//!
170//! ## Profile pictures
171//!
172//! ```rust,no_run
173//! # use chat_system::{GenericMessenger, Messenger, MessengerConfig};
174//! # use chat_system::config::DiscordConfig;
175//! # #[tokio::main] async fn main() -> anyhow::Result<()> {
176//! # let client = GenericMessenger::new(MessengerConfig::Discord(DiscordConfig { name: "d".into(), token: "t".into() }));
177//! // Retrieve a user's profile picture URL (returns None if not supported)
178//! if let Some(url) = client.get_profile_picture("user-id-123").await? {
179//!     println!("Avatar: {url}");
180//! }
181//!
182//! // Update the bot's own profile picture
183//! client.set_profile_picture("https://example.com/avatar.png").await?;
184//! # Ok(()) }
185//! ```
186//!
187//! ---
188//!
189//! ## Text status / custom status message
190//!
191//! ```rust,no_run
192//! # use chat_system::{GenericMessenger, Messenger, MessengerConfig, PresenceStatus};
193//! # use chat_system::config::SlackConfig;
194//! # #[tokio::main] async fn main() -> anyhow::Result<()> {
195//! # let client = GenericMessenger::new(MessengerConfig::Slack(SlackConfig { name: "s".into(), token: "t".into() }));
196//! // Presence indicator (Online / Away / Busy / Invisible / Offline)
197//! client.set_status(PresenceStatus::Busy).await?;
198//!
199//! // Text status shown next to the username (Slack, Discord, …)
200//! client.set_text_status("In a meeting 📅").await?;
201//! # Ok(()) }
202//! ```
203//!
204//! ---
205//!
206//! ## Replies
207//!
208//! Use [`SendOptions`] to reply to a specific message:
209//!
210//! ```rust,no_run
211//! # use chat_system::{GenericMessenger, Messenger, MessengerConfig, SendOptions};
212//! # use chat_system::config::DiscordConfig;
213//! # #[tokio::main] async fn main() -> anyhow::Result<()> {
214//! # let client = GenericMessenger::new(MessengerConfig::Discord(DiscordConfig { name: "d".into(), token: "t".into() }));
215//! client.send_message_with_options(SendOptions {
216//!     recipient: "#general",
217//!     content: "Thanks for the message!",
218//!     reply_to: Some("original-message-id"),
219//!     ..Default::default()
220//! }).await?;
221//! # Ok(()) }
222//! ```
223//!
224//! Incoming reply messages expose the parent via [`Message::reply_to`].
225//!
226//! ---
227//!
228//! ## Search
229//!
230//! ```rust,no_run
231//! # use chat_system::{GenericMessenger, Messenger, MessengerConfig, SearchQuery};
232//! # use chat_system::config::SlackConfig;
233//! # #[tokio::main] async fn main() -> anyhow::Result<()> {
234//! # let client = GenericMessenger::new(MessengerConfig::Slack(SlackConfig { name: "s".into(), token: "t".into() }));
235//! let results = client.search_messages(SearchQuery {
236//!     text: "deploy".into(),
237//!     channel: Some("#ops".into()),
238//!     limit: Some(20),
239//!     ..Default::default()
240//! }).await?;
241//! for msg in results {
242//!     println!("{}: {}", msg.sender, msg.content);
243//! }
244//! # Ok(()) }
245//! ```
246//!
247//! ---
248//!
249//! ## Rich text
250//!
251//! ```rust,no_run
252//! use chat_system::{RichText, RichTextNode};
253//!
254//! let msg = RichText(vec![
255//!     RichTextNode::Bold(vec![RichTextNode::Plain("Hello".into())]),
256//!     RichTextNode::Plain(", world! ".into()),
257//!     RichTextNode::Link {
258//!         url: "https://example.com".into(),
259//!         text: vec![RichTextNode::Plain("click".into())],
260//!     },
261//! ]);
262//!
263//! println!("{}", msg.to_discord_markdown());
264//! println!("{}", msg.to_telegram_html());
265//! println!("{}", msg.to_slack_mrkdwn());
266//! println!("{}", msg.to_irc_formatted());
267//! ```
268//!
269//! ---
270//!
271//! ## Channel capabilities
272//!
273//! Every [`ChannelType`] exposes its feature set via [`ChannelType::descriptor`]:
274//!
275//! ```rust
276//! use chat_system::ChannelType;
277//!
278//! let caps = ChannelType::Slack.descriptor().capabilities;
279//! assert!(caps.supports_reactions);
280//! assert!(caps.supports_threads);
281//!
282//! for ct in ChannelType::ALL {
283//!     println!("{:14} reactions={} threads={}", ct.display_name(),
284//!         ct.descriptor().capabilities.supports_reactions,
285//!         ct.descriptor().capabilities.supports_threads);
286//! }
287//! ```
288//!
289//! ---
290//!
291//! ## Protocol-specific clients
292//!
293//! When you need access to protocol-specific features not covered by the generic
294//! interface, you can construct the concrete messenger type directly:
295//!
296//! ```rust,no_run
297//! use chat_system::messengers::IrcMessenger;
298//! use chat_system::Messenger;
299//!
300//! #[tokio::main]
301//! async fn main() -> anyhow::Result<()> {
302//!     let mut client = IrcMessenger::new("my-bot", "irc.libera.chat", 6697, "my-bot")
303//!         .with_tls(true)
304//!         .with_channels(vec!["#rust"]);
305//!     client.initialize().await?;
306//!     client.send_message("#rust", "Hello, IRC!").await?;
307//!     client.disconnect().await?;
308//!     Ok(())
309//! }
310//! ```
311
312pub mod channel_type;
313pub mod config;
314pub mod markdown;
315pub mod message;
316pub mod messenger;
317pub mod messengers;
318pub mod rich_text;
319pub mod server;
320pub mod servers;
321
322pub use channel_type::{ChannelCapabilities, ChannelDescriptor, ChannelType, InboundMode};
323pub use config::{GenericMessenger, GenericServer, MessengerConfig, ServerConfig};
324pub use markdown::{chunk_markdown_html, markdown_to_slack, markdown_to_telegram_html};
325pub use message::{MediaAttachment, Message, Reaction, SendOptions};
326pub use messenger::{Messenger, MessengerManager, PresenceStatus, SearchQuery};
327pub use rich_text::{RichText, RichTextNode};
328pub use server::{ChatListener, ChatServer, MessageHandler, Server};
329pub use servers::IrcListener;