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::prelude::*;
14use handler::{SendChatKindEvent, handle_send_chat_kind_event};
15use uuid::Uuid;
16
17use crate::client::Client;
18
19pub struct ChatPlugin;
20impl Plugin for ChatPlugin {
21 fn build(&self, app: &mut App) {
22 app.add_message::<SendChatEvent>()
23 .add_message::<SendChatKindEvent>()
24 .add_message::<ChatReceivedEvent>()
25 .add_systems(
26 Update,
27 (handle_send_chat_event, handle_send_chat_kind_event).chain(),
28 );
29 }
30}
31
32/// A chat packet, either a system message or a chat message.
33#[derive(Debug, Clone, PartialEq)]
34pub enum ChatPacket {
35 System(Arc<ClientboundSystemChat>),
36 Player(Arc<ClientboundPlayerChat>),
37 Disguised(Arc<ClientboundDisguisedChat>),
38}
39
40macro_rules! regex {
41 ($re:literal $(,)?) => {{
42 static RE: std::sync::LazyLock<regex::Regex> =
43 std::sync::LazyLock::new(|| regex::Regex::new($re).unwrap());
44 &RE
45 }};
46}
47
48impl ChatPacket {
49 /// Get the message shown in chat for this packet.
50 ///
51 /// See [`Self::split_sender_and_content`] for more details about how this
52 /// works.
53 pub fn message(&self) -> FormattedText {
54 match self {
55 ChatPacket::System(p) => p.content.clone(),
56 ChatPacket::Player(p) => p.message(),
57 ChatPacket::Disguised(p) => p.message(),
58 }
59 }
60
61 /// A convenience function to determine the username of the sender and
62 /// the content of a chat message.
63 ///
64 /// This does not preserve formatting codes.
65 ///
66 /// This function uses a few checks to attempt to split the chat message,
67 /// and is intended to work on most servers. It won't work for every server
68 /// though, so in certain cases you may have to reimplement this yourself.
69 ///
70 /// If it's not a player-sent chat message or the sender couldn't be
71 /// determined, the username part will be None.
72 ///
73 /// Also see [`Self::sender`] and [`Self::content`] if you only need one of
74 /// the parts.
75 pub fn split_sender_and_content(&self) -> (Option<String>, String) {
76 match self {
77 ChatPacket::System(p) => {
78 let message = p.content.to_string();
79 // overlay messages aren't in chat
80 if p.overlay {
81 return (None, message);
82 }
83
84 // it's a system message, so we'll have to match the content with regex
85
86 // username surrounded by angle brackets (vanilla-like chat), and allow username
87 // prefixes like [Owner]
88 if let Some(m) = regex!(r"^<(?:\[[^\]]+?\] )?(\w{1,16})> (.+)$").captures(&message)
89 {
90 return (Some(m[1].to_string()), m[2].to_string());
91 }
92 // username surrounded by square brackets (essentials whispers, vanilla-like
93 // /say), and allow username prefixes
94 if let Some(m) =
95 regex!(r"^\[(?:\[[^\]]+?\] )?(\w{1,16})(?: -> me)?\] (.+)$").captures(&message)
96 {
97 return (Some(m[1].to_string()), m[2].to_string());
98 }
99 // username without angle brackets (2b2t whispers, vanilla-like whispers)
100 if let Some(m) =
101 regex!(r"^(\w{1,16}) whispers(?: to you)?: (.+)$").captures(&message)
102 {
103 return (Some(m[1].to_string()), m[2].to_string());
104 }
105
106 (None, message)
107 }
108 ChatPacket::Player(p) => (
109 // If it's a player chat packet, then the sender and content
110 // are already split for us.
111 Some(p.chat_type.name.to_string()),
112 p.body.content.clone(),
113 ),
114 ChatPacket::Disguised(p) => (
115 // disguised chat packets are basically the same as player chat packets but without
116 // the chat signing things
117 Some(p.chat_type.name.to_string()),
118 p.message.to_string(),
119 ),
120 }
121 }
122
123 /// Get the username of the sender of the message.
124 ///
125 /// If it's not a player-sent chat message or the sender couldn't be
126 /// determined, this will be None.
127 ///
128 /// See [`Self::split_sender_and_content`] for more details about how this
129 /// works.
130 pub fn sender(&self) -> Option<String> {
131 self.split_sender_and_content().0
132 }
133
134 /// Get the UUID of the sender of the message.
135 ///
136 /// If it's not a player-sent chat message, this will be None (this is
137 /// sometimes the case when a server uses a plugin to modify chat
138 /// messages).
139 pub fn sender_uuid(&self) -> Option<Uuid> {
140 match self {
141 ChatPacket::System(_) => None,
142 ChatPacket::Player(m) => Some(m.sender),
143 ChatPacket::Disguised(_) => None,
144 }
145 }
146
147 /// Get the content part of the message as a string.
148 ///
149 /// This does not preserve formatting codes. If it's not a player-sent chat
150 /// message or the sender couldn't be determined, this will contain the
151 /// entire message.
152 pub fn content(&self) -> String {
153 self.split_sender_and_content().1
154 }
155
156 /// Create a new `ChatPacket` from a string. This is meant to be used as a
157 /// convenience function for testing.
158 pub fn new(message: &str) -> Self {
159 ChatPacket::System(Arc::new(ClientboundSystemChat {
160 content: FormattedText::from(message),
161 overlay: false,
162 }))
163 }
164
165 /// Whether this message is an incoming whisper message (i.e. someone else
166 /// messaged the bot with /msg).
167 ///
168 /// This is not guaranteed to work correctly on custom servers.
169 pub fn is_whisper(&self) -> bool {
170 match self {
171 ChatPacket::System(p) => {
172 let message = p.content.to_string();
173 if p.overlay {
174 return false;
175 }
176 if regex!("^(-> me|[a-zA-Z_0-9]{1,16} whispers: )").is_match(&message) {
177 return true;
178 }
179
180 false
181 }
182 _ => match self.message() {
183 FormattedText::Text(_) => false,
184 FormattedText::Translatable(t) => t.key == "commands.message.display.incoming",
185 },
186 }
187 }
188}
189
190impl Client {
191 /// Send a chat message to the server.
192 ///
193 /// This only sends the chat packet and not the command packet, which means
194 /// on some servers you can use this to send chat messages that start
195 /// with a `/`. The [`Client::chat`] function handles checking whether
196 /// the message is a command and using the proper packet for you, so you
197 /// should use that instead.
198 pub fn write_chat_packet(&self, message: &str) {
199 self.ecs.lock().write_message(SendChatKindEvent {
200 entity: self.entity,
201 content: message.to_string(),
202 kind: ChatKind::Message,
203 });
204 }
205
206 /// Send a command packet to the server. The `command` argument should not
207 /// include the slash at the front.
208 ///
209 /// You can also just use [`Client::chat`] and start your message with a `/`
210 /// to send a command.
211 pub fn write_command_packet(&self, command: &str) {
212 self.ecs.lock().write_message(SendChatKindEvent {
213 entity: self.entity,
214 content: command.to_string(),
215 kind: ChatKind::Command,
216 });
217 }
218
219 /// Send a message in chat.
220 ///
221 /// ```rust,no_run
222 /// # use azalea_client::Client;
223 /// # async fn example(bot: Client) -> anyhow::Result<()> {
224 /// bot.chat("Hello, world!");
225 /// # Ok(())
226 /// # }
227 /// ```
228 pub fn chat(&self, content: impl Into<String>) {
229 self.ecs.lock().write_message(SendChatEvent {
230 entity: self.entity,
231 content: content.into(),
232 });
233 }
234}
235
236/// A client received a chat message packet.
237#[derive(Message, Debug, Clone)]
238pub struct ChatReceivedEvent {
239 pub entity: Entity,
240 pub packet: ChatPacket,
241}
242
243/// Send a chat message (or command, if it starts with a slash) to the server.
244#[derive(Message)]
245pub struct SendChatEvent {
246 pub entity: Entity,
247 pub content: String,
248}
249
250pub fn handle_send_chat_event(
251 mut events: MessageReader<SendChatEvent>,
252 mut send_chat_kind_events: MessageWriter<SendChatKindEvent>,
253) {
254 for event in events.read() {
255 if event.content.starts_with('/') {
256 send_chat_kind_events.write(SendChatKindEvent {
257 entity: event.entity,
258 content: event.content[1..].to_string(),
259 kind: ChatKind::Command,
260 });
261 } else {
262 send_chat_kind_events.write(SendChatKindEvent {
263 entity: event.entity,
264 content: event.content.clone(),
265 kind: ChatKind::Message,
266 });
267 }
268 }
269}
270
271/// A kind of chat packet, either a chat message or a command.
272pub enum ChatKind {
273 Message,
274 Command,
275}
276
277// TODO
278// MessageSigner, ChatMessageContent, LastSeenMessages
279// fn sign_message() -> MessageSignature {
280// MessageSignature::default()
281// }