1use crate::{ChannelType, IronError, Result};
11use crate::utils::get_channel_type;
12use crate::capabilities::Capability;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum IronVersion {
17 V1,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum LegionVersion {
23 V1,
24}
25
26impl LegionVersion {
27 pub fn as_capability(&self) -> &'static str {
29 match self {
30 LegionVersion::V1 => "+legion-protocol/v1",
31 }
32 }
33}
34
35impl IronVersion {
36 pub fn as_capability(&self) -> &'static str {
38 match self {
39 IronVersion::V1 => "+iron-protocol/v1",
40 }
41 }
42
43 pub fn to_legion_version(&self) -> LegionVersion {
45 match self {
46 IronVersion::V1 => LegionVersion::V1,
47 }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum IronNegotiationResult {
54 LegionCapable { version: LegionVersion },
56 IronCapable { version: IronVersion },
58 IrcFallback,
60 NotSupported,
62}
63
64#[derive(Debug, Clone)]
66pub struct IronSession {
67 iron_version: Option<IronVersion>, legion_version: Option<LegionVersion>, encrypted_channels: Vec<String>,
70 negotiation_complete: bool,
71}
72
73impl IronSession {
74 pub fn new() -> Self {
76 Self {
77 iron_version: None,
78 legion_version: None,
79 encrypted_channels: Vec::new(),
80 negotiation_complete: false,
81 }
82 }
83
84 pub fn set_version(&mut self, version: IronVersion) {
86 self.iron_version = Some(version);
87 }
88
89 pub fn set_legion_version(&mut self, version: LegionVersion) {
91 self.legion_version = Some(version);
92 }
93
94 pub fn is_iron_active(&self) -> bool {
96 (self.legion_version.is_some() || self.iron_version.is_some()) && self.negotiation_complete
97 }
98
99 pub fn is_legion_active(&self) -> bool {
101 self.legion_version.is_some() && self.negotiation_complete
102 }
103
104 pub fn version(&self) -> Option<IronVersion> {
106 self.iron_version
107 }
108
109 pub fn legion_version(&self) -> Option<LegionVersion> {
111 self.legion_version
112 }
113
114 pub fn complete_negotiation(&mut self) {
116 self.negotiation_complete = true;
117 }
118
119 pub fn is_encrypted_channel(&self, channel: &str) -> bool {
121 self.encrypted_channels.iter().any(|c| c == channel)
122 }
123
124 pub fn add_encrypted_channel(&mut self, channel: String) {
126 if !self.encrypted_channels.contains(&channel) {
127 self.encrypted_channels.push(channel);
128 }
129 }
130
131 pub fn remove_encrypted_channel(&mut self, channel: &str) {
133 self.encrypted_channels.retain(|c| c != channel);
134 }
135}
136
137impl Default for IronSession {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143pub struct IronChannelHandler;
145
146impl IronChannelHandler {
147 pub fn can_join_channel(
149 channel: &str,
150 user_has_legion: bool,
151 server_has_legion: bool,
152 ) -> Result<ChannelJoinResult> {
153 let channel_type = get_channel_type(channel);
154
155 match channel_type {
156 ChannelType::IrcGlobal | ChannelType::IrcLocal => {
157 Ok(ChannelJoinResult::Allowed)
159 }
160 ChannelType::LegionEncrypted => {
161 if user_has_legion && server_has_legion {
163 Ok(ChannelJoinResult::AllowedEncrypted)
164 } else {
165 Ok(ChannelJoinResult::Denied {
166 reason: IronChannelError::IncompatibleClient,
167 })
168 }
169 }
170 ChannelType::Invalid => Err(IronError::Parse(format!(
171 "Invalid channel name: {}",
172 channel
173 ))),
174 }
175 }
176
177 pub fn generate_error_message(channel: &str, error: &IronChannelError) -> String {
179 match error {
180 IronChannelError::IncompatibleClient => {
181 format!(
182 "Cannot join encrypted channel {} - requires Legion Protocol support. \
183 Upgrade to a Legion-compatible client or ask channel admin to create \
184 a standard IRC channel (#{}) for IRC users.",
185 channel,
186 &channel[1..] )
188 }
189 IronChannelError::EncryptionRequired => {
190 format!(
191 "Channel {} requires end-to-end encryption. \
192 Please use a Legion Protocol-compatible client.",
193 channel
194 )
195 }
196 }
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
202pub enum ChannelJoinResult {
203 Allowed,
205 AllowedEncrypted,
207 Denied { reason: IronChannelError },
209}
210
211#[derive(Debug, Clone, PartialEq, Eq)]
213pub enum IronChannelError {
214 IncompatibleClient,
216 EncryptionRequired,
218}
219
220pub fn detect_legion_support(
222 client_caps: &[Capability],
223 server_caps: &[Capability],
224) -> IronNegotiationResult {
225 let client_legion = client_caps
226 .iter()
227 .any(|cap| matches!(cap, Capability::LegionProtocolV1));
228 let server_legion = server_caps
229 .iter()
230 .any(|cap| matches!(cap, Capability::LegionProtocolV1));
231
232 let client_iron = client_caps
233 .iter()
234 .any(|cap| matches!(cap, Capability::IronProtocolV1));
235 let server_iron = server_caps
236 .iter()
237 .any(|cap| matches!(cap, Capability::IronProtocolV1));
238
239 match (client_legion, server_legion) {
241 (true, true) => IronNegotiationResult::LegionCapable {
242 version: LegionVersion::V1,
243 },
244 _ => {
245 match (client_iron, server_iron) {
247 (true, true) => IronNegotiationResult::IronCapable {
248 version: IronVersion::V1,
249 },
250 (true, false) | (false, true) => {
251 if client_legion || server_legion {
253 IronNegotiationResult::IrcFallback
254 } else {
255 IronNegotiationResult::IrcFallback
256 }
257 },
258 (false, false) => {
259 if client_legion || server_legion {
261 IronNegotiationResult::IrcFallback
262 } else {
263 IronNegotiationResult::NotSupported
264 }
265 },
266 }
267 }
268 }
269}
270
271#[deprecated(note = "Use detect_legion_support instead")]
273pub fn detect_iron_support(
274 client_caps: &[Capability],
275 server_caps: &[Capability],
276) -> IronNegotiationResult {
277 detect_legion_support(client_caps, server_caps)
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_iron_version_capability() {
286 assert_eq!(IronVersion::V1.as_capability(), "+iron-protocol/v1");
288
289 assert_eq!(LegionVersion::V1.as_capability(), "+legion-protocol/v1");
291
292 assert_eq!(IronVersion::V1.to_legion_version(), LegionVersion::V1);
294 }
295
296 #[test]
297 fn test_channel_access_control() {
298 let result = IronChannelHandler::can_join_channel("#general", false, false).unwrap();
300 assert_eq!(result, ChannelJoinResult::Allowed);
301
302 let result = IronChannelHandler::can_join_channel("!encrypted", true, true).unwrap();
304 assert_eq!(result, ChannelJoinResult::AllowedEncrypted);
305
306 let result = IronChannelHandler::can_join_channel("!encrypted", false, true).unwrap();
308 assert!(matches!(
309 result,
310 ChannelJoinResult::Denied {
311 reason: IronChannelError::IncompatibleClient
312 }
313 ));
314 }
315
316 #[test]
317 fn test_legion_detection() {
318 let client_caps = vec![Capability::LegionProtocolV1, Capability::MessageTags];
320 let server_caps = vec![Capability::LegionProtocolV1, Capability::Sasl];
321
322 let result = detect_legion_support(&client_caps, &server_caps);
323 assert_eq!(
324 result,
325 IronNegotiationResult::LegionCapable {
326 version: LegionVersion::V1
327 }
328 );
329
330 let client_caps = vec![Capability::IronProtocolV1, Capability::MessageTags];
332 let server_caps = vec![Capability::IronProtocolV1, Capability::Sasl];
333
334 let result = detect_legion_support(&client_caps, &server_caps);
335 assert_eq!(
336 result,
337 IronNegotiationResult::IronCapable {
338 version: IronVersion::V1
339 }
340 );
341
342 let client_caps = vec![Capability::MessageTags];
344 let result = detect_legion_support(&client_caps, &server_caps);
345 assert_eq!(result, IronNegotiationResult::IrcFallback);
346
347 #[allow(deprecated)]
349 let result = detect_iron_support(&client_caps, &server_caps);
350 assert_eq!(result, IronNegotiationResult::IrcFallback);
351 }
352
353 #[test]
354 fn test_legion_session() {
355 let mut session = IronSession::new();
356 assert!(!session.is_iron_active());
357 assert!(!session.is_legion_active());
358
359 session.set_version(IronVersion::V1);
361 session.complete_negotiation();
362 assert!(session.is_iron_active());
363 assert!(!session.is_legion_active());
364 assert_eq!(session.version(), Some(IronVersion::V1));
365
366 let mut legion_session = IronSession::new();
368 legion_session.set_legion_version(LegionVersion::V1);
369 legion_session.complete_negotiation();
370 assert!(legion_session.is_iron_active()); assert!(legion_session.is_legion_active());
372 assert_eq!(legion_session.legion_version(), Some(LegionVersion::V1));
373
374 session.add_encrypted_channel("!secure".to_string());
375 assert!(session.is_encrypted_channel("!secure"));
376 assert!(!session.is_encrypted_channel("!other"));
377 }
378}