Compare commits

...
Sign in to create a new pull request.

6 commits

Author SHA1 Message Date
7a292c429a Bump version to 0.6.1 2025-02-23 23:34:12 +01:00
095d2cea86 Fix nick hue hashing algorithm in some edge cases
When the nick consisted entirely of non-alphanumeric characters and
included at least one colon-delimited emoji, the hue hashing
reimplementation would produce an incorrect result because
colon-delimited emoji were removed at the wrong point in the hashing
process.
2025-02-23 22:34:23 +01:00
6eea194d52 Update emoji 2025-02-23 21:40:39 +01:00
4f7cc49b63 Bump version to 0.6.0 2025-02-21 00:40:15 +01:00
1d444684f7 Update dependencies 2025-02-20 20:16:50 +01:00
bc3e3b1e13 Update tokio-tungstenite 2025-02-20 20:16:11 +01:00
5 changed files with 53 additions and 26 deletions

View file

@ -14,6 +14,18 @@ Procedure when bumping the version number:
## Unreleased ## Unreleased
## v0.6.1 - 2025-02-23
### Changed
- Updated set of emoji names
### Fixed
- Nick hue hashing algorithm in some edge cases
## v0.6.0 - 2025-02-21
### Added ### Added
- `api::Time::from_timestamp` - `api::Time::from_timestamp`

View file

@ -1,34 +1,34 @@
[package] [package]
name = "euphoxide" name = "euphoxide"
version = "0.5.1" version = "0.6.1"
edition = "2021" edition = "2021"
[features] [features]
bot = ["dep:async-trait", "dep:clap", "dep:cookie"] bot = ["dep:async-trait", "dep:clap", "dep:cookie"]
[dependencies] [dependencies]
async-trait = { version = "0.1.83", optional = true } async-trait = { version = "0.1.86", optional = true }
caseless = "0.2.1" caseless = "0.2.2"
cookie = { version = "0.18.1", optional = true } cookie = { version = "0.18.1", optional = true }
futures-util = { version = "0.3.31", default-features = false, features = ["sink"] } futures-util = { version = "0.3.31", default-features = false, features = ["sink"] }
jiff = { version = "0.1.15", features = ["serde"] } jiff = { version = "0.2.1", features = ["serde"] }
log = "0.4.22" log = "0.4.25"
serde = { version = "1.0.215", features = ["derive"] } serde = { version = "1.0.218", features = ["derive"] }
serde_json = "1.0.133" serde_json = "1.0.139"
tokio = { version = "1.42.0", features = ["time", "sync", "macros", "rt"] } tokio = { version = "1.43.0", features = ["time", "sync", "macros", "rt"] }
tokio-stream = "0.1.16" tokio-stream = "0.1.17"
tokio-tungstenite = { version = "0.24.0", features = ["rustls-tls-native-roots"] } tokio-tungstenite = { version = "0.26.2", features = ["rustls-tls-native-roots"] }
unicode-normalization = "0.1.24" unicode-normalization = "0.1.24"
[dependencies.clap] [dependencies.clap]
version = "4.5.22" version = "4.5.30"
optional = true optional = true
default-features = false default-features = false
features = ["std", "derive", "deprecated"] features = ["std", "derive", "deprecated"]
[dev-dependencies] # For example bot [dev-dependencies] # For example bot
rustls = "0.23.19" rustls = "0.23.23"
tokio = { version = "1.42.0", features = ["rt-multi-thread"] } tokio = { version = "1.43.0", features = ["rt-multi-thread"] }
[[example]] [[example]]
name = "testbot_instance" name = "testbot_instance"

View file

