fix(api): distinguish a deleted reply target from no reply (#1884)

This commit is contained in:
Hampus
2026-08-24 12:39:11 +02:00
committed by GitHub
parent 7b646f891e
commit 735ecc1cca
8 changed files with 250 additions and 181 deletions
+58 -16
View File
@@ -39,6 +39,12 @@ const BUCKET_DURATION_MS: i64 = 864_000_000;
const FLUXER_EPOCH_MS: i64 = 1_420_070_400_000;
const SERVICE_REQUEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
const MESSAGE_REFERENCE_TYPE_DEFAULT: i32 = 0;
fn effective_reference_type(reference: &MessageReference) -> i32 {
reference
.reference_type
.unwrap_or(MESSAGE_REFERENCE_TYPE_DEFAULT)
}
const MESSAGE_FLAG_SUPPRESS_EMBEDS: i64 = 1 << 2;
const PUBLIC_USER_FLAGS: i64 =
(1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6);
@@ -787,7 +793,7 @@ impl MessagesShard {
let Some(reference) = &message.message_reference else {
continue;
};
if reference.reference_type != Some(MESSAGE_REFERENCE_TYPE_DEFAULT) {
if effective_reference_type(reference) != MESSAGE_REFERENCE_TYPE_DEFAULT {
continue;
}
let (Some(channel_id), Some(message_id)) = (reference.channel_id, reference.message_id)
@@ -1099,23 +1105,25 @@ impl MessagesShard {
Some((
reference.channel_id?,
reference.message_id?,
reference.reference_type?,
effective_reference_type(reference),
))
})
.filter(|(_, _, reference_type)| *reference_type == MESSAGE_REFERENCE_TYPE_DEFAULT)
.and_then(|(channel_id, message_id, _)| {
context.referenced_messages.get(&(channel_id, message_id))
})
.map(|referenced| {
let mut referenced_options = options.clone();
referenced_options.nonce = None;
referenced_options.tts = false;
Box::new(self.map_message_response(
referenced,
&referenced_options,
context,
false,
))
.map(|(channel_id, message_id, _)| {
context
.referenced_messages
.get(&(channel_id, message_id))
.map(|referenced| {
let mut referenced_options = options.clone();
referenced_options.nonce = None;
referenced_options.tts = false;
Box::new(self.map_message_response(
referenced,
&referenced_options,
context,
false,
))
})
})
} else {
None
@@ -2743,7 +2751,7 @@ fn map_message_reference(reference: &MessageReference) -> Option<ApiMessageRefer
channel_id: reference.channel_id?.to_string(),
message_id: reference.message_id?.to_string(),
guild_id: reference.guild_id.map(|id| id.to_string()),
reference_type: reference.reference_type.unwrap_or_default(),
reference_type: effective_reference_type(reference),
})
}
@@ -3211,6 +3219,40 @@ mod tests {
assert_eq!(bucket_page_limit(500), BUCKET_INDEX_PAGE_SIZE);
}
#[test]
fn reference_with_no_stored_type_is_treated_as_a_reply_everywhere() {
let legacy = MessageReference {
channel_id: Some(1),
message_id: Some(2),
guild_id: None,
reference_type: None,
};
let explicit = MessageReference {
reference_type: Some(MESSAGE_REFERENCE_TYPE_DEFAULT),
..legacy.clone()
};
let forward = MessageReference {
reference_type: Some(MESSAGE_REFERENCE_TYPE_DEFAULT + 1),
..legacy.clone()
};
assert_eq!(
effective_reference_type(&legacy),
MESSAGE_REFERENCE_TYPE_DEFAULT
);
assert_eq!(
effective_reference_type(&legacy),
effective_reference_type(&explicit)
);
assert_ne!(
effective_reference_type(&forward),
MESSAGE_REFERENCE_TYPE_DEFAULT
);
let mapped = map_message_reference(&legacy).expect("legacy reference maps");
assert_eq!(mapped.reference_type, MESSAGE_REFERENCE_TYPE_DEFAULT);
}
#[test]
fn around_window_limits_match_reference_api() {
assert_eq!(around_window_limits(0), (0, 0));
+91 -2
View File
@@ -2,6 +2,14 @@
use serde::{Deserialize, Serialize};
fn deserialize_double_option<'de, D, T>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
Option::<T>::deserialize(deserializer).map(Some)
}
mod serde_id {
use serde::Deserialize;
use serde::de::{self, Deserializer};
@@ -461,8 +469,12 @@ pub struct ApiMessageResponse {
pub nonce: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub call: Option<ApiMessageCallResponse>,
#[serde(skip_serializing_if = "Option::is_none")]
pub referenced_message: Option<Box<ApiMessageResponse>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_double_option"
)]
pub referenced_message: Option<Option<Box<ApiMessageResponse>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -740,4 +752,81 @@ mod tests {
vec![1_472_426_752_046_002_208]
);
}
fn minimal_api_message(
referenced: Option<Option<Box<ApiMessageResponse>>>,
) -> ApiMessageResponse {
let author = ApiUserPartialResponse {
id: "1".to_string(),
username: "user".to_string(),
discriminator: "0001".to_string(),
global_name: None,
avatar: None,
avatar_color: None,
bot: None,
system: None,
flags: 0,
mention_flags: None,
};
ApiMessageResponse {
id: "2".to_string(),
channel_id: "3".to_string(),
author,
webhook_id: None,
message_type: 0,
flags: 0,
content: String::new(),
timestamp: "2026-01-01T00:00:00Z".to_string(),
edited_timestamp: None,
pinned: false,
mention_everyone: false,
tts: false,
mentions: Vec::new(),
mention_roles: Vec::new(),
mention_channels: None,
users: None,
embeds: Vec::new(),
attachments: Vec::new(),
stickers: Vec::new(),
nsfw_emojis: None,
reactions: None,
message_reference: None,
message_snapshots: None,
nonce: None,
call: None,
referenced_message: referenced,
}
}
fn msgpack_round_trip(
referenced: Option<Option<Box<ApiMessageResponse>>>,
) -> Option<Option<Box<ApiMessageResponse>>> {
let encoded =
rmp_serde::to_vec_named(&minimal_api_message(referenced)).expect("encodes to msgpack");
rmp_serde::from_slice::<ApiMessageResponse>(&encoded)
.expect("decodes from msgpack")
.referenced_message
}
#[test]
fn referenced_message_null_survives_the_msgpack_round_trip() {
assert!(msgpack_round_trip(None).is_none());
assert!(matches!(msgpack_round_trip(Some(None)), Some(None)));
assert!(matches!(
msgpack_round_trip(Some(Some(Box::new(minimal_api_message(None))))),
Some(Some(_))
));
}
#[test]
fn referenced_message_null_serialises_as_json_null_not_an_absent_key() {
let deleted = serde_json::to_value(minimal_api_message(Some(None))).expect("serialises");
assert_eq!(
deleted.get("referenced_message"),
Some(&serde_json::Value::Null)
);
let absent = serde_json::to_value(minimal_api_message(None)).expect("serialises");
assert!(absent.get("referenced_message").is_none());
}
}