1use secrecy::SecretString;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub enum GitLabScope {
8 Project {
10 id: String,
12 },
13 Group {
15 id: String,
17 },
18 Global,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub enum GitHubScope {
25 Repository {
27 owner: String,
29 repo: String,
30 },
31 Organization {
33 name: String,
35 },
36 Global,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub enum ClickUpScope {
43 List {
45 id: String,
46 team_id: Option<String>,
48 },
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub enum JiraScope {
54 Project {
56 key: String,
58 },
59 MultiProject { keys: Vec<String> },
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub enum ConfluenceScope {
66 Space {
68 key: Option<String>,
70 },
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub enum SlackScope {
76 Workspace { team_id: Option<String> },
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub enum TelegramScope {
83 Bot { bot_username: Option<String> },
85}
86
87#[derive(Debug, Clone)]
93pub enum ConfluenceAuthConfig {
94 BearerToken {
95 token: SecretString,
96 },
97 Basic {
98 username: String,
99 password: SecretString,
100 },
101}
102
103#[derive(Debug, Clone)]
114pub enum ProviderConfig {
115 GitLab {
116 base_url: String,
117 access_token: SecretString,
118 scope: GitLabScope,
119 extra: HashMap<String, serde_json::Value>,
120 },
121 GitHub {
122 base_url: String,
123 access_token: SecretString,
124 scope: GitHubScope,
125 extra: HashMap<String, serde_json::Value>,
126 },
127 ClickUp {
128 access_token: SecretString,
129 scope: ClickUpScope,
130 extra: HashMap<String, serde_json::Value>,
131 },
132 Jira {
133 base_url: String,
134 access_token: SecretString,
135 email: String,
136 scope: JiraScope,
137 flavor: Option<devboy_jira::JiraFlavor>,
140 extra: HashMap<String, serde_json::Value>,
141 },
142 Confluence {
143 base_url: String,
144 auth: ConfluenceAuthConfig,
145 scope: ConfluenceScope,
146 api_version: Option<String>,
147 extra: HashMap<String, serde_json::Value>,
148 },
149 Fireflies {
151 api_key: SecretString,
152 extra: HashMap<String, serde_json::Value>,
153 },
154 Slack {
156 base_url: String,
157 access_token: SecretString,
158 scope: SlackScope,
159 required_scopes: Vec<String>,
160 extra: HashMap<String, serde_json::Value>,
161 },
162 Telegram {
164 base_url: String,
165 access_token: SecretString,
166 scope: TelegramScope,
167 extra: HashMap<String, serde_json::Value>,
168 },
169 Custom {
171 name: String,
172 config: HashMap<String, serde_json::Value>,
173 },
174}
175
176impl ProviderConfig {
177 pub fn provider_name(&self) -> &str {
179 match self {
180 Self::GitLab { .. } => "gitlab",
181 Self::GitHub { .. } => "github",
182 Self::ClickUp { .. } => "clickup",
183 Self::Jira { .. } => "jira",
184 Self::Confluence { .. } => "confluence",
185 Self::Fireflies { .. } => "fireflies",
186 Self::Slack { .. } => "slack",
187 Self::Telegram { .. } => "telegram",
188 Self::Custom { name, .. } => name,
189 }
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct ProxyConfig {
200 pub url: String,
201 #[serde(default)]
202 pub headers: HashMap<String, String>,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct ProviderMetadata {
215 pub data: serde_json::Value,
217}
218
219impl ProviderMetadata {
220 pub fn new(data: serde_json::Value) -> Self {
221 Self { data }
222 }
223}
224
225#[derive(Debug, Clone)]
236pub struct AdditionalContext {
237 pub provider: ProviderConfig,
238 pub proxy: Option<ProxyConfig>,
239 pub metadata: Option<ProviderMetadata>,
240 pub extra: HashMap<String, serde_json::Value>,
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use secrecy::ExposeSecret;
247
248 fn token(s: &str) -> SecretString {
249 SecretString::from(s.to_string())
250 }
251
252 #[test]
253 fn test_provider_config_gitlab_project_scope() {
254 let config = ProviderConfig::GitLab {
255 base_url: "https://gitlab.com".into(),
256 access_token: token("glpat-xxx"),
257 scope: GitLabScope::Project { id: "12345".into() },
258 extra: HashMap::new(),
259 };
260 assert_eq!(config.provider_name(), "gitlab");
261 }
262
263 #[test]
264 fn test_provider_config_github_repo_scope() {
265 let config = ProviderConfig::GitHub {
266 base_url: "https://api.github.com".into(),
267 access_token: token("ghp_xxx"),
268 scope: GitHubScope::Repository {
269 owner: "meteora-pro".into(),
270 repo: "devboy-tools".into(),
271 },
272 extra: HashMap::new(),
273 };
274 assert_eq!(config.provider_name(), "github");
275 }
276
277 #[test]
278 fn test_provider_config_custom() {
279 let config = ProviderConfig::Custom {
280 name: "my-provider".into(),
281 config: HashMap::new(),
282 };
283 assert_eq!(config.provider_name(), "my-provider");
284 }
285
286 #[test]
287 fn test_provider_config_confluence_scope() {
288 let config = ProviderConfig::Confluence {
289 base_url: "https://wiki.example.com".into(),
290 auth: ConfluenceAuthConfig::BearerToken {
291 token: token("pat-token"),
292 },
293 scope: ConfluenceScope::Space {
294 key: Some("ENG".into()),
295 },
296 api_version: Some("v1".into()),
297 extra: HashMap::new(),
298 };
299 assert_eq!(config.provider_name(), "confluence");
300 }
301
302 #[test]
303 fn test_provider_name_clickup() {
304 let config = ProviderConfig::ClickUp {
305 access_token: token("pk_test"),
306 scope: ClickUpScope::List {
307 id: "list1".into(),
308 team_id: None,
309 },
310 extra: HashMap::new(),
311 };
312 assert_eq!(config.provider_name(), "clickup");
313 }
314
315 #[test]
316 fn test_provider_name_jira() {
317 let config = ProviderConfig::Jira {
318 base_url: "https://jira.example.com".into(),
319 access_token: token("tok"),
320 email: "a@b.com".into(),
321 scope: JiraScope::Project { key: "X".into() },
322 flavor: None,
323 extra: HashMap::new(),
324 };
325 assert_eq!(config.provider_name(), "jira");
326 }
327
328 #[test]
329 fn test_provider_name_telegram() {
330 let config = ProviderConfig::Telegram {
331 base_url: "https://api.telegram.org".into(),
332 access_token: token("bot-token"),
333 scope: TelegramScope::Bot { bot_username: None },
334 extra: HashMap::new(),
335 };
336 assert_eq!(config.provider_name(), "telegram");
337 }
338
339 #[test]
340 fn test_provider_metadata_new() {
341 let data = serde_json::json!({"statuses": [{"name": "Done"}]});
342 let meta = ProviderMetadata::new(data.clone());
343 assert_eq!(meta.data, data);
344 }
345
346 #[test]
347 fn test_proxy_config_serialize_deserialize() {
348 let mut headers = HashMap::new();
351 headers.insert("X-Routing".into(), "internal".into());
352 let proxy = ProxyConfig {
353 url: "https://proxy.internal/jira".into(),
354 headers,
355 };
356 let json = serde_json::to_string(&proxy).unwrap();
357 let deserialized: ProxyConfig = serde_json::from_str(&json).unwrap();
358 assert_eq!(deserialized.url, "https://proxy.internal/jira");
359 assert_eq!(deserialized.headers["X-Routing"], "internal");
360 }
361
362 #[test]
363 fn test_provider_config_debug_redacts_access_token() {
364 let config = ProviderConfig::GitLab {
365 base_url: "https://gitlab.com".into(),
366 access_token: token("super-secret-glpat"),
367 scope: GitLabScope::Project { id: "12345".into() },
368 extra: HashMap::new(),
369 };
370 let dbg = format!("{:?}", config);
371 assert!(
372 !dbg.contains("super-secret-glpat"),
373 "Debug must redact access_token, got: {dbg}"
374 );
375 }
376
377 #[test]
378 fn test_confluence_auth_basic_password_redacted() {
379 let auth = ConfluenceAuthConfig::Basic {
380 username: "dev@example.com".into(),
381 password: token("super-secret-password"),
382 };
383 let dbg = format!("{:?}", auth);
384 assert!(
385 !dbg.contains("super-secret-password"),
386 "Basic password must not appear in Debug: {dbg}"
387 );
388
389 if let ConfluenceAuthConfig::Basic { password, .. } = &auth {
391 assert_eq!(password.expose_secret(), "super-secret-password");
392 }
393 }
394}