1use crate::backend::{QtThread, Shared, load_config, set_status, tr};
10use jiff::Zoned;
11use jiff::tz::TimeZone;
12use sioul_core::projects::ProjectStore;
13use sioul_core::config::Config;
14use sioul_core::github::{self, Issue};
15use sioul_core::links::{self, LocalLinks};
16use sioul_core::trust::Trust;
17use sioul_core::vdir::{self, Collection, Kind};
18use sioul_core::{porch, tasks};
19use sioul_sync::github::Wanted;
20use sioul_sync::{Control, SyncError};
21use std::collections::BTreeSet;
22use std::path::PathBuf;
23use std::sync::Arc;
24use std::time::Duration;
25
26const EVERY: Duration = Duration::from_secs(30 * 60);
27const SETTLED: i64 = 3600;
30
31pub(crate) fn start(qt: &QtThread, shared: &Arc<Shared>) {
33 if !load_config().github.enabled || crate::backend::offline() {
34 return;
35 }
36 let control = Arc::new(Control::default());
37 {
38 let Ok(mut running) = shared.github.lock() else { return };
39 if let Some(running) = running.as_ref() {
40 running.retry();
41 return;
42 }
43 *running = Some(Arc::clone(&control));
44 }
45 let (qt, shared) = (qt.clone(), Arc::clone(shared));
46 std::thread::spawn(move || {
47 while !control.stopped() {
48 let config = load_config();
49 if !config.github.enabled {
50 break;
51 }
52 match bring(&config) {
53 Ok(0) => {}
54 Ok(_) => crate::work::show_work(&qt, &shared),
55 Err(e) => {
56 set_status(&qt, e.sentence(tr(), "GitHub"));
57 if e.is_lasting() && !control.park() {
59 break;
60 }
61 }
62 }
63 control.pause(EVERY);
64 }
65 if let Ok(mut running) = shared.github.lock()
66 && running.as_ref().is_some_and(|c| Arc::ptr_eq(c, &control))
67 {
68 *running = None;
69 }
70 });
71}
72
73pub(crate) fn stop(shared: &Shared) {
75 if let Some(control) = shared.github.lock().ok().and_then(|mut running| running.take()) {
76 control.stop();
77 }
78}
79
80fn list() -> Option<Collection> {
82 vdir::collections(Kind::Calendars).into_iter().find(|c| c.account == vdir::LOCAL && c.id == github::LIST_ID)
83}
84
85fn bring(config: &Config) -> Result<usize, SyncError> {
89 let list = match list() {
90 Some(list) => list,
91 None => vdir::create(Kind::Calendars, vdir::LOCAL, github::LIST_NAME, None, &["VTODO"]).map_err(SyncError::Disk)?,
92 };
93 let wanted = Wanted { assigned: config.github.assigned, reviews: config.github.reviews, created: config.github.created, mentioned: config.github.mentioned };
94 let open = sioul_sync::github::open_issues(wanted)?;
95 let store = config.notes_root_path().and_then(|root| ProjectStore::load(&root).ok()).map(|s| s.with_ties(&LocalLinks::load(&LocalLinks::default_path())));
97 let project_of = |issue: &Issue| store.as_ref().and_then(|s| s.route(&github::as_card(issue)).first().map(|r| r.project.id.clone()));
98 let now = Zoned::now();
99 let mut changed = 0;
100 let mut seen: BTreeSet<PathBuf> = BTreeSet::new();
101 for issue in &open {
102 let path = list.dir.join(github::file_name(&issue.repo, issue.number));
103 seen.insert(path.clone());
104 let text = match std::fs::read_to_string(&path) {
105 Ok(before) => github::updated(&before, issue, project_of(issue).as_deref(), &now),
106 Err(_) => Some(github::new_task(issue, project_of(issue).as_deref(), &now)),
107 };
108 if let Some(text) = text {
109 vdir::write_item(&path, &text).map_err(SyncError::Disk)?;
110 changed += 1;
111 }
112 }
113 for path in list.items().into_iter().filter(|p| !seen.contains(p)) {
114 let Ok(before) = std::fs::read_to_string(&path) else { continue };
115 if github::state_of(&before).as_deref() != Some("open") {
116 continue;
117 }
118 let Some((repo, number)) = tasks::task_of_text(&before, &TimeZone::system()).and_then(|t| github::parse_uid(&t.uid)) else { continue };
119 let text = match sioul_sync::github::issue(&repo, number, &github::reason_of(&before))? {
120 Some(issue) if issue.state == "closed" => github::updated(&before, &issue, None, &now),
121 Some(issue) if issue.updated_at.parse::<jiff::Timestamp>().is_ok_and(|at| now.timestamp().as_second() - at.as_second() < SETTLED) => None,
123 _ => github::gone(&before, &now),
124 };
125 if let Some(text) = text {
126 vdir::write_item(&path, &text).map_err(SyncError::Disk)?;
127 changed += 1;
128 }
129 }
130 Ok(changed)
131}
132
133pub(crate) fn tie_mail(config: &Config, files: &[PathBuf], ties: &mut LocalLinks) {
138 if !config.github.enabled {
139 return;
140 }
141 let Some(list) = list() else { return };
142 let senders = porch::Senders::load(config);
143 let judged = porch::judge(files, &config.mail_sources(), None, &porch::KnownSenders::default(), &senders, Zoned::now().timestamp().as_second());
144 for card in judged.into_iter().filter(|t| t.trust == Trust::Verified).map(|t| t.card) {
145 let (Some((repo, number)), Some(mid)) = (github::thread_of(&card), card.message_id.as_deref()) else { continue };
146 let Ok(text) = std::fs::read_to_string(list.dir.join(github::file_name(&repo, number))) else { continue };
147 let Some(task) = tasks::task_of_text(&text, &TimeZone::system()) else { continue };
148 ties.add(&links::mail_uri(mid), &links::task_uri(&task.uid), "link");
149 if let Some(project) = task.projects.first() {
150 ties.add(&links::mail_uri(mid), &links::project_uri(project), "project");
151 }
152 }
153}