use std::collections::BTreeMap;
use serde::Deserialize;
use serde_json::{Map, Value, json};
use crate::{CONTACTS, Client, Error, Result, ids_of};
#[derive(Debug, Clone, Default, Deserialize)]
pub struct NameComponent {
#[serde(default)]
pub kind: String,
#[serde(default)]
pub value: String,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Name {
#[serde(default)]
pub full: Option<String>,
#[serde(default)]
pub components: Vec<NameComponent>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct EmailEntry {
pub address: String,
#[serde(default)]
pub contexts: Option<BTreeMap<String, bool>>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Phone {
pub number: String,
#[serde(default)]
pub contexts: Option<BTreeMap<String, bool>>,
#[serde(default)]
pub features: Option<BTreeMap<String, bool>>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Organization {
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Note {
pub note: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Card {
pub id: String,
#[serde(default)]
pub address_book_ids: BTreeMap<String, bool>,
#[serde(default)]
pub name: Option<Name>,
#[serde(default)]
pub emails: Option<BTreeMap<String, EmailEntry>>,
#[serde(default)]
pub phones: Option<BTreeMap<String, Phone>>,
#[serde(default)]
pub organizations: Option<BTreeMap<String, Organization>>,
#[serde(default)]
pub notes: Option<BTreeMap<String, Note>>,
#[serde(skip)]
pub account: String,
}
impl Card {
pub fn full_name(&self) -> String {
if let Some(full) = self.name.as_ref().and_then(|name| name.full.clone()).filter(|full| !full.is_empty()) {
return full;
}
let parts: Vec<&str> = self
.name
.iter()
.flat_map(|name| name.components.iter())
.filter(|part| !part.value.is_empty() && part.kind != "separator")
.map(|part| part.value.as_str())
.collect();
if !parts.is_empty() {
return parts.join(" ");
}
self.emails().into_iter().next().unwrap_or_default()
}
pub fn emails(&self) -> Vec<String> {
self.emails.iter().flat_map(|map| map.values()).map(|entry| entry.address.clone()).collect()
}
pub fn phones(&self) -> Vec<String> {
self.phones.iter().flat_map(|map| map.values()).map(|phone| phone.number.clone()).collect()
}
pub fn organization(&self) -> Option<String> {
let names: Vec<_> =
self.organizations.iter().flat_map(|map| map.values()).filter_map(|o| o.name.clone()).collect();
(!names.is_empty()).then(|| names.join(", "))
}
pub fn notes(&self) -> Vec<String> {
self.notes.iter().flat_map(|map| map.values()).map(|note| note.note.clone()).collect()
}
}
#[derive(Deserialize)]
struct List<T> {
list: Vec<T>,
}
pub async fn cards(client: &Client, text: &str) -> Result<Vec<Card>> {
let mut out = Vec::new();
for (account, _) in client.session.accounts_with(CONTACTS) {
let mut filter = Map::new();
if !text.trim().is_empty() {
filter.insert("text".into(), text.trim().into());
}
let responses = client
.request(
&[CONTACTS],
vec![
("ContactCard/query", json!({ "accountId": account, "filter": filter }), "q"),
("ContactCard/get", json!({ "accountId": account, "#ids": ids_of("q", "ContactCard/query") }), "g"),
],
)
.await?;
let value = responses.get("g").cloned().unwrap_or(json!({ "list": [] }));
let cards = serde_json::from_value::<List<Card>>(value).map_err(|error| Error::Protocol(error.to_string()))?;
out.extend(cards.list.into_iter().map(|mut card| {
card.account = account.to_owned();
card
}));
}
out.sort_by_key(|card| card.full_name().to_lowercase());
Ok(out)
}
#[derive(Debug, Clone, Copy, Default, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct BookRights {
pub may_write: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AddressBook {
pub id: String,
pub name: String,
#[serde(default)]
pub is_default: bool,
#[serde(default)]
pub my_rights: Option<BookRights>,
#[serde(skip)]
pub account: String,
#[serde(skip)]
pub shared: Option<String>,
}
impl AddressBook {
pub fn writable(&self) -> bool {
self.my_rights.is_none_or(|rights| rights.may_write)
}
}
pub async fn address_books(client: &Client) -> Result<Vec<AddressBook>> {
let mut out = Vec::new();
for (id, account) in client.session.accounts_with(CONTACTS) {
let result = client.call(&[CONTACTS], "AddressBook/get", json!({ "accountId": id })).await?;
let books =
serde_json::from_value::<List<AddressBook>>(result).map_err(|error| Error::Protocol(error.to_string()))?;
out.extend(books.list.into_iter().map(|mut book| {
book.account = id.to_owned();
book.shared = (!account.is_personal).then(|| account.name.clone());
book
}));
}
out.sort_by_key(|book| (book.shared.is_some(), !book.is_default, book.name.to_lowercase()));
Ok(out)
}
pub const CONTEXTS: [&str; 2] = ["work", "private"];
pub const FEATURES: [&str; 7] = ["mobile", "voice", "text", "video", "fax", "pager", "textphone"];
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Labelled {
pub value: String,
pub labels: Vec<String>,
}
impl Labelled {
fn of(value: &str, maps: &[&Option<BTreeMap<String, bool>>]) -> Self {
let labels = maps
.iter()
.flat_map(|map| map.iter().flat_map(|map| map.iter()))
.filter(|(_, on)| **on)
.map(|(label, _)| label.clone())
.collect();
Self { value: value.to_owned(), labels }
}
pub fn render(&self) -> String {
if self.labels.is_empty() { self.value.clone() } else { format!("{} ({})", self.value, self.labels.join(", ")) }
}
pub fn parse(text: &str, allowed: &[&str]) -> std::result::Result<Self, String> {
let text = text.trim();
let (value, labels) = match text.strip_suffix(')').and_then(|rest| rest.rsplit_once('(')) {
Some((value, labels)) => {
(value.trim(), labels.split(',').map(|label| label.trim().to_lowercase()).collect())
}
None => (text, Vec::new()),
};
let labels: Vec<String> = labels.into_iter().filter(|label: &String| !label.is_empty()).collect();
if let Some(unknown) = labels.iter().find(|label| !allowed.contains(&label.as_str())) {
return Err(format!("unknown label {unknown} – one of {}", allowed.join(", ")));
}
Ok(Self { value: value.to_owned(), labels })
}
fn set(&self, among: &[&str]) -> Value {
let on: Map<String, Value> = self
.labels
.iter()
.filter(|label| among.contains(&label.as_str()))
.map(|label| (label.clone(), json!(true)))
.collect();
if on.is_empty() { Value::Null } else { Value::Object(on) }
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CardForm {
pub name: String,
pub emails: Vec<Labelled>,
pub phones: Vec<Labelled>,
pub organizations: Vec<String>,
pub note: String,
}
impl CardForm {
pub fn of(card: &Card) -> Self {
Self {
name: card.full_name(),
emails: card
.emails
.iter()
.flat_map(|map| map.values())
.map(|entry| Labelled::of(&entry.address, &[&entry.contexts]))
.collect(),
phones: card
.phones
.iter()
.flat_map(|map| map.values())
.map(|phone| Labelled::of(&phone.number, &[&phone.contexts, &phone.features]))
.collect(),
organizations: card
.organizations
.iter()
.flat_map(|map| map.values())
.filter_map(|organization| organization.name.clone())
.collect(),
note: card.notes().join("\n"),
}
}
pub fn object(&self) -> Value {
let map = |prefix: &str, values: Vec<Value>| -> Value {
if values.is_empty() {
return Value::Null;
}
Value::Object(
values.into_iter().enumerate().map(|(n, value)| (format!("{prefix}{}", n + 1), value)).collect(),
)
};
let emails = self
.emails
.iter()
.map(|email| json!({ "@type": "EmailAddress", "address": email.value, "contexts": email.set(&CONTEXTS) }))
.collect();
let phones = self
.phones
.iter()
.map(|phone| {
json!({ "@type": "Phone", "number": phone.value, "contexts": phone.set(&CONTEXTS),
"features": phone.set(&FEATURES) })
})
.collect();
let organizations =
self.organizations.iter().map(|name| json!({ "@type": "Organization", "name": name })).collect();
json!({
"name": { "full": self.name },
"emails": map("e", emails),
"phones": map("p", phones),
"organizations": map("o", organizations),
"notes": if self.note.is_empty() { Value::Null } else { json!({ "n1": { "@type": "Note", "note": self.note } }) },
})
}
}
pub async fn create(client: &Client, book: &AddressBook, form: &CardForm) -> Result<()> {
let mut object = form.object();
object["@type"] = "Card".into();
object["version"] = "1.0".into();
object["addressBookIds"] = json!({ book.id.as_str(): true });
client.set(&[CONTACTS], "ContactCard", &book.account, json!({ "create": { "c": object } }), json!({})).await?;
Ok(())
}
pub async fn update(client: &Client, book: &AddressBook, id: &str, form: &CardForm) -> Result<()> {
let mut object = form.object();
object["addressBookIds"] = json!({ book.id.as_str(): true });
client.set(&[CONTACTS], "ContactCard", &book.account, json!({ "update": { id: object } }), json!({})).await?;
Ok(())
}
pub async fn raw_cards(client: &Client, account: &str, ids: &[String]) -> Result<Vec<Map<String, Value>>> {
let result = client.call(&[CONTACTS], "ContactCard/get", json!({ "accountId": account, "ids": ids })).await?;
Ok(result
.get("list")
.and_then(Value::as_array)
.map(|list| list.iter().filter_map(|card| card.as_object().cloned()).collect())
.unwrap_or_default())
}
pub async fn import(
client: &Client,
book: &AddressBook,
cards: Vec<Map<String, Value>>,
) -> Result<(usize, Vec<String>)> {
let create: Map<String, Value> = cards
.into_iter()
.enumerate()
.map(|(n, mut card)| {
card.insert("addressBookIds".into(), json!({ book.id.as_str(): true }));
(format!("i{n}"), Value::Object(card))
})
.collect();
let result =
client.call(&[CONTACTS], "ContactCard/set", json!({ "accountId": book.account, "create": create })).await?;
Ok(crate::calendars::made_and_refused(&result))
}
pub async fn destroy(client: &Client, account: &str, id: &str) -> Result<()> {
client.set(&[CONTACTS], "ContactCard", account, json!({ "destroy": [id] }), json!({})).await?;
Ok(())
}