mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(api): distinguish a deleted reply target from no reply (#1884)
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user