azalea_client/plugins/chat/
handler.rs

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