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_event::<SendChatEvent>()
23 .add_event::<SendChatKindEvent>()
24 .add_event::<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. If it's not a
135 /// player-sent chat message, this will be None (this is sometimes the case
136 /// when a server uses a plugin to modify chat messages).
137 pub fn sender_uuid(&self) -> Option<Uuid> {
138 match self {
139 ChatPacket::System(_) => None,
140 ChatPacket::Player(m) => Some(m.sender),
141 ChatPacket::Disguised(_) => None,
142 }
143 }
144
145 /// Get the content part of the message as a string. This does not preserve
146 /// formatting codes. If it's not a player-sent chat message or the sender
147 /// couldn't be determined, this will contain the entire message.
148 pub fn content(&self) -> String {
149 self.split_sender_and_content().1
150 }
151
152 /// Create a new Chat from a string. This is meant to be used as a
153 /// convenience function for testing.
154 pub fn new(message: &str) -> Self {
155 ChatPacket::System(Arc::new(ClientboundSystemChat {
156 content: FormattedText::from(message),
157 overlay: false,
158 }))
159 }
160
161 /// Whether this message is an incoming whisper message (i.e. someone else
162 /// dm'd the bot with /msg). It works by checking the translation key, so it
163 /// won't work on servers that use their own whisper system.
164 pub fn is_whisper(&self) -> bool {
165 match self {
166 ChatPacket::System(p) => {
167 let message = p.content.to_string();
168 if p.overlay {
169 return false;
170 }
171 if regex!("^(-> me|[a-zA-Z_0-9]{1,16} whispers: )").is_match(&message) {
172 return true;
173 }
174
175 false
176 }
177 _ => match self.message() {
178 FormattedText::Text(_) => false,
179 FormattedText::Translatable(t) => t.key == "commands.message.display.incoming",
180 },
181 }
182 }
183}
184
185impl Client {
186 /// Send a chat message to the server. This only sends the chat packet and
187 /// not the command packet, which means on some servers you can use this to
188 /// send chat messages that start with a `/`. The [`Client::chat`] function
189 /// handles checking whether the message is a command and using the
190 /// proper packet for you, so you should use that instead.
191 pub fn write_chat_packet(&self, message: &str) {
192 self.ecs.lock().send_event(SendChatKindEvent {
193 entity: self.entity,
194 content: message.to_string(),
195 kind: ChatKind::Message,
196 });
197 }
198
199 /// Send a command packet to the server. The `command` argument should not
200 /// include the slash at the front.
201 ///
202 /// You can also just use [`Client::chat`] and start your message with a `/`
203 /// to send a command.
204 pub fn write_command_packet(&self, command: &str) {
205 self.ecs.lock().send_event(SendChatKindEvent {
206 entity: self.entity,
207 content: command.to_string(),
208 kind: ChatKind::Command,
209 });
210 }
211
212 /// Send a message in chat.
213 ///
214 /// ```rust,no_run
215 /// # use azalea_client::Client;
216 /// # async fn example(bot: Client) -> anyhow::Result<()> {
217 /// bot.chat("Hello, world!");
218 /// # Ok(())
219 /// # }
220 /// ```
221 pub fn chat(&self, content: impl Into<String>) {
222 self.ecs.lock().send_event(SendChatEvent {
223 entity: self.entity,
224 content: content.into(),
225 });
226 }
227}
228
229/// A client received a chat message packet.
230#[derive(Event, Debug, Clone)]
231pub struct ChatReceivedEvent {
232 pub entity: Entity,
233 pub packet: ChatPacket,
234}
235
236/// Send a chat message (or command, if it starts with a slash) to the server.
237#[derive(Event)]
238pub struct SendChatEvent {
239 pub entity: Entity,
240 pub content: String,
241}
242
243pub fn handle_send_chat_event(
244 mut events: EventReader<SendChatEvent>,
245 mut send_chat_kind_events: EventWriter<SendChatKindEvent>,
246) {
247 for event in events.read() {
248 if event.content.starts_with('/') {
249 send_chat_kind_events.write(SendChatKindEvent {
250 entity: event.entity,
251 content: event.content[1..].to_string(),
252 kind: ChatKind::Command,
253 });
254 } else {
255 send_chat_kind_events.write(SendChatKindEvent {
256 entity: event.entity,
257 content: event.content.clone(),
258 kind: ChatKind::Message,
259 });
260 }
261 }
262}
263
264/// A kind of chat packet, either a chat message or a command.
265pub enum ChatKind {
266 Message,
267 Command,
268}
269
270// TODO
271// MessageSigner, ChatMessageContent, LastSeenMessages
272// fn sign_message() -> MessageSignature {
273// MessageSignature::default()
274// }