/*
* Copyright (C) 2021 Nico Hickman
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3.
*
* signal-rs-ut is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#![feature(type_alias_impl_trait)]
#![feature(async_closure)]
#[macro_use]
extern crate qmetaobject;
use cpp::cpp;
use presage::config::*;
use presage::prelude::*;
use presage::*;
use qmetaobject::QAbstractListModel;
use qmetaobject::*;
use std::ffi::CStr;
use std::io::Read;
use tokio::sync::mpsc::*;
#[allow(non_snake_case)]
mod signal;
use signal::*;
use std::collections::*;
mod config;
use config::*;
use notify_rust::Notification;
use std::cell::*;
use std::path::PathBuf;
use std::result::Result;
mod presage_manager;
mod registration;
use presage_manager::*;
#[derive(QObject, Default)]
pub struct SignalUI {
base: qt_base_class!(trait QObject),
state: Option,
name: qt_property!(QString; NOTIFY name_changed),
name_changed: qt_signal!(),
init_state: qt_method!(fn(&mut self)),
show_view: qt_method!(fn(&mut self, view: String)),
signalResponse: qt_signal!(response: String),
chats: qt_property!(RefCell>; NOTIFY chats_changed),
messages: qt_property!(RefCell>; NOTIFY msgs_changed),
current_messages: qt_property!(String),
contacts: qt_property!(RefCell>; NOTIFY contacts_changed),
contacts_changed: qt_signal!(),
msgs_changed: qt_signal!(),
chats_changed: qt_signal!(),
show_chat: qt_method!(fn(&self, tel: String)),
add_contact: qt_method!(fn(&self, name: String, tel: String)),
send_message: qt_method!(fn(&self, to: String, message: String, attachment: String)),
edit_contact: qt_method!(fn(&self, name: String, tel: String, id: String)),
contact: qt_method!(fn(&self, tel: String) -> String),
del_message: qt_method!(fn(&self, chat_id: String, sent_at: i64)),
ready: qt_signal!(),
group: qt_method!(fn(&self, chat_id: String) -> String),
resolve_name: qt_method!(fn(&self, source: String) -> String),
avatar: qt_method!(fn(&self, id: String) -> QString),
set_avatar: qt_method!(fn(&self, id: String, path: String) -> String),
send_group_message: qt_method!(fn(&self, chat_id: String, message: String, attachment: String)),
remove_from_group: qt_method!(fn(&self, group_id: String, phone: String)),
}
impl SignalUI {
fn remove_from_group(&self, group_id: String, phone: String) {
self.send_req(SignalRequest::RemoveFromGroup {
group_id,
contact: phone,
});
}
fn group(&self, chat_id: String) -> String {
serde_json::to_string(&self.state.as_ref().unwrap().group(&chat_id).unwrap()).unwrap()
}
fn send_res(&self, resp: SignalResponse) {
self.state
.as_ref()
.unwrap()
.res_sender
.send(resp)
.expect("Couldn't send SignalResponse");
}
fn send_req(&self, reqp: SignalRequest) {
self.state
.as_ref()
.unwrap()
.req_sender
.send(reqp)
.expect("Couldn't send SignalRequest");
}
fn del_message(&self, chat_id: String, sent_at: i64) {
self.send_res(SignalResponse::RmMessage { chat_id, sent_at });
}
fn avatar(&self, id: String) -> QString {
QString::from(self.state.as_ref().unwrap().avatar(&id).unwrap_or_default())
}
fn set_avatar(&self, id: String, path: String) -> String {
let mut data: Vec = vec![];
std::fs::File::open(path)
.unwrap()
.read_to_end(&mut data)
.unwrap();
self.state.as_ref().unwrap().set_avatar(id, &data)
}
fn contact(&self, tel: String) -> String {
let fetched = self
.state
.as_ref()
.unwrap()
.contact(&tel)
.unwrap_or_default();
serde_json::to_string(&fetched).unwrap()
}
fn update_chat(&mut self, chat_id: String) {
let name = self.resolve_name(chat_id.clone());
if let Some(msgs) = self.state.as_ref().unwrap().messages_id(&chat_id).ok() {
if msgs.len() > 0 {
let latest = msgs.values().max_by_key(|x| x.sent_at).unwrap();
let mut chats = self.chats.borrow_mut();
for i in 0..chats.row_count() as usize {
let mut chat = chats[i].clone();
if chat.tel == chat_id {
chat.name = name.clone();
chat.last = latest.message.clone();
chat.timestamp = latest.sent_at;
chat.messages = vec![latest.clone()];
chats.remove(i);
chats.insert(0, chat);
break;
}
}
}
}
}
fn edit_contact(&self, name: String, tel: String, id: String) {
self.send_req(SignalRequest::EditContact {
name,
phone: tel,
id,
});
}
fn send_message(&self, to: String, message: String, attachment: String) {
let attachment = match attachment.len() {
0 => None,
_ => Some(vec![attachment]),
};
self.send_req(SignalRequest::SendMessage {
to,
message,
attachment,
});
}
fn send_group_message(&self, chat_id: String, message: String, attachment: String) {
let attachment = match attachment.len() {
0 => None,
_ => Some(vec![attachment]),
};
let group = self.state.as_ref().unwrap().group(&chat_id).unwrap();
self.send_req(SignalRequest::SendMessageToGroup {
members: group.members,
message,
attachment,
id: chat_id,
master_key: group.master_key,
});
}
fn add_contact(&self, name: String, tel: String) {
self.send_req(SignalRequest::EditContact {
name,
phone: tel,
id: "".to_string(),
});
}
fn show_chat(&mut self, view: String) {
self.messages.replace(
self.state
.as_ref()
.unwrap()
.messages_id(view.as_str())
.unwrap()
.values()
.cloned()
.rev()
.map(|x| QMLSignalMessage::from_msg(x))
.collect(),
);
self.msgs_changed();
self.current_messages = view.clone();
if let Some(map) = self.state.as_ref().unwrap().messages_id(&view).ok() {
self.state
.as_ref()
.unwrap()
.res_sender
.send(SignalResponse::HistoryMessage {
phone: view,
messages: map.values().cloned().collect(),
})
.expect("Couldn't send HistoryMessage");
}
}
fn init_state(&mut self) {
let config: Config = confy::load("signal-rs").expect("Couldn't handle config file");
let qptr = QPointer::from(&*self);
let cfg = config.clone();
let process_res = queued_callback(move |res: SignalResponse| {
let cfg = cfg.clone();
qptr.as_pinned().map(move |self_| {
let ref mut slf = self_.borrow_mut();
slf.state.as_mut().unwrap().process_response(res.clone());
match res {
SignalResponse::ChatList(ref chats) => {
slf.chats.borrow_mut().reset_data(chats.clone());
slf.chats_changed();
}
SignalResponse::ContactList(_) => {
slf.update_chats();
}
SignalResponse::ShowView(to_show) => {
slf.show_view(to_show);
}
SignalResponse::ModelContactList(contacts) => {
slf.contacts.borrow_mut().reset_data(contacts);
slf.contacts_changed();
}
SignalResponse::AddHist(ref msg, new) => {
if new {
if msg.chat_id == slf.current_messages {
slf.messages
.get_mut()
.insert(0, QMLSignalMessage::from_msg(msg.clone()));
}
let cfg = cfg;
if cfg.notifications == NotificationPolicy::Enabled {
Notification::new()
.summary(&slf.resolve_name(msg.source.clone()))
.body(msg.message.as_str())
.show()
.unwrap();
}
slf.update_chat(msg.chat_id.clone());
}
}
SignalResponse::RmMessage {
ref chat_id,
sent_at,
} => {
if &slf.current_messages == chat_id {
let ref mut msg_model = slf.messages.get_mut();
let count = msg_model.row_count() as usize;
for i in 0..count {
if msg_model[i].sent_at == sent_at {
msg_model.remove(i);
break;
}
}
}
println!("Ge");
}
SignalResponse::Groups(_) => {
let ids = slf
.state
.as_mut()
.unwrap()
.msg_store
.all()
.unwrap()
.into_iter()
.map(|x| {
return x.inner.source;
})
.collect::>();
let mut done: HashMap = HashMap::new();
for id in ids {
if !done.contains_key(id.as_str()) {
done.insert(id.clone(), true);
slf.state.as_mut().unwrap().resolve_name(&id).unwrap();
}
}
println!("Finished initialization");
slf.update_contacts();
slf.show_view("chats".to_string());
slf.ready();
slf.signalResponse(serde_json::to_string(&res).unwrap());
}
_ => {}
};
});
});
let (res_sender, res_receiver) =
glib::MainContext::channel::(glib::PRIORITY_DEFAULT);
let l_res_sender = res_sender.clone();
let (req_sender, req_receiver) = unbounded_channel::();
self.state = Some(Presage::new(
l_res_sender,
config.clone(),
req_sender.clone(),
));
res_receiver.attach(None, move |value| {
process_res(value);
glib::Continue(true)
});
req_sender
.send(SignalRequest::GetChatList)
.expect("Couldn't send GetChatList");
req_sender
.send(SignalRequest::GetGroups)
.expect("Couldn't send GetGroups");
res_sender
.send(SignalResponse::Groups(vec![]))
.expect("Couldn't send Groups");
let cstore_path = PathBuf::from(
config
.clone()
.data_dir
.clone()
.replace("~", home::home_dir().unwrap().to_str().unwrap()),
);
qmetaobject::future::execute_async(async move {
Presage::run(
presage::config::SledConfigStore::new(cstore_path.join(PathBuf::from("cstore")))
.unwrap(),
res_sender,
req_receiver,
)
.await;
});
}
/// Shows the given view in the app_box
pub fn show_view(&mut self, view: String) {
match view.as_ref() {
"chats" => {
self.update_chats();
}
"createChat" => {
self.update_contacts();
}
_ => {}
}
}
pub fn update_chats(&mut self) {
let mut constructed: Vec = vec![];
let mut chats: Vec = self
.state
.as_ref()
.unwrap()
.contacts()
.values()
.enumerate()
.filter_map(|(i, contact)| {
if let Some(msgs) = self.state.as_ref().unwrap().messages_id(&contact.tel).ok() {
if msgs.len() > 0 {
let latest = msgs.values().max_by_key(|x| x.sent_at).unwrap();
constructed.push(contact.tel.clone());
return Some(Chat {
ID: i as i32,
name: contact.name.clone(),
tel: contact.tel.clone(),
is_group: false,
last: latest.message.clone(),
timestamp: latest.sent_at,
messages: vec![latest.clone()],
});
}
}
None
})
.collect();
for (i, (gid, group)) in self
.state
.as_ref()
.unwrap()
.groups()
.unwrap()
.into_iter()
.enumerate()
{
if let Some(msgs) = self.state.as_ref().unwrap().messages_id(&gid).ok() {
if msgs.len() > 0 {
let latest = msgs.values().max_by_key(|x| x.sent_at).unwrap();
constructed.push(gid.clone());
chats.push(Chat {
ID: (chats.len() + i) as i32,
name: group.name,
tel: gid,
is_group: true,
last: latest.message.clone(),
timestamp: latest.sent_at,
messages: vec![latest.clone()],
});
}
}
}
for (i, (id, msgs)) in self
.state
.as_ref()
.unwrap()
.messages()
.unwrap()
.into_iter()
.filter(|(id, _msgs)| !constructed.contains(&id))
.enumerate()
{
if msgs.len() > 0 {
let latest = msgs.values().max_by_key(|x| x.sent_at).unwrap();
if latest.message.as_str() == "🧝\u{200d}♀\u{fe0f}" {
println!("{:?} - {}", latest, id);
}
chats.push(Chat {
ID: (chats.len() + i) as i32,
name: self.resolve_name(id.clone()),
tel: id,
is_group: false,
last: latest.message.clone(),
timestamp: latest.sent_at,
messages: msgs.values().cloned().collect(),
})
}
}
chats.sort_by_key(|c| -1 * c.timestamp);
self.send_res(SignalResponse::ChatList(chats));
}
pub fn resolve_name(&mut self, source: String) -> String {
return self
.state
.as_mut()
.unwrap()
.resolve_name(source.as_str())
.unwrap();
}
pub fn update_contacts(&self) {
let mut contacts: Vec = self
.state
.as_ref()
.unwrap()
.contacts()
.into_iter()
.map(|(_k, v)| v)
.collect();
contacts.sort_by_key(|x| x.name.clone());
self.send_res(SignalResponse::ModelContactList(contacts));
}
}
/// An enum that represents a view that can be displayed.
#[derive(Clone, Debug)]
pub enum View {
Chats,
/// Contains the telephone number of the chat to show
Messages(String),
Password,
CreateChat,
CreateContact,
/// Contains the ID of the contact to edit
EditContact(String),
Settings,
}
mod qrc;
#[tokio::main]
async fn main() -> Result<(), std::boxed::Box> {
/*let conf: Config = confy::load("signal-rs").unwrap();
let data_dir = conf
.data_dir
.replace("~", home::home_dir().unwrap().to_str().unwrap());
let config_store = presage::config::SledConfigStore::new(
PathBuf::from(data_dir).join(PathBuf::from("cstore")),
)
.unwrap();*/
/*let mut manager = Manager::with_config_store(config_store, ProtocolContext::default()).unwrap();
manager
.link_secondary_device(
libsignal_service::configuration::SignalServers::Production,
"pre".to_string(),
)
.await
.unwrap();*/
let mut registered = false;
unsafe {
cpp! { {
#include
#include
}}
cpp! ([] {
QCoreApplication::setApplicationName(QStringLiteral("signal-rs.nicohman"));
QCoreApplication::setOrganizationDomain(QStringLiteral("nicohman.com"));
QCoreApplication::setOrganizationName(QStringLiteral("signal-rs.nicohman"));
});
}
qrc::load();
webengine::initialize();
if registered {
qml_register_type::(
CStr::from_bytes_with_nul(b"SignalUI\0").unwrap(),
0,
1,
CStr::from_bytes_with_nul(b"SignalUI\0").unwrap(),
);
let mut engine = QmlEngine::new();
engine.load_file("qrc:/qml/main.qml".into());
engine.exec();
} else {
qml_register_type::(
CStr::from_bytes_with_nul(b"RegistrationController\0").unwrap(),
0,
1,
CStr::from_bytes_with_nul(b"RegistrationController\0").unwrap(),
);
let mut engine = QmlEngine::new();
engine.load_file("qrc:/qml/Registration.qml".into());
engine.exec();
}
println!("Shutting down");
Ok(())
}