azalea_client/plugins/chat/
mod.rs

1//! Implementations of chat-related features.
2
3pub mod handler;
4
5use std::sync::Arc;
6
7use azalea_chat::FormattedText;
8use azalea_protocol::packets::game::{
9    c_disguised_chat::ClientboundDisguisedChat, c_player_chat::ClientboundPlayerChat,
10    c_system_chat::ClientboundSystemChat,
11};
12use bevy_app::{App, Plugin, Update};
13use bevy_ecs::{
14    entity::Entity,
15    event::{EventReader, EventWriter},
16    prelude::Event,
17    schedule::IntoSystemConfigs,
18};
19use handler::{SendChatKindEvent, handle_send_chat_kind_event};
20use uuid::Uuid;
21
22use crate::client::Client;
23
24pub struct ChatPlugin;
25impl Plugin for ChatPlugin {
26    fn build(&self, app: &mut App) {
27        app.add_event::<SendChatEvent>()
28            .add_event::<SendChatKindEvent>()
29            .add_event::<ChatReceivedEvent>()
30            .add_systems(
31                Update,
32                (handle_send_chat_event, handle_send_chat_kind_event).chain(),
33            );
34    }
35}
36
37/// A chat packet, either a system message or a chat message.
38#[derive(Debug, Clone, PartialEq)]
39pub enum ChatPacket {
40    System(Arc<ClientboundSystemChat>),
41    Player(Arc<ClientboundPlayerChat>),
42    Disguised(Arc<ClientboundDisguisedChat>),
43}
44
45macro_rules! regex {
46    ($re:literal $(,)?) => {{
47        static RE: std::sync::LazyLock<regex::Regex> =
48            std::sync::LazyLock::new(|| regex::Regex::new($re).unwrap());
49        &RE
50    }};
51}
52
53impl ChatPacket {
54    /// Get the message shown in chat for this packet.
55    pub fn message(&self) -> FormattedText {
56        match self {
57            ChatPacket::System(p) => p.content.clone(),
58            ChatPacket::Player(p) => p.message(),
59            ChatPacket::Disguised(p) => p.message(),
60        }
61    }
62
63    /// Determine the username of the sender and content of the message. This
64    /// does not preserve formatting codes. If it's not a player-sent chat
65    /// message or the sender couldn't be determined, the username part will be
66    /// None.
67    pub fn split_sender_and_content(&self) -> (Option<String>, String) {
68        match self {
69            ChatPacket::System(p) => {
70                let message = p.content.to_string();
71                // Overlay messages aren't in chat
72                if p.overlay {
73                    return (None, message);
74                }
75                // It's a system message, so we'll have to match the content
76                // with regex
77                if let Some(m) = regex!("^<([a-zA-Z_0-9]{1,16})> (.+)$").captures(&message) {
78                    return (Some(m[1].to_string()), m[2].to_string());
79                }
80
81                (None, message)
82            }
83            ChatPacket::Player(p) => (
84                // If it's a player chat packet, then the sender and content
85                // are already split for us.
86                Some(p.chat_type.name.to_string()),
87                p.body.content.clone(),
88            ),
89            ChatPacket::Disguised(p) => (
90                // disguised chat packets are basically the same as player chat packets but without
91                // the chat signing things
92                Some(p.chat_type.name.to_string()),
93                p.message.to_string(),
94            ),
95        }
96    }
97
98    /// Get the username of the sender of the message. If it's not a
99    /// player-sent chat message or the sender couldn't be determined, this
100    /// will be None.
101    pub fn sender(&self) -> Option<String> {
102        self.split_sender_and_content().0
103    }
104
105    /// Get the UUID of the sender of the message. If it's not a
106    /// player-sent chat message, this will be None (this is sometimes the case
107    /// when a server uses a plugin to modify chat messages).
108    pub fn sender_uuid(&self) -> Option<Uuid> {
109        match self {
110            ChatPacket::System(_) => None,
111            ChatPacket::Player(m) => Some(m.sender),
112            ChatPacket::Disguised(_) => None,
113        }
114    }
115
116    /// Get the content part of the message as a string. This does not preserve
117    /// formatting codes. If it's not a player-sent chat message or the sender
118    /// couldn't be determined, this will contain the entire message.
119    pub fn content(&self) -> String {
120        self.split_sender_and_content().1
121    }
122
123    /// Create a new Chat from a string. This is meant to be used as a
124    /// convenience function for testing.
125    pub fn new(message: &str) -> Self {
126        ChatPacket::System(Arc::new(ClientboundSystemChat {
127            content: FormattedText::from(message),
128            overlay: false,
129        }))
130    }
131
132    /// Whether this message was sent with /msg (or aliases). It works by
133    /// checking the translation key, so it won't work on servers that use their
134    /// own whisper system.
135    pub fn is_whisper(&self) -> bool {
136        match self.message() {
137            FormattedText::Text(_) => false,
138            FormattedText::Translatable(t) => t.key == "commands.message.display.incoming",
139        }
140    }
141}
142
143impl Client {
144    /// Send a chat message to the server. This only sends the chat packet and
145    /// not the command packet, which means on some servers you can use this to
146    /// send chat messages that start with a `/`. The [`Client::chat`] function
147    /// handles checking whether the message is a command and using the
148    /// proper packet for you, so you should use that instead.
149    pub fn send_chat_packet(&self, message: &str) {
150        self.ecs.lock().send_event(SendChatKindEvent {
151            entity: self.entity,
152            content: message.to_string(),
153            kind: ChatKind::Message,
154        });
155        let _ = self.run_schedule_sender.try_send(());
156    }
157
158    /// Send a command packet to the server. The `command` argument should not
159    /// include the slash at the front.
160    ///
161    /// You can also just use [`Client::chat`] and start your message with a `/`
162    /// to send a command.
163    pub fn send_command_packet(&self, command: &str) {
164        self.ecs.lock().send_event(SendChatKindEvent {
165            entity: self.entity,
166            content: command.to_string(),
167            kind: ChatKind::Command,
168        });
169        let _ = self.run_schedule_sender.try_send(());
170    }
171
172    /// Send a message in chat.
173    ///
174    /// ```rust,no_run
175    /// # use azalea_client::Client;
176    /// # async fn example(bot: Client) -> anyhow::Result<()> {
177    /// bot.chat("Hello, world!");
178    /// # Ok(())
179    /// # }
180    /// ```
181    pub fn chat(&self, content: &str) {
182        self.ecs.lock().send_event(SendChatEvent {
183            entity: self.entity,
184            content: content.to_string(),
185        });
186        let _ = self.run_schedule_sender.try_send(());
187    }
188}
189
190/// A client received a chat message packet.
191#[derive(Event, Debug, Clone)]
192pub struct ChatReceivedEvent {
193    pub entity: Entity,
194    pub packet: ChatPacket,
195}
196
197/// Send a chat message (or command, if it starts with a slash) to the server.
198#[derive(Event)]
199pub struct SendChatEvent {
200    pub entity: Entity,
201    pub content: String,
202}
203
204pub fn handle_send_chat_event(
205    mut events: EventReader<SendChatEvent>,
206    mut send_chat_kind_events: EventWriter<SendChatKindEvent>,
207) {
208    for event in events.read() {
209        if event.content.starts_with('/') {
210            send_chat_kind_events.send(SendChatKindEvent {
211                entity: event.entity,
212                content: event.content[1..].to_string(),
213                kind: ChatKind::Command,
214            });
215        } else {
216            send_chat_kind_events.send(SendChatKindEvent {
217                entity: event.entity,
218                content: event.content.clone(),
219                kind: ChatKind::Message,
220            });
221        }
222    }
223}
224
225/// A kind of chat packet, either a chat message or a command.
226pub enum ChatKind {
227    Message,
228    Command,
229}
230
231// TODO
232// MessageSigner, ChatMessageContent, LastSeenMessages
233// fn sign_message() -> MessageSignature {
234//     MessageSignature::default()
235// }