1use crate::{Result, SockudoError};
2use std::fmt;
3use std::str::FromStr;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
7pub enum Channel {
8 Public(PublicChannel),
9 Private(PrivateChannel),
10 Presence(PresenceChannel),
11 Encrypted(EncryptedChannel),
12}
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct PublicChannel(ChannelName);
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub struct PrivateChannel(ChannelName);
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24pub struct PresenceChannel(ChannelName);
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub struct EncryptedChannel(ChannelName);
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub struct ChannelName(String);
33
34impl ChannelName {
35 pub fn new(name: impl Into<String>) -> Result<Self> {
37 let name = name.into();
38 validate_channel_name(&name)?;
39 Ok(Self(name.to_owned()))
40 }
41
42 pub fn as_str(&self) -> &str {
44 &self.0
45 }
46
47 pub fn into_string(self) -> String {
49 self.0
50 }
51}
52
53impl AsRef<str> for ChannelName {
54 fn as_ref(&self) -> &str {
55 &self.0
56 }
57}
58
59impl fmt::Display for ChannelName {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 write!(f, "{}", self.0)
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ChannelType {
68 Public,
69 Private,
70 Presence,
71 Encrypted,
72}
73
74impl Channel {
75 pub fn from_string(s: impl Into<String>) -> Result<Self> {
77 let s = s.into();
78
79 if s.starts_with("private-encrypted-") {
80 let name = s.strip_prefix("private-encrypted-").unwrap();
81 Ok(Channel::Encrypted(EncryptedChannel(ChannelName::new(
82 name,
83 )?)))
84 } else if s.starts_with("presence-") {
85 let name = s.strip_prefix("presence-").unwrap();
86 Ok(Channel::Presence(PresenceChannel(ChannelName::new(name)?)))
87 } else if s.starts_with("private-") {
88 let name = s.strip_prefix("private-").unwrap();
89 Ok(Channel::Private(PrivateChannel(ChannelName::new(name)?)))
90 } else {
91 Ok(Channel::Public(PublicChannel(ChannelName::new(s)?)))
92 }
93 }
94
95 pub fn full_name(&self) -> String {
97 match self {
98 Channel::Public(ch) => ch.0.to_string(),
99 Channel::Private(ch) => format!("private-{}", ch.0),
100 Channel::Presence(ch) => format!("presence-{}", ch.0),
101 Channel::Encrypted(ch) => format!("private-encrypted-{}", ch.0),
102 }
103 }
104
105 pub fn channel_type(&self) -> ChannelType {
107 match self {
108 Channel::Public(_) => ChannelType::Public,
109 Channel::Private(_) => ChannelType::Private,
110 Channel::Presence(_) => ChannelType::Presence,
111 Channel::Encrypted(_) => ChannelType::Encrypted,
112 }
113 }
114
115 pub fn requires_auth(&self) -> bool {
117 !matches!(self, Channel::Public(_))
118 }
119
120 pub fn is_encrypted(&self) -> bool {
122 matches!(self, Channel::Encrypted(_))
123 }
124}
125
126impl fmt::Display for Channel {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 write!(f, "{}", self.full_name())
129 }
130}
131
132impl FromStr for Channel {
133 type Err = SockudoError;
134
135 fn from_str(s: &str) -> Result<Self> {
136 Channel::from_string(s)
137 }
138}
139
140impl PublicChannel {
142 pub fn new(name: impl Into<String>) -> Result<Self> {
143 Ok(Self(ChannelName::new(name)?))
144 }
145}
146
147impl PrivateChannel {
148 pub fn new(name: impl Into<String>) -> Result<Self> {
149 Ok(Self(ChannelName::new(name)?))
150 }
151}
152
153impl PresenceChannel {
154 pub fn new(name: impl Into<String>) -> Result<Self> {
155 Ok(Self(ChannelName::new(name)?))
156 }
157}
158
159impl EncryptedChannel {
160 pub fn new(name: impl Into<String>) -> Result<Self> {
161 Ok(Self(ChannelName::new(name)?))
162 }
163}
164
165use regex::Regex;
167use std::sync::LazyLock;
168
169static CHANNEL_NAME_PATTERN: LazyLock<Regex> =
170 LazyLock::new(|| Regex::new(r"^[A-Za-z0-9_\-=@,.;]+$").unwrap());
171
172fn validate_channel_name(name: &str) -> Result<()> {
173 if name.is_empty() {
174 return Err(SockudoError::Validation {
175 message: "Channel name cannot be empty".to_string(),
176 });
177 }
178
179 if name.len() > 200 {
180 return Err(SockudoError::Validation {
181 message: format!("Channel name too long: '{}' (max 200 characters)", name),
182 });
183 }
184
185 if !CHANNEL_NAME_PATTERN.is_match(name) {
186 return Err(SockudoError::Validation {
187 message: format!(
188 "Invalid channel name: '{}'. Must match pattern: [A-Za-z0-9_\\-=@,.;]+",
189 name
190 ),
191 });
192 }
193
194 Ok(())
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_channel_creation() {
203 assert!(Channel::from_string("test-channel").is_ok());
204 assert!(Channel::from_string("private-test").is_ok());
205 assert!(Channel::from_string("presence-test").is_ok());
206 assert!(Channel::from_string("private-encrypted-test").is_ok());
207 }
208
209 #[test]
210 fn test_channel_type_detection() {
211 let public = Channel::from_string("test").unwrap();
212 assert_eq!(public.channel_type(), ChannelType::Public);
213
214 let private = Channel::from_string("private-test").unwrap();
215 assert_eq!(private.channel_type(), ChannelType::Private);
216 }
217
218 #[test]
219 fn test_channel_name_validation() {
220 assert!(ChannelName::new("").is_err());
221 assert!(ChannelName::new("a".repeat(201)).is_err());
222 assert!(ChannelName::new("test channel").is_err()); assert!(ChannelName::new("test-channel_123").is_ok());
224 }
225}