Skip to content

Commit

Permalink
feat: sync contacts and groups from signal manager (#227)
Browse files Browse the repository at this point in the history
Gurk copies new and changed contacts and groups from presage's signal
manager on startup. Since presage syncs the contacts when linking a new
secondary device, gurk does not start empty anymore on the first run.

Currently, presage/libsignal-service does not have an implementation for
group syncing, therefore groups are not synced.
  • Loading branch information
boxdot authored Jul 28, 2023
1 parent 258f958 commit b1ddf7b
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 26 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
### Added

- Copy selected message to clipboard ([#210])
- Implement storing and rendering of mentions ([#215, #136])
- Implement storing and rendering of mentions ([#215], [#136])
- Sync contacts and groups from signal manager ([#226], [#227])

### Changed

Expand All @@ -22,6 +23,8 @@
[#136]: https://github.com/boxdot/gurk-rs/pull/136
[#215]: https://github.com/boxdot/gurk-rs/pull/215
[#216]: https://github.com/boxdot/gurk-rs/pull/216
[#226]: https://github.com/boxdot/gurk-rs/pull/226
[#227]: https://github.com/boxdot/gurk-rs/pull/227

## 0.3.0

Expand Down
36 changes: 15 additions & 21 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,15 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use gurk::app::App;
use gurk::storage::{sync_from_signal, JsonStorage, MemCache, SqliteStorage, Storage};
use gurk::{config, signal, ui};
use presage::prelude::Content;
use tokio::select;
use tokio_stream::StreamExt;
use tracing::{error, info, metadata::LevelFilter};
use tui::{backend::CrosstermBackend, Terminal};

use gurk::app::App;
use gurk::storage::{JsonStorage, MemCache, SqliteStorage, Storage};

const TARGET_FPS: u64 = 144;
const RECEIPT_TICK_PERIOD: u64 = 144;
const FRAME_BUDGET: Duration = Duration::from_millis(1000 / TARGET_FPS);
Expand All @@ -40,11 +39,6 @@ struct Args {
/// Relinks the device (helpful when device was unlinked)
#[clap(long)]
relink: bool,
/// Dump raw messages to `messages.json` in the current working directory
///
/// Used for collecting benchmark data
#[clap(long)]
dump_messages: bool,
}

#[tokio::main(flavor = "current_thread")]
Expand Down Expand Up @@ -100,7 +94,7 @@ pub enum Event {
async fn run_single_threaded(relink: bool) -> anyhow::Result<()> {
let (mut signal_manager, config) = signal::ensure_linked_device(relink).await?;

let storage: Box<dyn Storage> = if config.sqlite.enabled {
let mut storage: Box<dyn Storage> = if config.sqlite.enabled {
let mut sqlite_storage = SqliteStorage::open(&config.sqlite.url).with_context(|| {
format!(
"failed to open sqlite data storage at: {}",
Expand Down Expand Up @@ -130,20 +124,14 @@ async fn run_single_threaded(relink: bool) -> anyhow::Result<()> {
Box::new(json_storage)
};

sync_from_signal(&*signal_manager, &mut *storage);

let (mut app, mut app_events) = App::try_new(config, signal_manager.clone_boxed(), storage)?;

// sync task can be only spawned after we start to listen to message, because it relies on
// message sender to be running
let mut contact_sync_task = app.request_contacts_sync();

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

let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;

let (tx, mut rx) = tokio::sync::mpsc::channel::<Event>(100);
tokio::spawn({
let tx = tx.clone();
Expand All @@ -163,10 +151,6 @@ async fn run_single_threaded(relink: bool) -> anyhow::Result<()> {
}
});

let backend = CrosstermBackend::new(stdout);

let mut terminal = Terminal::new(backend)?;

let inner_tx = tx.clone();
tokio::task::spawn_local(async move {
loop {
Expand Down Expand Up @@ -218,6 +202,16 @@ async fn run_single_threaded(relink: bool) -> anyhow::Result<()> {
}
});

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

let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;

let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;

let mut res = Ok(()); // result on quit
Expand Down
10 changes: 9 additions & 1 deletion src/signal/impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use anyhow::{anyhow, Context};
use async_trait::async_trait;
use chrono::Utc;
use gh_emoji::Replacer;
use presage::libsignal_service::prelude::ProfileKey;
use presage::libsignal_service::prelude::{Group, ProfileKey};
use presage::prelude::content::Reaction;
use presage::prelude::proto::data_message::Quote;
use presage::prelude::proto::{AttachmentPointer, ReceiptMessage};
Expand Down Expand Up @@ -323,6 +323,14 @@ impl SignalManager for PresageManager {
async fn receive_messages(&mut self) -> anyhow::Result<Pin<Box<dyn Stream<Item = Content>>>> {
Ok(Box::pin(self.manager.receive_messages().await?))
}

fn contacts(&self) -> Box<dyn Iterator<Item = Contact>> {
Box::new(self.manager.contacts().into_iter().flatten().flatten())
}

fn groups(&self) -> Box<dyn Iterator<Item = (GroupMasterKeyBytes, Group)>> {
Box::new(self.manager.groups().into_iter().flatten().flatten())
}
}

async fn upload_attachments(
Expand Down
4 changes: 4 additions & 0 deletions src/signal/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::path::PathBuf;
use std::pin::Pin;

use async_trait::async_trait;
use presage::libsignal_service::prelude::Group;
use presage::prelude::proto::AttachmentPointer;
use presage::prelude::{AttachmentSpec, Contact, Content};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -62,6 +63,9 @@ pub trait SignalManager {
fn contact_by_id(&self, id: Uuid) -> anyhow::Result<Option<Contact>>;

async fn receive_messages(&mut self) -> anyhow::Result<Pin<Box<dyn Stream<Item = Content>>>>;

fn contacts(&self) -> Box<dyn Iterator<Item = Contact>>;
fn groups(&self) -> Box<dyn Iterator<Item = (GroupMasterKeyBytes, Group)>>;
}

pub struct ResolvedGroup {
Expand Down
12 changes: 10 additions & 2 deletions src/signal/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{cell::RefCell, rc::Rc};

use async_trait::async_trait;
use gh_emoji::Replacer;
use presage::libsignal_service::prelude::AttachmentIdentifier;
use presage::libsignal_service::prelude::{AttachmentIdentifier, Group};
use presage::prelude::proto::data_message::Quote;
use presage::prelude::proto::AttachmentPointer;
use presage::prelude::{AttachmentSpec, Contact, Content};
Expand All @@ -15,7 +15,7 @@ use crate::data::{Channel, GroupData, Message};
use crate::receipt::Receipt;
use crate::util::utc_now_timestamp_msec;

use super::{Attachment, ProfileKeyBytes, ResolvedGroup, SignalManager};
use super::{Attachment, GroupMasterKeyBytes, ProfileKeyBytes, ResolvedGroup, SignalManager};

/// Signal manager mock which does not send any messages.
pub struct SignalManagerMock {
Expand Down Expand Up @@ -142,4 +142,12 @@ impl SignalManager for SignalManagerMock {
sent_messages: self.sent_messages.clone(),
})
}

fn contacts(&self) -> Box<dyn Iterator<Item = Contact>> {
Box::new(std::iter::empty())
}

fn groups(&self) -> Box<dyn Iterator<Item = (GroupMasterKeyBytes, Group)>> {
Box::new(std::iter::empty())
}
}
96 changes: 96 additions & 0 deletions src/storage/copy.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
use tracing::{debug, error};

use crate::data::{Channel, ChannelId, GroupData, TypingSet};
use crate::signal::SignalManager;

use super::Storage;

#[derive(Debug, Default)]
Expand Down Expand Up @@ -29,3 +34,94 @@ pub fn copy(from: &dyn Storage, to: &mut dyn Storage) -> Stats {

stats
}

/// Copies contacts and groups from the signal manager into the storages
///
/// If contact/group is not in the storage, a new one is created. Group channels are updated,
/// existing contacts are skipped. Contacts with empty name are also skipped.
///
/// Note: At the moment, there is no group sync implemented in presage, so only contacts are
/// synced fully.
pub fn sync_from_signal(manager: &dyn SignalManager, storage: &mut dyn Storage) {
for contact in manager.contacts() {
if contact.name.is_empty() {
// not sure what to do with contacts without a name
continue;
}
let channel_id = contact.uuid.into();
if storage.channel(channel_id).is_none() {
debug!(
name =% contact.name,
"storing new contact from signal manager"
);
storage.store_channel(Channel {
id: channel_id,
name: contact.name.trim().to_owned(),
group_data: None,
unread_messages: 0,
typing: TypingSet::new(false),
});
}
}

for (master_key_bytes, group) in manager.groups() {
let channel_id = match ChannelId::from_master_key_bytes(master_key_bytes) {
Ok(channel_id) => channel_id,
Err(error) => {
error!(%error, "failed to derive group id from master key bytes");
continue;
}
};
let new_group_data = || GroupData {
master_key_bytes,
members: group.members.iter().map(|member| member.uuid).collect(),
revision: group.revision,
};
match storage.channel(channel_id) {
Some(mut channel) => {
let mut is_changed = false;
if channel.name != group.title {
channel.to_mut().name = group.title;
is_changed = true;
}
if channel.group_data.as_ref().map(|d| d.revision) != Some(group.revision) {
let group_data = channel
.to_mut()
.group_data
.get_or_insert_with(new_group_data);
group_data.revision = group.revision;
is_changed = true;
}
if channel
.group_data
.as_ref()
.map(|d| d.members.iter())
.into_iter()
.flatten()
.ne(group.members.iter().map(|m| &m.uuid))
{
let group_data = channel
.to_mut()
.group_data
.get_or_insert_with(new_group_data);
group_data.members = group.members.iter().map(|m| m.uuid).collect();
is_changed = true;
}
if is_changed {
debug!(?channel_id, "storing modified channel from signal manager");
storage.store_channel(channel.into_owned());
}
}
None => {
debug!(?channel_id, "storing new channel from signal manager");
storage.store_channel(Channel {
id: channel_id,
name: group.title,
group_data: Some(new_group_data()),
unread_messages: 0,
typing: TypingSet::new(true),
});
}
}
}
}
2 changes: 1 addition & 1 deletion src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use uuid::Uuid;

use crate::data::{Channel, ChannelId, Message};

pub use copy::copy;
pub use copy::{copy, sync_from_signal};
pub use forgetful::ForgetfulStorage;
pub use json::JsonStorage;
pub use memcache::MemCache;
Expand Down

0 comments on commit b1ddf7b

Please sign in to comment.