Skip to content

Commit

Permalink
Sync contacts
Browse files Browse the repository at this point in the history
Now we resolve user names from the synced address book, if possible.
We fallback to the profile name, if contact is not in the address
book. And eventually to contacts phone number, if profile retrieval
failed.

The contacts are synced on start-up of the app, however max one time
per hour.

Resolved #140
  • Loading branch information
boxdot committed Mar 11, 2022
1 parent 52f6976 commit 049ca91
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 181 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ debug = 0
lto = "thin"

[dependencies]
presage = { git = "https://github.com/whisperfish/presage.git", branch = "main" }
presage = { git = "https://github.com/whisperfish/presage.git", rev = "f09db39" }

anyhow = "1.0.40"
async-trait = "0.1.51"
Expand Down
208 changes: 133 additions & 75 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ use crate::util::{
};

use anyhow::{anyhow, Context as _};
use chrono::{DateTime, Duration, Utc};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
use itertools::Itertools;
use log::info;
use notify_rust::Notification;
use phonenumber::{Mode, PhoneNumber};
use presage::prelude::proto::{AttachmentPointer, ReceiptMessage, TypingMessage};
Expand All @@ -34,6 +36,9 @@ use std::convert::{TryFrom, TryInto};
use std::path::Path;
use std::str::FromStr;

/// Amount of time to skip contacts sync after the last sync
const CONTACTS_SYNC_DEADLINE_SEC: i64 = 60 * 60; // 1h

