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