1use crate::backend::{load_config, say, tr};
11use sioul_core::porch::{self, By, Judged, Standing};
12use sioul_core::reach::Who;
13
14fn addresses(text: &str) -> Vec<String> {
16 let text = text.trim();
17 let list: Vec<String> = if text.starts_with('[') { serde_json::from_str(text).unwrap_or_default() } else { vec![text.to_string()] };
18 list.into_iter().map(|a| a.trim().to_ascii_lowercase()).filter(|a| !a.is_empty()).collect()
19}
20
21fn said(judged: &Judged) -> String {
25 let one = tr().text(&format!("sender-one-{}", judged.who.id()), None);
26 match judged.by {
27 By::Address | By::Number => say("sender-from-address", &[("list", one)]),
28 By::Card => say("sender-from-card", &[("list", one), ("name", judged.name.clone())]),
29 By::Category => say("sender-from-category", &[("list", one), ("name", judged.name.clone())]),
30 By::Domain | By::Prefix => say("sender-from-domain", &[("list", one), ("name", judged.name.trim_start_matches("*@").to_string())]),
31 By::LetIn => say("sender-from-let-in", &[("list", one)]),
32 By::Book => say("sender-from-book", &[("list", one)]),
33 By::Default => say("sender-from-default", &[("list", one)]),
34 }
35}
36
37fn by_id(by: By) -> String {
39 serde_json::to_value(by).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default()
40}
41
42pub(crate) fn standing_json(text: &str) -> String {
50 let config = load_config();
51 let senders = porch::Senders::load(&config);
52 let attention = sioul_core::attention::Attention::of(&config);
53 let first = addresses(text).into_iter().next().unwrap_or_default();
54 let (judged, own) = match first.strip_prefix(porch::TEL) {
55 Some(number) => {
56 let judged = senders.judge_number(number);
57 let own = judged.by == By::Number;
58 (judged, own)
59 }
60 None => {
61 let judged = senders.judge(&first);
62 let own = judged.by == By::Address;
63 (judged, own)
64 }
65 };
66 let choices: Vec<serde_json::Value> = std::iter::once(serde_json::json!({ "value": "", "label": tr().text("sender-categories", None) }))
67 .chain(Standing::ALL.iter().map(|s| serde_json::json!({ "value": s.as_str(), "label": sioul_core::attention::list_choice(tr(), Who::of(*s), &attention, true) })))
68 .collect();
69 serde_json::json!({
70 "standing": judged.who.id(),
71 "from": by_id(judged.by),
72 "name": judged.name,
73 "own": own,
74 "choice": if own { judged.who.id() } else { "" },
75 "said": said(&judged),
76 "choices": choices,
77 })
78 .to_string()
79}
80
81pub(crate) fn set_standing_of(text: &str, standing: &str) -> String {
85 let config = load_config();
86 let list = addresses(text);
87 let Some(first) = list.first().cloned() else { return String::new() };
88 let wanted = Standing::read(standing);
89 if wanted.is_none() && !standing.is_empty() {
90 return String::new();
91 }
92 for address in &list {
93 let done = match wanted {
94 Some(standing) => porch::set_standing(&config, address, standing),
95 None => porch::clear_standing(&config, address),
96 };
97 if let Err(e) = done {
98 return e;
99 }
100 }
101 let entry = porch::normalize(&first).unwrap_or(first).trim_start_matches("*@").to_string();
102 match wanted {
103 Some(standing) => say(&format!("sender-now-{}", standing.as_str()), &[("entry", entry)]),
104 None => say("sender-now-categories", &[("entry", entry)]),
105 }
106}
107
108fn card(key: &str) -> Option<sioul_core::contacts::Contact> {
110 let key = key.trim();
111 (!key.is_empty()).then(|| sioul_core::contacts::all().into_iter().find(|c| c.key == key || c.uid.trim() == key)).flatten()
112}
113
114pub(crate) fn person_json(key: &str) -> String {
121 let config = load_config();
122 let senders = porch::Senders::load(&config);
123 let Some(card) = card(key) else { return String::from("null") };
124 let judged = senders.judge_card(&card.key).unwrap_or_else(Judged::stranger);
125 let choice = senders.person_standing(&card.key).map_or("", |s| s.as_str());
127 let own: Vec<String> = card
129 .emails
130 .iter()
131 .map(|e| senders.judge(e.value.trim().trim_start_matches("mailto:")))
132 .chain(card.phones.iter().map(|p| senders.judge_number(&p.value)))
133 .filter(|j| matches!(j.by, By::Address | By::Number) && j.who != judged.who)
134 .map(|j| say("sender-own-entry", &[("entry", j.name.clone()), ("list", tr().text(&format!("sender-one-{}", j.who.id()), None))]))
135 .collect();
136 let sentence = if judged.by == By::Card { say("sender-from-address", &[("list", tr().text(&format!("sender-one-{}", judged.who.id()), None))]) } else { said(&judged) };
138 let choices: Vec<serde_json::Value> = std::iter::once(serde_json::json!({ "value": "", "label": tr().text("sender-categories", None) }))
139 .chain(Standing::ALL.iter().map(|s| serde_json::json!({ "value": s.as_str(), "label": tr().text(&format!("sender-one-{}", s.as_str()), None) })))
140 .collect();
141 serde_json::json!({
142 "who": judged.who.id(),
143 "from": by_id(judged.by),
144 "choice": choice,
145 "said": sentence,
146 "own": own,
147 "choices": choices,
148 })
149 .to_string()
150}
151
152pub(crate) fn set_person(key: &str, standing: &str) -> String {
157 let config = load_config();
158 let Some(card) = card(key) else { return String::new() };
159 let wanted = Standing::read(standing);
160 if wanted.is_none() && !standing.is_empty() {
161 return String::new();
162 }
163 let addresses: Vec<String> = card.emails.iter().map(|e| e.value.clone()).collect();
164 let numbers: Vec<String> = card.phones.iter().map(|p| p.value.clone()).collect();
165 if let Err(e) = porch::set_person(&config, &card.uid, &addresses, &numbers, wanted) {
166 return e;
167 }
168 let name = if card.name.trim().is_empty() { addresses.first().cloned().unwrap_or_default() } else { card.name.trim().to_string() };
169 match wanted {
170 Some(standing) => say(&format!("person-now-{}", standing.as_str()), &[("name", name)]),
171 None => say("person-now-categories", &[("name", name)]),
172 }
173}
174
175pub(crate) fn unblock(entry: &str) -> String {
179 let config = load_config();
180 let entry = entry.trim();
181 match porch::unblock(&config, entry) {
182 Ok(()) => say("ui-unblocked", &[("entry", porch::Entry::read(entry, sioul_core::reach::region(&config)).map_or_else(|| entry.to_string(), |e| e.shown()))]),
183 Err(e) => e,
184 }
185}