fix(channel/signal): route UUID senders as direct recipients

Privacy-enabled Signal users have no sourceNumber, so sender()
falls back to their UUID from the source field.  Previously
parse_recipient_target() treated non-E.164 strings without the
group: prefix as group IDs, causing signal-cli to reject the
UUID as an invalid base64 group ID.

Add is_uuid() helper using the already-imported uuid crate and
recognise valid UUIDs as Direct targets alongside E.164 numbers.
This commit is contained in:
bhagwan 2026-02-18 16:07:10 -05:00 committed by Chummy
parent 8b4607a1ef
commit c405cdf19a

View file

@ -119,12 +119,18 @@ impl SignalChannel {
(2..=15).contains(&number.len()) && number.chars().all(|c| c.is_ascii_digit())
}
/// Check whether a string is a valid UUID (signal-cli uses these for
/// privacy-enabled users who have opted out of sharing their phone number).
fn is_uuid(s: &str) -> bool {
Uuid::parse_str(s).is_ok()
}
fn parse_recipient_target(recipient: &str) -> RecipientTarget {
if let Some(group_id) = recipient.strip_prefix(GROUP_TARGET_PREFIX) {
return RecipientTarget::Group(group_id.to_string());
}
if Self::is_e164(recipient) {
if Self::is_e164(recipient) || Self::is_uuid(recipient) {
RecipientTarget::Direct(recipient.to_string())
} else {
RecipientTarget::Group(recipient.to_string())
@ -653,6 +659,15 @@ mod tests {
);
}
#[test]
fn parse_recipient_target_uuid_is_direct() {
let uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
assert_eq!(
SignalChannel::parse_recipient_target(uuid),
RecipientTarget::Direct(uuid.to_string())
);
}
#[test]
fn parse_recipient_target_non_e164_plus_is_group() {
assert_eq!(
@ -661,6 +676,24 @@ mod tests {
);
}
#[test]
fn is_uuid_valid() {
assert!(SignalChannel::is_uuid(
"a1b2c3d4-e5f6-7890-abcd-ef1234567890"
));
assert!(SignalChannel::is_uuid(
"00000000-0000-0000-0000-000000000000"
));
}
#[test]
fn is_uuid_invalid() {
assert!(!SignalChannel::is_uuid("+1234567890"));
assert!(!SignalChannel::is_uuid("not-a-uuid"));
assert!(!SignalChannel::is_uuid("group:abc123"));
assert!(!SignalChannel::is_uuid(""));
}
#[test]
fn sender_prefers_source_number() {
let env = Envelope {
@ -685,6 +718,73 @@ mod tests {
assert_eq!(SignalChannel::sender(&env), Some("uuid-123".to_string()));
}
#[test]
fn process_envelope_uuid_sender_dm() {
let uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
let ch = SignalChannel::new(
"http://127.0.0.1:8686".to_string(),
"+1234567890".to_string(),
None,
vec!["*".to_string()],
false,
false,
);
let env = Envelope {
source: Some(uuid.to_string()),
source_number: None,
data_message: Some(DataMessage {
message: Some("Hello from privacy user".to_string()),
timestamp: Some(1_700_000_000_000),
group_info: None,
attachments: None,
}),
story_message: None,
timestamp: Some(1_700_000_000_000),
};
let msg = ch.process_envelope(&env).unwrap();
assert_eq!(msg.sender, uuid);
assert_eq!(msg.reply_target, uuid);
assert_eq!(msg.content, "Hello from privacy user");
// Verify reply routing: UUID sender in DM should route as Direct
let target = SignalChannel::parse_recipient_target(&msg.reply_target);
assert_eq!(target, RecipientTarget::Direct(uuid.to_string()));
}
#[test]
fn process_envelope_uuid_sender_in_group() {
let uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
let ch = SignalChannel::new(
"http://127.0.0.1:8686".to_string(),
"+1234567890".to_string(),
Some("testgroup".to_string()),
vec!["*".to_string()],
false,
false,
);
let env = Envelope {
source: Some(uuid.to_string()),
source_number: None,
data_message: Some(DataMessage {
message: Some("Group msg from privacy user".to_string()),
timestamp: Some(1_700_000_000_000),
group_info: Some(GroupInfo {
group_id: Some("testgroup".to_string()),
}),
attachments: None,
}),
story_message: None,
timestamp: Some(1_700_000_000_000),
};
let msg = ch.process_envelope(&env).unwrap();
assert_eq!(msg.sender, uuid);
assert_eq!(msg.reply_target, "group:testgroup");
// Verify reply routing: group message should still route as Group
let target = SignalChannel::parse_recipient_target(&msg.reply_target);
assert_eq!(target, RecipientTarget::Group("testgroup".to_string()));
}
#[test]
fn sender_none_when_both_missing() {
let env = Envelope {