Skip to main content

app_facade_api/
messaging.rs

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// App Facade owns its public messaging identity types. Their wire shapes intentionally overlap
12// Core's internal messaging model, but neither package depends on the other.
13#[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}