pub struct App {
pub config: Config,
signal_manager: Box<dyn SignalManager>,
Expand Down Expand Up @@ -255,13 +260,18 @@ impl BoxData {
#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppData {
pub channels: FilteredStatefulList<Channel>,
/// Names retrieved from profiles or phone number if it failed
///
/// Do not use directly, use [`App::name_by_id`] instead.
pub names: HashMap<Uuid, String>,
#[serde(skip)] // ! We may want to save it
pub input: BoxData,
#[serde(skip)]
pub search_box: BoxData,
#[serde(skip)]
pub is_multiline_input: bool,
#[serde(default)]
pub contacts_sync_request_at: Option<DateTime<Utc>>,
}

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -338,23 +348,6 @@ pub struct GroupData {
}

impl Channel {
pub fn contains_user(&self, name: &str, hm: &HashMap<Uuid, String>) -> bool {
match self.group_data {
Some(ref gd) => gd.members.iter().any(|u| name_by_id(hm, *u).contains(name)),
None => self.name.contains(name),
}
}

pub fn match_pattern(&self, pattern: &str, hm: &HashMap<Uuid, String>) -> bool {
if pattern.is_empty() {
return true;
}
match pattern.chars().next().unwrap() {
'@' => self.contains_user(&pattern[1..], hm),
_ => self.name.contains(pattern),
}
}

pub fn reset_writing(&mut self, user: Uuid) {
match &mut self.typing {
TypingSet::GroupTyping(ref mut hash_set) => {
Expand Down Expand Up @@ -551,7 +544,7 @@ impl App {
storage: Box<dyn Storage>,
) -> anyhow::Result<Self> {
let user_id = signal_manager.user_id();
let data = storage.load_app_data(user_id, config.user.name.clone())?;
let data = storage.load_app_data()?;
Ok(Self {
config,
signal_manager,
Expand All @@ -576,35 +569,59 @@ impl App {
}
}

pub fn writing_people(&self, channel: &Channel) -> String {
if !channel.is_writing() {
return String::from("");
}
let uuids: Vec<Uuid> = match &channel.typing {
TypingSet::GroupTyping(hash_set) => hash_set.clone().into_iter().collect(),
TypingSet::SingleTyping(a) => {
if *a {
vec![channel.user_id().unwrap()]
} else {
Vec::new()
pub fn writing_people(&self, channel: &Channel) -> Option<String> {
if channel.is_writing() {
let uuids: Box<dyn Iterator<Item = Uuid>> = match &channel.typing {
TypingSet::GroupTyping(uuids) => Box::new(uuids.iter().copied()),
TypingSet::SingleTyping(a) => {
if *a {
Box::new(std::iter::once(channel.user_id().unwrap()))
} else {
Box::new(std::iter::empty())
}
}
}
};
format!(
"{:?} writing...",
uuids
.into_iter()
.map(|u| self.name_by_id(u))
.collect::<Vec<&str>>()
)
};
Some(format!(
"[{}] writing...",
uuids.map(|id| self.name_by_id(id)).format(", ")
))
} else {
None
}
}

pub fn save(&self) -> anyhow::Result<()> {
self.storage.save_app_data(&self.data)
}

pub fn name_by_id(&self, id: Uuid) -> &str {
name_by_id(&self.data.names, id)
pub fn name_by_id(&self, id: Uuid) -> String {
if self.user_id == id {
// it's me
self.config.user.name.clone()
} else if let Some(contact) = self
.signal_manager
.contact_by_id(id)
.ok()
.flatten()
.filter(|contact| !contact.name.is_empty())
{
// user is known via our contact list
contact.name
} else if let Some(name) = self.data.names.get(&id) {
// user should be at least known via their profile or phone number
name.clone()
} else {
// give up
"Unknown User".to_string()
}
}

pub fn channel_name<'a>(&self, channel: &'a Channel) -> Cow<'a, str> {
if let Some(id) = channel.user_id() {
self.name_by_id(id).into()
} else {
(&channel.name).into()
}
}

pub fn on_key(&mut self, key: KeyEvent) -> anyhow::Result<()> {
Expand Down Expand Up @@ -911,18 +928,17 @@ impl App {
.ensure_group_channel_exists(master_key, revision)
.await
.context("failed to create group channel")?;
let from = self
.ensure_user_is_known(uuid, profile_key, phone_number)
.await
.to_string();

self.ensure_user_is_known(uuid, profile_key, phone_number)
.await;
let from = self.name_by_id(uuid);

(channel_idx, from)
} else {
// incoming direct message
let name = self
.ensure_user_is_known(uuid, profile_key, phone_number)
.await
.to_string();
self.ensure_user_is_known(uuid, profile_key, phone_number)
.await;
let name = self.name_by_id(uuid);
let channel_idx = self.ensure_contact_channel_exists(uuid, &name).await;
let from = self.data.channels.items[channel_idx].name.clone();
// Reset typing notification as the Tipyng::Stop are not always sent by the server when a message is sent.
Expand Down Expand Up @@ -1257,11 +1273,13 @@ impl App {
.iter()
.position(|channel| channel.id == channel_id)?;
let channel = &mut self.data.channels.items[channel_idx];

let message = channel
.messages
.items
.iter_mut()
.find(|m| m.arrived_at == target_sent_timestamp)?;

let reaction_idx = message
.reactions
.iter()
Expand All @@ -1281,19 +1299,25 @@ impl App {

if is_added && channel_id != ChannelId::User(self.user_id) {
// Notification
let sender_name = name_by_id(&self.data.names, sender_uuid);
let summary = if let ChannelId::Group(_) = channel.id {
Cow::from(format!("{} in {}", sender_name, channel.name))
} else {
Cow::from(sender_name)
};
let mut notification = format!("{} reacted {}", summary, emoji);
let mut notification = format!("reacted {}", emoji);
if let Some(text) = message.message.as_ref() {
notification.push_str(" to: ");
notification.push_str(text);
}

// makes borrow checker happy
let channel_id = channel.id;
let channel_name = channel.name.clone();

let sender_name = self.name_by_id(sender_uuid);
let summary = if let ChannelId::Group(_) = channel_id {
Cow::from(format!("{} in {}", sender_name, channel_name))
} else {
Cow::from(sender_name)
};

if notify {
self.notify(&summary, &notification);
self.notify(&summary, &format!("{summary} {notification}"));
}

self.touch_channel(channel_idx);
Expand Down Expand Up @@ -1375,19 +1399,15 @@ impl App {
uuid: Uuid,
profile_key: Vec<u8>,
phone_number: PhoneNumber,
) -> &str {
if self
.try_ensure_user_is_known(uuid, profile_key)
.await
.is_none()
{
) {
if !self.try_ensure_user_is_known(uuid, profile_key).await {
let phone_number_name = phone_number.format().mode(Mode::E164).to_string();
self.data.names.insert(uuid, phone_number_name);
}
self.data.names.get(&uuid).unwrap()
}

async fn try_ensure_user_is_known(&mut self, uuid: Uuid, profile_key: Vec<u8>) -> Option<&str> {
/// Returns `true`, if user name was resolved successfully, otherwise `false`
async fn try_ensure_user_is_known(&mut self, uuid: Uuid, profile_key: Vec<u8>) -> bool {
let is_phone_number_or_unknown = self
.data
.names
Expand All @@ -1396,12 +1416,18 @@ impl App {
.unwrap_or(true);
if is_phone_number_or_unknown {
let name = match profile_key.try_into() {
Ok(key) => self.signal_manager.contact_name(uuid, key).await,
Ok(key) => {
self.signal_manager
.resolve_name_from_profile(uuid, key)
.await
}
Err(_) => None,
};
self.data.names.insert(uuid, name?);
if let Some(name) = name {
self.data.names.insert(uuid, name);
}
}
self.data.names.get(&uuid).map(|s| s.as_str())
self.data.names.contains_key(&uuid)
}

async fn try_ensure_users_are_known(
Expand Down Expand Up @@ -1445,11 +1471,9 @@ impl App {
.iter()
.position(|channel| channel.user_id() == Some(uuid))
{
if let Some(name) = self.data.names.get(&uuid) {
let channel = &mut self.data.channels.items[channel_idx];
if &channel.name != name {
channel.name = name.clone();
}
let channel = &mut self.data.channels.items[channel_idx];
if channel.name != name {
channel.name = name.to_string();
}
channel_idx
} else {
Expand Down Expand Up @@ -1580,10 +1604,44 @@ impl App {
pub fn is_help(&self) -> bool {
self.display_help
}
}

pub fn name_by_id(names: &HashMap<Uuid, String>, id: Uuid) -> &str {
names.get(&id).map(|s| s.as_ref()).unwrap_or("Unknown Name")
pub(crate) async fn request_contacts_sync(&mut self) -> anyhow::Result<()> {
let now = Utc::now();
let do_sync = self
.data
.contacts_sync_request_at
.map(|dt| dt + Duration::seconds(CONTACTS_SYNC_DEADLINE_SEC) < now)
.unwrap_or(true);
if do_sync {
info!("requesting contact sync");
self.signal_manager.request_contacts_sync().await?;
self.data.contacts_sync_request_at = Some(now);
self.save().unwrap();
}
Ok(())
}

/// Filters visible channel based on the provided `pattern`
///
/// `pattern` is compared to channel name or channel member contact names, case insensitively.
pub(crate) fn filter_channels(&mut self, pattern: &str) {
let pattern = pattern.to_lowercase();

// move out `channels` temporarily to make borrow checker happy
let mut channels = std::mem::take(&mut self.data.channels);
channels.filter(|channel: &Channel| match pattern.chars().next() {
None => true,
Some('@') => match channel.group_data.as_ref() {
Some(group_data) => group_data
.members
.iter()
.any(|&id| self.name_by_id(id).to_lowercase().contains(&pattern[1..])),
None => channel.name.to_lowercase().contains(&pattern[1..]),
},
_ => channel.name.to_lowercase().contains(&pattern),
});
self.data.channels = channels;
}
}

/// Returns an emoji string if `s` is an emoji or if `s` is a GitHub emoji shortcode.
Expand Down
3 changes: 3 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,16 @@ async fn is_online() -> bool {

async fn run_single_threaded(relink: bool) -> anyhow::Result<()> {
let (signal_manager, config) = signal::ensure_linked_device(relink).await?;

let storage = JsonStorage::new(config.data_path.clone(), config::fallback_data_path());
let mut app = App::try_new(
config,
Box::new(PresageManager::new(signal_manager.clone())),
Box::new(storage),
)?;

app.request_contacts_sync().await?;

enable_raw_mode()?;
let _raw_mode_guard = scopeguard::guard((), |_| {
disable_raw_mode().unwrap();
Expand Down
Loading

0 comments on commit 049ca91

Please sign in to comment.