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 LinearScope {
66 Team {
68 id: String,
70 key: Option<String>,
72 },
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub enum YouGileScope {
78 Board {
80 id: String,
82 },
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub enum ConfluenceScope {
88 Space {
90 key: Option<String>,
92 },
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub enum SlackScope {
98 Workspace { team_id: Option<String> },
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub enum TelegramScope {
105 Bot { bot_username: Option<String> },
107}
108
109#[derive(Debug, Clone)]
115pub enum ConfluenceAuthConfig {
116 BearerToken {
117 token: SecretString,
118 },
119 Basic {
120 username: String,
121 password: SecretString,
122 },
123}
124
125#[derive(Debug, Clone)]
136pub enum ProviderConfig {
137 GitLab {
138 base_url: String,
139 access_token: SecretString,
140 scope: GitLabScope,
141 extra: HashMap<String, serde_json::Value>,
142 },
143 GitHub {
144 base_url: String,
145 access_token: SecretString,
146 scope: GitHubScope,
147 extra: HashMap<String, serde_json::Value>,
148 },
149 ClickUp {
150 access_token: SecretString,
151 scope: ClickUpScope,
152 extra: HashMap<String, serde_json::Value>,
153 },
154 Jira {
155 base_url: String,
156 access_token: SecretString,
157 email: String,
158 scope: JiraScope,
159 flavor: Option<devboy_jira::JiraFlavor>,
162 extra: HashMap<String, serde_json::Value>,
163 },
164 Linear {
165 base_url: String,
166 access_token: SecretString,
167 scope: LinearScope,
168 extra: HashMap<String, serde_json::Value>,
169 },
170 YouGile {
171 base_url: String,
172 access_token: SecretString,
173 scope: YouGileScope,
174 extra: HashMap<String, serde_json::Value>,
175 },
176 Confluence {
177 base_url: String,
178 auth: ConfluenceAuthConfig,
179 scope: ConfluenceScope,
180 flavor: Option<devboy_confluence::ConfluenceFlavor>,
181 cloud_id: Option<String>,
182 api_version: Option<String>,
183 extra: HashMap<String, serde_json::Value>,
184 },
185 Fireflies {
187 api_key: SecretString,
188 extra: HashMap<String, serde_json::Value>,
189 },
190 Slack {
192 base_url: String,
193 access_token: SecretString,
194 scope: SlackScope,
195 required_scopes: Vec<String>,
196 extra: HashMap<String, serde_json::Value>,
197 },
198 Telegram {
200 base_url: String,
201 access_token: SecretString,
202 scope: TelegramScope,
203 extra: HashMap<String, serde_json::Value>,
204 },
205 Custom {
207 name: String,
208 config: HashMap<String, serde_json::Value>,
209 },
210}
211
212impl ProviderConfig {
213 pub fn provider_name(&self) -> &str {
215 match self {
216 Self::GitLab { .. } => "gitlab",
217 Self::GitHub { .. } => "github",
218 Self::ClickUp { .. } => "clickup",
219 Self::Jira { .. } => "jira",
220 Self::Linear { .. } => "linear",
221 Self::YouGile { .. } => "yougile",
222 Self::Confluence { .. } => "confluence",
223 Self::Fireflies { .. } => "fireflies",
224 Self::Slack { .. } => "slack",
225 Self::Telegram { .. } => "telegram",
226 Self::Custom { name, .. } => name,
227 }
228 }
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct ProxyConfig {
238 pub url: String,
239 #[serde(default)]
240 pub headers: HashMap<String, String>,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct ProviderMetadata {
253 pub data: serde_json::Value,
255}
256
257impl ProviderMetadata {
258 pub fn new(data: serde_json::Value) -> Self {
259 Self { data }
260 }
261}
262
263#[derive(Debug, Clone)]
274pub struct AdditionalContext {
275 pub provider: ProviderConfig,
276 pub proxy: Option<ProxyConfig>,
277 pub metadata: Option<ProviderMetadata>,
278 pub extra: HashMap<String, serde_json::Value>,
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use secrecy::ExposeSecret;
285
286 fn token(s: &str) -> SecretString {
287 SecretString::from(s.to_string())
288 }
289
290 #[test]
291 fn test_provider_config_gitlab_project_scope() {
292 let config = ProviderConfig::GitLab {
293 base_url: "https://gitlab.com".into(),
294 access_token: token("glpat-xxx"),
295 scope: GitLabScope::Project { id: "12345".into() },
296 extra: HashMap::new(),
297 };
298 assert_eq!(config.provider_name(), "gitlab");
299 }
300
301 #[test]
302 fn test_provider_config_github_repo_scope() {
303 let config = ProviderConfig::GitHub {
304 base_url: "https://api.github.com".into(),
305 access_token: token("ghp_xxx"),
306 scope: GitHubScope::Repository {
307 owner: "meteora-pro".into(),
308 repo: "devboy-tools".into(),
309 },
310 extra: HashMap::new(),
311 };
312 assert_eq!(config.provider_name(), "github");
313 }
314
315 #[test]
316 fn test_provider_config_custom() {
317 let config = ProviderConfig::Custom {
318 name: "my-provider".into(),
319 config: HashMap::new(),
320 };
321 assert_eq!(config.provider_name(), "my-provider");
322 }
323
324 #[test]
325 fn test_provider_config_confluence_scope() {
326 let config = ProviderConfig::Confluence {
327 base_url: "https://wiki.example.com".into(),
328 auth: ConfluenceAuthConfig::BearerToken {
329 token: token("pat-token"),
330 },
331 scope: ConfluenceScope::Space {
332 key: Some("ENG".into()),
333 },
334 flavor: None,
335 cloud_id: None,
336 api_version: Some("v1".into()),
337 extra: HashMap::new(),
338 };
339 assert_eq!(config.provider_name(), "confluence");
340 }
341
342 #[test]
343 fn test_provider_name_clickup() {
344 let config = ProviderConfig::ClickUp {
345 access_token: token("pk_test"),
346 scope: ClickUpScope::List {
347 id: "list1".into(),
348 team_id: None,
349 },
350 extra: HashMap::new(),
351 };
352 assert_eq!(config.provider_name(), "clickup");
353 }
354
355 #[test]
356 fn test_provider_name_jira() {
357 let config = ProviderConfig::Jira {
358 base_url: "https://jira.example.com".into(),
359 access_token: token("tok"),
360 email: "a@b.com".into(),
361 scope: JiraScope::Project { key: "X".into() },
362 flavor: None,
363 extra: HashMap::new(),
364 };
365 assert_eq!(config.provider_name(), "jira");
366 }
367
368 #[test]
369 fn test_provider_name_linear() {
370 let config = ProviderConfig::Linear {
371 base_url: "https://api.linear.app/graphql".into(),
372 access_token: token("lin_api_x"),
373 scope: LinearScope::Team {
374 id: "team-1".into(),
375 key: Some("ENG".into()),
376 },
377 extra: HashMap::new(),
378 };
379 assert_eq!(config.provider_name(), "linear");
380 }
381
382 #[test]
383 fn test_provider_name_yougile() {
384 let config = ProviderConfig::YouGile {
385 base_url: "https://yougile.com/api-v2".into(),
386 access_token: token("tok"),
387 scope: YouGileScope::Board {
388 id: "board-1".into(),
389 },
390 extra: HashMap::new(),
391 };
392 assert_eq!(config.provider_name(), "yougile");
393 }
394
395 #[test]
396 fn test_provider_name_telegram() {
397 let config = ProviderConfig::Telegram {
398 base_url: "https://api.telegram.org".into(),
399 access_token: token("bot-token"),
400 scope: TelegramScope::Bot { bot_username: None },
401 extra: HashMap::new(),
402 };
403 assert_eq!(config.provider_name(), "telegram");
404 }
405
406 #[test]
407 fn test_provider_metadata_new() {
408 let data = serde_json::json!({"statuses": [{"name": "Done"}]});
409 let meta = ProviderMetadata::new(data.clone());
410 assert_eq!(meta.data, data);
411 }
412
413 #[test]
414 fn test_proxy_config_serialize_deserialize() {
415 let mut headers = HashMap::new();
418 headers.insert("X-Routing".into(), "internal".into());
419 let proxy = ProxyConfig {
420 url: "https://proxy.internal/jira".into(),
421 headers,
422 };
423 let json = serde_json::to_string(&proxy).unwrap();
424 let deserialized: ProxyConfig = serde_json::from_str(&json).unwrap();
425 assert_eq!(deserialized.url, "https://proxy.internal/jira");
426 assert_eq!(deserialized.headers["X-Routing"], "internal");
427 }
428
429 #[test]
430 fn test_provider_config_debug_redacts_access_token() {
431 let config = ProviderConfig::GitLab {
432 base_url: "https://gitlab.com".into(),
433 access_token: token("super-secret-glpat"),
434 scope: GitLabScope::Project { id: "12345".into() },
435 extra: HashMap::new(),
436 };
437 let dbg = format!("{:?}", config);
438 assert!(
439 !dbg.contains("super-secret-glpat"),
440 "Debug must redact access_token, got: {dbg}"
441 );
442 }
443
444 #[test]
445 fn test_confluence_auth_basic_password_redacted() {
446 let auth = ConfluenceAuthConfig::Basic {
447 username: "dev@example.com".into(),
448 password: token("super-secret-password"),
449 };
450 let dbg = format!("{:?}", auth);
451 assert!(
452 !dbg.contains("super-secret-password"),
453 "Basic password must not appear in Debug: {dbg}"
454 );
455
456 if let ConfluenceAuthConfig::Basic { password, .. } = &auth {
458 assert_eq!(password.expose_secret(), "super-secret-password");
459 }
460 }
461}