azalea_client/plugins/chat/
handler.rs

1use std::time::{SystemTime, UNIX_EPOCH};
2
3use azalea_protocol::packets::{
4    Packet,
5    game::{ServerboundChat, ServerboundChatCommand, s_chat::LastSeenMessagesUpdate},
6};
7use bevy_ecs::prelude::*;
8
9use super::ChatKind;
10use crate::packet::game::SendGamePacketEvent;
11#[cfg(feature = "online-mode")]
12use crate::{Account, chat_signing::ChatSigningSession};
13
14/// Send a chat packet to the server of a specific kind (chat message or
15/// command). Usually you just want [`SendChatEvent`] instead.
16///
17/// Usually setting the kind to `Message` will make it send a chat message even
18/// if it starts with a slash, but some server implementations will always do a
19/// command if it starts with a slash.
20///
21/// If you're wondering why this isn't two separate events, it's so ordering is
22/// preserved if multiple chat messages and commands are sent at the same time.
23///
24/// [`SendChatEvent`]: super::SendChatEvent
25#[derive(Message)]
26pub struct SendChatKindEvent {
27    pub entity: Entity,
28    pub content: String,
29    pub kind: ChatKind,
30}
31
32pub fn handle_send_chat_kind_event(
33    mut events: MessageReader<SendChatKindEvent>,
34    mut commands: Commands,
35    #[cfg(feature = "online-mode")] mut query: Query<(&Account, &mut ChatSigningSession)>,
36) {
37    for event in events.read() {
38        let content = event
39            .content
40            .chars()
41            .filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | 'ยง'))
42            .take(256)
43            .collect::<String>();
44
45        let timestamp = SystemTime::now();
46
47        let packet = match event.kind {
48            ChatKind::Message => {
49                let salt = azalea_crypto::signing::make_salt();
50
51                #[cfg(feature = "online-mode")]
52                let signature = if let Ok((account, mut chat_session)) = query.get_mut(event.entity)
53                {
54                    Some(create_signature(
55                        account,
56                        &mut chat_session,
57                        salt,
58                        timestamp,
59                        &content,
60                    ))
61                } else {
62                    None
63                };
64                #[cfg(not(feature = "online-mode"))]
65                let signature = None;
66
67                ServerboundChat {
68                    message: content,
69                    timestamp: timestamp
70                        .duration_since(UNIX_EPOCH)
71                        .expect("Time shouldn't be before epoch")
72                        .as_millis()
73                        .try_into()
74                        .expect("Instant should fit into a u64"),
75                    salt,
76                    signature,
77                    // TODO: implement last_seen_messages
78                    last_seen_messages: LastSeenMessagesUpdate::default(),
79                }
80            }
81            .into_variant(),
82            ChatKind::Command => {
83                // TODO: commands that require chat signing
84                ServerboundChatCommand { command: content }.into_variant()
85            }
86        };
87
88        commands.trigger(SendGamePacketEvent::new(event.entity, packet));
89    }
90}
91
92#[cfg(feature = "online-mode")]
93pub fn create_signature(
94    account: &Account,
95    chat_session: &mut ChatSigningSession,
96    salt: u64,
97    timestamp: SystemTime,
98    message: &str,
99) -> azalea_crypto::signing::MessageSignature {
100    use azalea_crypto::signing::SignChatMessageOptions;
101
102    let certs = account.certs.lock();
103    let certs = certs.as_ref().expect("certs shouldn't be set back to None");
104
105    let signature = azalea_crypto::signing::sign_chat_message(&SignChatMessageOptions {
106        account_uuid: account.uuid.expect("account must have a uuid"),
107        chat_session_uuid: chat_session.session_id,
108        message_index: chat_session.messages_sent,
109        salt,
110        timestamp,
111        message: message.to_owned(),
112        private_key: certs.private_key.clone(),
113    });
114
115    chat_session.messages_sent += 1;
116
117    signature
118}