@ -427,7 +427,7 @@ impl Conn {
} }
tungstenite::Message::Ping(_) => {} tungstenite::Message::Ping(_) => {}
tungstenite::Message::Pong(payload) => { tungstenite::Message::Pong(payload) => {
if self.last_ws_ping_payload == Some(payload) { if self.last_ws_ping_payload == Some(payload.to_vec()) {
self.last_ws_ping_replied_to = true; self.last_ws_ping_replied_to = true;
} }
} }
@ -528,7 +528,9 @@ impl Conn {
let ws_payload = now.as_millisecond().to_be_bytes().to_vec(); let ws_payload = now.as_millisecond().to_be_bytes().to_vec();
self.last_ws_ping_payload = Some(ws_payload.clone()); self.last_ws_ping_payload = Some(ws_payload.clone());
self.last_ws_ping_replied_to = false; self.last_ws_ping_replied_to = false;
self.ws.send(tungstenite::Message::Ping(ws_payload)).await?; self.ws
.send(tungstenite::Message::Ping(ws_payload.into()))
.await?;
// Send new euph ping // Send new euph ping
let euph_payload = Time::from_timestamp(now); let euph_payload = Time::from_timestamp(now);
@ -561,7 +563,7 @@ impl Conn {
.into_packet()?; .into_packet()?;
debug!(target: "euphoxide::conn::full", "Sending {packet:?}"); debug!(target: "euphoxide::conn::full", "Sending {packet:?}");
let msg = tungstenite::Message::Text(serde_json::to_string(&packet)?); let msg = tungstenite::Message::Text(serde_json::to_string(&packet)?.into());
self.ws.send(msg).await?; self.ws.send(msg).await?;
let _ = reply_tx.send(self.replies.wait_for(id)); let _ = reply_tx.send(self.replies.wait_for(id));
@ -579,7 +581,7 @@ impl Conn {
.into_packet()?; .into_packet()?;
debug!(target: "euphoxide::conn::full", "Sending {packet:?}"); debug!(target: "euphoxide::conn::full", "Sending {packet:?}");
let msg = tungstenite::Message::Text(serde_json::to_string(&packet)?); let msg = tungstenite::Message::Text(serde_json::to_string(&packet)?.into());
self.ws.send(msg).await?; self.ws.send(msg).await?;
Ok(()) Ok(())

View file

@ -878,7 +878,7 @@
"fist_raised": "270a", "fist_raised": "270a",
"fist_right": "1f91c", "fist_right": "1f91c",
"five": "35-fe0f-20e3", "five": "35-fe0f-20e3",
"fjafjkldskf7jkfdj": "1f577", "fjafjkldskf7jkfdj": "1f577-fe0f",
"flags": "1f38f", "flags": "1f38f",
"flamingo": "1f9a9", "flamingo": "1f9a9",
"flashlight": "1f526", "flashlight": "1f526",
@ -958,6 +958,7 @@
"georgia": "1f1ec-1f1ea", "georgia": "1f1ec-1f1ea",
"ghana": "1f1ec-1f1ed", "ghana": "1f1ec-1f1ed",
"ghost": "1f47b", "ghost": "1f47b",
"ghoti": "1f41f",
"gibraltar": "1f1ec-1f1ee", "gibraltar": "1f1ec-1f1ee",
"gift": "1f381", "gift": "1f381",
"gift_heart": "1f49d", "gift_heart": "1f49d",
@ -2985,7 +2986,7 @@
"speaking_head": "1f5e3-fe0f", "speaking_head": "1f5e3-fe0f",
"speech_balloon": "1f4ac", "speech_balloon": "1f4ac",
"speedboat": "1f6a4", "speedboat": "1f6a4",
"spider": "1f577", "spider": "1f577-fe0f",
"spider_web": "1f578-fe0f", "spider_web": "1f578-fe0f",
"spiral_calendar": "1f5d3-fe0f", "spiral_calendar": "1f5d3-fe0f",
"spiral_notepad": "1f5d2-fe0f", "spiral_notepad": "1f5d2-fe0f",

View file

@ -5,9 +5,10 @@ use unicode_normalization::UnicodeNormalization;
use crate::emoji::Emoji; use crate::emoji::Emoji;
/// Does not remove emoji. fn hue_normalize(emoji: &Emoji, text: &str) -> String {
fn hue_normalize(text: &str) -> String { emoji
text.chars() .remove(text)
.chars()
.filter(|&c| c.is_ascii_alphanumeric() || c == '_' || c == '-') .filter(|&c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
.map(|c| c.to_ascii_lowercase()) .map(|c| c.to_ascii_lowercase())
.collect() .collect()
@ -15,7 +16,7 @@ fn hue_normalize(text: &str) -> String {
/// A re-implementation of [euphoria's nick hue hashing algorithm][0]. /// A re-implementation of [euphoria's nick hue hashing algorithm][0].
/// ///
/// [0]: https://github.com/CylonicRaider/heim/blob/master/client/lib/hueHash.js /// [0]: https://github.com/CylonicRaider/heim/blob/097a1fde89ada53de2b70e51e635257f27956e4e/client/lib/heim/hueHash.js
fn hue_hash(text: &str, offset: i64) -> u8 { fn hue_hash(text: &str, offset: i64) -> u8 {
let mut val = 0_i32; let mut val = 0_i32;
for bibyte in text.encode_utf16() { for bibyte in text.encode_utf16() {
@ -35,7 +36,13 @@ const GREENIE_OFFSET: i64 = 148 - 192; // 148 - hue_hash("greenie", 0)
/// This should be slightly faster than [`hue`] but produces incorrect results /// This should be slightly faster than [`hue`] but produces incorrect results
/// if any colon-delimited emoji are present. /// if any colon-delimited emoji are present.
pub fn hue_without_removing_emoji(nick: &str) -> u8 { pub fn hue_without_removing_emoji(nick: &str) -> u8 {
let normalized = hue_normalize(nick); // An emoji-less version of hue_normalize
let normalized = nick
.chars()
.filter(|&c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
.map(|c| c.to_ascii_lowercase())
.collect::<String>();
if normalized.is_empty() { if normalized.is_empty() {
hue_hash(nick, GREENIE_OFFSET) hue_hash(nick, GREENIE_OFFSET)
} else { } else {
@ -48,9 +55,14 @@ pub fn hue_without_removing_emoji(nick: &str) -> u8 {
/// This is a reimplementation of [euphoria's nick hue hashing algorithm][0]. It /// This is a reimplementation of [euphoria's nick hue hashing algorithm][0]. It
/// should always return the same value as the official client's implementation. /// should always return the same value as the official client's implementation.
/// ///
/// [0]: https://github.com/CylonicRaider/heim/blob/978c921063e6b06012fc8d16d9fbf1b3a0be1191/client/lib/hueHash.js /// [0]: https://github.com/CylonicRaider/heim/blob/097a1fde89ada53de2b70e51e635257f27956e4e/client/lib/heim/hueHash.js
pub fn hue(emoji: &Emoji, nick: &str) -> u8 { pub fn hue(emoji: &Emoji, nick: &str) -> u8 {
hue_without_removing_emoji(&emoji.remove(nick)) let normalized = hue_normalize(emoji, nick);
if normalized.is_empty() {
hue_hash(nick, GREENIE_OFFSET)
} else {
hue_hash(&normalized, GREENIE_OFFSET)
}
} }
/// Normalize a nick to a form that can be compared against other nicks. /// Normalize a nick to a form that can be compared against other nicks.