1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
4use conversation_api::ConversationSurface;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use std::{fmt, str::FromStr};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
9pub struct RecipientId(String);
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct RecipientIdError;
13
14impl fmt::Display for RecipientIdError {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 f.write_str("invalid canonical recipient identity")
17 }
18}
19impl std::error::Error for RecipientIdError {}
20
21impl RecipientId {
22 pub fn messaging(surface: &ConversationSurface) -> Result<Self, RecipientIdError> {
24 let surface_id = surface.canonical_id();
25 let id = if let Some(address) = surface_id.strip_prefix("cs1:mp:") {
26 format!("m:p:{address}")
27 } else if let Some(address) = surface_id.strip_prefix("cs1:mg:") {
28 format!("m:g:{address}")
29 } else {
30 return Err(RecipientIdError);
31 };
32 id.parse()
33 }
34
35 pub fn node(node_type: &str, node_id: &str) -> Result<Self, RecipientIdError> {
36 if !valid_kind(node_type) || !valid_segment(node_id) {
37 return Err(RecipientIdError);
38 }
39 Ok(Self(format!(
40 "n:{node_type}:{}",
41 URL_SAFE_NO_PAD.encode(node_id)
42 )))
43 }
44
45 pub fn as_str(&self) -> &str {
46 &self.0
47 }
48
49 pub fn messaging_surface(&self) -> Option<ConversationSurface> {
50 surface_id(&self.0)?.parse().ok()
51 }
52
53 pub fn messaging_base(&self) -> Option<Self> {
56 let surface = match self.messaging_surface()? {
57 ConversationSurface::MessagingPersonal {
58 provider,
59 account_id,
60 conversation_id,
61 ..
62 } => {
63 ConversationSurface::messaging_personal(provider, account_id, conversation_id, None)
64 .ok()?
65 }
66 ConversationSurface::MessagingGroup {
67 provider,
68 account_id,
69 conversation_id,
70 ..
71 } => ConversationSurface::messaging_group(provider, account_id, conversation_id, None)
72 .ok()?,
73 _ => return None,
74 };
75 Self::messaging(&surface).ok()
76 }
77
78 pub fn node_address(&self) -> Option<(&str, String)> {
79 let tail = self.0.strip_prefix("n:")?;
80 let (kind, encoded) = tail.split_once(':')?;
81 Some((kind, decode(encoded).ok()?))
82 }
83}
84
85fn surface_id(id: &str) -> Option<String> {
86 if let Some(address) = id.strip_prefix("m:p:") {
87 Some(format!("cs1:mp:{address}"))
88 } else {
89 id.strip_prefix("m:g:")
90 .map(|address| format!("cs1:mg:{address}"))
91 }
92}
93
94fn valid_kind(value: &str) -> bool {
95 let mut bytes = value.bytes();
96 bytes.next().is_some_and(|first| first.is_ascii_lowercase())
97 && bytes.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
98}
99fn valid_segment(value: &str) -> bool {
100 !value.is_empty() && value.trim() == value
101}
102fn decode(encoded: &str) -> Result<String, RecipientIdError> {
103 let value = String::from_utf8(
104 URL_SAFE_NO_PAD
105 .decode(encoded)
106 .map_err(|_| RecipientIdError)?,
107 )
108 .map_err(|_| RecipientIdError)?;
109 if !valid_segment(&value) || URL_SAFE_NO_PAD.encode(&value) != encoded {
110 return Err(RecipientIdError);
111 }
112 Ok(value)
113}
114
115impl FromStr for RecipientId {
116 type Err = RecipientIdError;
117 fn from_str(id: &str) -> Result<Self, Self::Err> {
118 if let Some(surface_id) = surface_id(id) {
119 let surface: ConversationSurface = surface_id.parse().map_err(|_| RecipientIdError)?;
120 if surface.canonical_id() != surface_id {
121 return Err(RecipientIdError);
122 }
123 } else if let Some(tail) = id.strip_prefix("n:") {
124 let (kind, encoded) = tail.split_once(':').ok_or(RecipientIdError)?;
125 if !valid_kind(kind) {
126 return Err(RecipientIdError);
127 }
128 decode(encoded)?;
129 } else {
130 return Err(RecipientIdError);
131 }
132 Ok(Self(id.to_owned()))
133 }
134}
135impl fmt::Display for RecipientId {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 f.write_str(&self.0)
138 }
139}
140impl Serialize for RecipientId {
141 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
142 serializer.serialize_str(&self.0)
143 }
144}
145impl<'de> Deserialize<'de> for RecipientId {
146 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
147 String::deserialize(deserializer)?
148 .parse()
149 .map_err(serde::de::Error::custom)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 #[test]
157 fn shared_conformance_vectors() {
158 let fixtures: serde_json::Value =
159 serde_json::from_str(include_str!("../fixtures/recipient-id.conformance.json"))
160 .unwrap();
161 for case in fixtures["valid"].as_array().unwrap() {
162 let id: RecipientId = case["id"].as_str().unwrap().parse().unwrap();
163 assert_eq!(serde_json::to_value(&id).unwrap(), case["id"]);
164 if let Some(surface) = case.get("surface") {
165 let parsed: ConversationSurface = surface.as_str().unwrap().parse().unwrap();
166 assert_eq!(RecipientId::messaging(&parsed).unwrap(), id);
167 assert_eq!(id.messaging_surface().unwrap(), parsed);
168 } else {
169 let (kind, node) = id.node_address().unwrap();
170 assert_eq!(kind, case["nodeType"].as_str().unwrap());
171 assert_eq!(node, case["nodeId"].as_str().unwrap());
172 assert_eq!(RecipientId::node(kind, &node).unwrap(), id);
173 }
174 }
175 for case in fixtures["invalid"].as_array().unwrap() {
176 assert!(
177 case.as_str().unwrap().parse::<RecipientId>().is_err(),
178 "{case}"
179 );
180 }
181 }
182 #[test]
183 fn lane_delivery_and_base_authorization_are_distinct() {
184 let id: RecipientId = "m:g:telegram:Ym90:Y2hhdA:dG9waWM".parse().unwrap();
185 assert_eq!(
186 id.messaging_base().unwrap().as_str(),
187 "m:g:telegram:Ym90:Y2hhdA"
188 );
189 assert_eq!(id.as_str(), "m:g:telegram:Ym90:Y2hhdA:dG9waWM");
190 }
191}