1#[path = "control.rs"]
2mod control;
3#[path = "targets.rs"]
4mod targets;
5pub use control::*;
6use conversation_api::ConversationSurface;
7use serde::{Deserialize, Serialize};
8use std::fmt;
9pub use targets::*;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ConversationAudience {
16 Personal,
17 Shared,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase", deny_unknown_fields)]
22pub struct MessagingAddress {
23 pub provider: String,
24 pub account_id: String,
25 pub conversation_id: String,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub lane_id: Option<String>,
28 pub audience: ConversationAudience,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct MessagingModelError(&'static str);
33
34impl fmt::Display for MessagingModelError {
35 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36 formatter.write_str(self.0)
37 }
38}
39
40impl std::error::Error for MessagingModelError {}
41
42impl MessagingAddress {
43 pub fn new(
44 provider: impl Into<String>,
45 account_id: impl Into<String>,
46 conversation_id: impl Into<String>,
47 lane_id: Option<String>,
48 audience: ConversationAudience,
49 ) -> Result<Self, MessagingModelError> {
50 Ok(Self {
51 provider: provider_id(provider.into())?,
52 account_id: segment(account_id.into(), "messaging account id is invalid")?,
53 conversation_id: segment(
54 conversation_id.into(),
55 "messaging conversation id is invalid",
56 )?,
57 lane_id: lane_id
58 .map(|value| segment(value, "messaging lane id is invalid"))
59 .transpose()?,
60 audience,
61 })
62 }
63
64 #[must_use]
65 pub fn base_address(&self) -> Self {
66 let mut base = self.clone();
67 base.lane_id = None;
68 base
69 }
70
71 pub fn conversation_surface(&self) -> Result<ConversationSurface, MessagingModelError> {
72 let result = match self.audience {
73 ConversationAudience::Personal => ConversationSurface::messaging_personal(
74 &self.provider,
75 &self.account_id,
76 &self.conversation_id,
77 self.lane_id.clone(),
78 ),
79 ConversationAudience::Shared => ConversationSurface::messaging_group(
80 &self.provider,
81 &self.account_id,
82 &self.conversation_id,
83 self.lane_id.clone(),
84 ),
85 };
86 result.map_err(|_| MessagingModelError("messaging address cannot form a surface"))
87 }
88
89 pub fn validate(self) -> Result<Self, MessagingModelError> {
90 Self::new(
91 self.provider,
92 self.account_id,
93 self.conversation_id,
94 self.lane_id,
95 self.audience,
96 )
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase", deny_unknown_fields)]
102pub struct ExternalActor {
103 pub provider: String,
104 pub account_id: String,
105 pub external_user_id: String,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub display_name: Option<String>,
108}
109
110impl ExternalActor {
111 pub fn new(
112 provider: impl Into<String>,
113 account_id: impl Into<String>,
114 external_user_id: impl Into<String>,
115 display_name: Option<String>,
116 ) -> Result<Self, MessagingModelError> {
117 Ok(Self {
118 provider: provider_id(provider.into())?,
119 account_id: segment(account_id.into(), "messaging actor account id is invalid")?,
120 external_user_id: segment(
121 external_user_id.into(),
122 "messaging external user id is invalid",
123 )?,
124 display_name: display_name.map(optional_segment).transpose()?,
125 })
126 }
127
128 pub fn validate(self) -> Result<Self, MessagingModelError> {
129 Self::new(
130 self.provider,
131 self.account_id,
132 self.external_user_id,
133 self.display_name,
134 )
135 }
136}
137
138fn provider_id(value: String) -> Result<String, MessagingModelError> {
139 let normalized = value.trim().to_ascii_lowercase();
140 if normalized.is_empty()
141 || !normalized
142 .as_bytes()
143 .first()
144 .is_some_and(u8::is_ascii_lowercase)
145 || !normalized
146 .bytes()
147 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
148 {
149 return Err(MessagingModelError("messaging provider id is invalid"));
150 }
151 Ok(normalized)
152}
153
154fn segment(value: String, error: &'static str) -> Result<String, MessagingModelError> {
155 if value.is_empty() || value.trim() != value {
156 return Err(MessagingModelError(error));
157 }
158 Ok(value)
159}
160
161fn optional_segment(value: String) -> Result<String, MessagingModelError> {
162 segment(value, "messaging display name is invalid")
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn public_messaging_identity_is_validated_without_core_api() {
171 let address = MessagingAddress::new(
172 "Telegram",
173 "bot:1",
174 "chat:2",
175 Some("topic:3".to_string()),
176 ConversationAudience::Shared,
177 )
178 .unwrap();
179 assert_eq!(address.provider, "telegram");
180 assert!(address.conversation_surface().is_ok());
181 assert_eq!(address.base_address().lane_id, None);
182 assert!(ExternalActor::new("telegram", " bot", "user", None).is_err());
183 }
184}