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