1use std::collections::HashMap;
7
8#[derive(Debug, Clone, Default)]
10pub struct ProjectChannelConfig {
11 pub telegram_topic_id: Option<String>,
12 #[allow(dead_code)]
13 pub telegram_bot_token: Option<String>,
14 #[allow(dead_code)]
15 pub telegram_chat_id: Option<String>,
16 pub discord_channel_id: Option<String>,
17 #[allow(dead_code)]
18 pub discord_bot_token: Option<String>,
19 #[allow(dead_code)]
20 pub discord_guild_id: Option<String>,
21}
22
23#[derive(Debug, Clone, Default)]
25pub struct GlobalChannelConfig {
26 pub telegram_general_topic_id: Option<String>,
27 pub discord_general_channel_id: Option<String>,
28 pub control_telegram_topic_id: Option<String>,
30 pub control_discord_channel_id: Option<String>,
32}
33
34pub struct ChannelRouter {
36 target_to_repo: HashMap<(String, String), String>,
38 repo_to_targets: HashMap<String, HashMap<String, String>>,
40 general: HashMap<String, String>,
42 control: HashMap<String, String>,
44 project_list: Vec<String>,
46}
47
48impl ChannelRouter {
49 pub fn new(global: &GlobalChannelConfig, projects: &[(String, ProjectChannelConfig)]) -> Self {
51 let mut target_to_repo = HashMap::new();
52 let mut repo_to_targets: HashMap<String, HashMap<String, String>> = HashMap::new();
53 let mut general = HashMap::new();
54 let mut control = HashMap::new();
55 let mut project_list = Vec::new();
56
57 if let Some(ref id) = global.telegram_general_topic_id {
58 general.insert("telegram".to_string(), id.clone());
59 }
60 if let Some(ref id) = global.discord_general_channel_id {
61 general.insert("discord".to_string(), id.clone());
62 }
63 if let Some(ref id) = global.control_telegram_topic_id {
64 control.insert("telegram".to_string(), id.clone());
65 }
66 if let Some(ref id) = global.control_discord_channel_id {
67 control.insert("discord".to_string(), id.clone());
68 }
69
70 for (repo, config) in projects {
71 project_list.push(repo.clone());
72 let mut targets = HashMap::new();
73
74 if let Some(ref topic_id) = config.telegram_topic_id {
75 target_to_repo.insert(("telegram".to_string(), topic_id.clone()), repo.clone());
76 targets.insert("telegram".to_string(), topic_id.clone());
77 }
78
79 if let Some(ref channel_id) = config.discord_channel_id {
80 target_to_repo.insert(("discord".to_string(), channel_id.clone()), repo.clone());
81 targets.insert("discord".to_string(), channel_id.clone());
82 }
83
84 repo_to_targets.insert(repo.clone(), targets);
85 }
86
87 Self {
88 target_to_repo,
89 repo_to_targets,
90 general,
91 control,
92 project_list,
93 }
94 }
95
96 pub fn resolve_project(&self, channel: &str, topic_or_channel_id: &str) -> Option<&str> {
99 self.target_to_repo
100 .get(&(channel.to_string(), topic_or_channel_id.to_string()))
101 .map(|s| s.as_str())
102 }
103
104 pub fn is_general(&self, channel: &str, topic_or_channel_id: &str) -> bool {
106 self.general
107 .get(channel)
108 .map(|id| id == topic_or_channel_id)
109 .unwrap_or(false)
110 }
111
112 pub fn is_control_channel(&self, channel: &str, topic_or_channel_id: &str) -> bool {
114 self.control
115 .get(channel)
116 .map(|id| id == topic_or_channel_id)
117 .unwrap_or(false)
118 }
119
120 pub fn target_for_project(&self, repo: &str, channel: &str) -> Option<&str> {
122 self.repo_to_targets
123 .get(repo)
124 .and_then(|targets| targets.get(channel))
125 .map(|s| s.as_str())
126 }
127
128 pub fn projects(&self) -> &[String] {
130 &self.project_list
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 fn test_router() -> ChannelRouter {
139 let global = GlobalChannelConfig {
140 telegram_general_topic_id: Some("1".to_string()),
141 discord_general_channel_id: Some("9999".to_string()),
142 ..Default::default()
143 };
144 let projects = vec![
145 (
146 "owner/orch".to_string(),
147 ProjectChannelConfig {
148 telegram_topic_id: Some("42".to_string()),
149 discord_channel_id: Some("1111".to_string()),
150 ..Default::default()
151 },
152 ),
153 (
154 "owner/bean".to_string(),
155 ProjectChannelConfig {
156 telegram_topic_id: Some("87".to_string()),
157 discord_channel_id: Some("2222".to_string()),
158 ..Default::default()
159 },
160 ),
161 ];
162 ChannelRouter::new(&global, &projects)
163 }
164
165 #[test]
166 fn resolve_telegram_topic_to_project() {
167 let router = test_router();
168 assert_eq!(router.resolve_project("telegram", "42"), Some("owner/orch"));
169 assert_eq!(router.resolve_project("telegram", "87"), Some("owner/bean"));
170 assert_eq!(router.resolve_project("telegram", "99"), None);
171 }
172
173 #[test]
174 fn resolve_discord_channel_to_project() {
175 let router = test_router();
176 assert_eq!(
177 router.resolve_project("discord", "1111"),
178 Some("owner/orch")
179 );
180 assert_eq!(
181 router.resolve_project("discord", "2222"),
182 Some("owner/bean")
183 );
184 }
185
186 #[test]
187 fn is_general_channel() {
188 let router = test_router();
189 assert!(router.is_general("telegram", "1"));
190 assert!(router.is_general("discord", "9999"));
191 assert!(!router.is_general("telegram", "42"));
192 assert!(!router.is_general("discord", "1111"));
193 }
194
195 #[test]
196 fn is_control_channel() {
197 let global = GlobalChannelConfig {
198 telegram_general_topic_id: Some("1".to_string()),
199 discord_general_channel_id: Some("9999".to_string()),
200 control_telegram_topic_id: Some("55".to_string()),
201 control_discord_channel_id: Some("77".to_string()),
202 };
203 let router = ChannelRouter::new(&global, &[]);
204 assert!(router.is_control_channel("telegram", "55"));
205 assert!(router.is_control_channel("discord", "77"));
206 assert!(!router.is_control_channel("telegram", "1")); assert!(!router.is_control_channel("discord", "9999")); assert!(!router.is_control_channel("telegram", "42")); assert!(!router.is_control_channel("slack", "55")); }
211
212 #[test]
213 fn control_channel_unconfigured_returns_false() {
214 let router = test_router(); assert!(!router.is_control_channel("telegram", "1"));
216 assert!(!router.is_control_channel("discord", "9999"));
217 }
218
219 #[test]
220 fn target_for_project() {
221 let router = test_router();
222 assert_eq!(
223 router.target_for_project("owner/orch", "telegram"),
224 Some("42")
225 );
226 assert_eq!(
227 router.target_for_project("owner/bean", "discord"),
228 Some("2222")
229 );
230 assert_eq!(router.target_for_project("owner/unknown", "telegram"), None);
231 }
232
233 #[test]
234 fn projects_list() {
235 let router = test_router();
236 assert_eq!(router.projects().len(), 2);
237 assert!(router.projects().contains(&"owner/orch".to_string()));
238 assert!(router.projects().contains(&"owner/bean".to_string()));
239 }
240
241 #[test]
242 fn unmapped_target_returns_none() {
243 let router = test_router();
244 assert_eq!(router.resolve_project("telegram", "1"), None); assert_eq!(router.resolve_project("slack", "anything"), None);
246 }
247
248 #[test]
249 fn project_with_custom_config_doesnt_affect_others() {
250 let global = GlobalChannelConfig {
251 telegram_general_topic_id: Some("1".into()),
252 discord_general_channel_id: None,
253 ..Default::default()
254 };
255 let projects = vec![
256 (
257 "owner/orch".into(),
258 ProjectChannelConfig {
259 telegram_topic_id: Some("42".into()),
260 telegram_bot_token: Some("custom-token".into()),
261 ..Default::default()
262 },
263 ),
264 (
265 "owner/bean".into(),
266 ProjectChannelConfig {
267 telegram_topic_id: Some("87".into()),
268 ..Default::default()
269 },
270 ),
271 ];
272 let router = ChannelRouter::new(&global, &projects);
273 assert_eq!(router.resolve_project("telegram", "42"), Some("owner/orch"));
275 assert_eq!(router.resolve_project("telegram", "87"), Some("owner/bean"));
276 assert_eq!(
278 router.target_for_project("owner/orch", "telegram"),
279 Some("42")
280 );
281 assert_eq!(
282 router.target_for_project("owner/bean", "telegram"),
283 Some("87")
284 );
285 assert!(router.is_general("telegram", "1"));
287 assert_eq!(router.projects().len(), 2);
289 }
290}