~nicohman/signal-rs

1c0a90d2060474c1cd69264ce6678858fc0dc54c — nicohman 9 months ago 526c539
Add start of SCLI support
M src/axolotl.rs => src/axolotl.rs +2 -2
@@ 15,7 15,7 @@ impl AxolotlSession {
            .await
            .unwrap();
        let mut session = SignalSession::from_socket(socket);
         loop {
        loop {
            tokio::select! {
                next = session.stream.next() => {
                    res_sender.send(next.unwrap()).expect("Couldn't pass on websocket response");


@@ 61,7 61,7 @@ impl AxolotlSession {
    }
}
impl SignalBackend for AxolotlSession {
    fn new(res_sender: glib::Sender<SignalResponse>) -> AxolotlSession {
    fn new(res_sender: glib::Sender<SignalResponse>, _config: Config) -> AxolotlSession {
        AxolotlSession {
            res_sender,
            contacts: HashMap::new(),

M src/config.rs => src/config.rs +17 -11
@@ 1,29 1,35 @@
use serde::*;
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
pub struct Config {
	#[serde(default)]
    #[serde(default)]
    pub backend: Backend,
    pub number: Option<String>,
    #[serde(default)]
    pub theme: Theme,
    pub scli: Option<SignalCLIConfig>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Eq, PartialEq)]
pub enum Backend {
    Axolotl,
    SignalCLI,
}
impl Default for Backend {
	fn default() -> Self {
		Backend::Axolotl
	}
    fn default() -> Self {
        Backend::Axolotl
    }
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Theme {
	Base,
	Signal
    Base,
    Signal,
}
impl Default for Theme {
	fn default() -> Self {
		Theme::Base
	}
}
\ No newline at end of file
    fn default() -> Self {
        Theme::Base
    }
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SignalCLIConfig {
    pub username: String,
    pub data_dir: String,
}

M src/main.rs => src/main.rs +14 -134
@@ 49,6 49,7 @@ use widgets::Settings;
use widgets::*;
mod axolotl;
use axolotl::*;
mod scli;
const BASE_CSS: &'static str = include_str!("base.css");
const SIGNAL_CSS: &'static str = include_str!("signal.css");
const STYLE_CSS: &'static str = include_str!("style.css");


@@ 56,7 57,7 @@ pub trait SignalBackend {
    fn messages<'a>(&'a self) -> &'a HashMap<String, BTreeMap<i64, Message>>;
    fn contacts<'a>(&'a self) -> &'a HashMap<String, Contact>;
    fn process_response(&mut self, response: SignalResponse);
    fn new(res_sender: glib::Sender<SignalResponse>) -> Self;
    fn new(res_sender: glib::Sender<SignalResponse>, config: Config) -> Self;
}
/// The main state manager/UI controller
pub struct SignalState<T: SignalBackend> {


@@ 169,8 170,8 @@ where
            edit_contact,
            user_tel: None,
            backend: config.backend,
            config: Rc::new(RefCell::new(config)),
            sbackend: T::new(res_sender),
            config: Rc::new(RefCell::new(config.clone())),
            sbackend: T::new(res_sender, config),
            settings,
            providers,
        }


@@ 309,6 310,9 @@ where
            SignalResponse::ShowView(to_show) => {
                self.show_view(to_show);
            }
            SignalResponse::ContactList(_) => {
                self.update_chats();
            }
            SignalResponse::Type(typ) => match typ.as_ref() {
                "getEncryptionPw" => {
                    self.show_view(View::Password);


@@ 342,23 346,6 @@ where
            _ => {}
        }
    }
    pub fn fetch_contacts_scli(&mut self) {
        let output = Command::new("signal-cli")
            .arg("--username")
            .arg(self.user_tel.as_ref().unwrap().clone())
            .arg("listContacts")
            .output()
            .expect("Couldn't start listContacts");
        let res = BufRead::lines(output.stdout.as_slice())
            .into_iter()
            .filter_map(|s| SignalCLIContactInfo::from_str(s.unwrap()))
            .map(|ci| Contact {
                tel: ci.number,
                name: ci.name,
            })
            .collect();
        self.process_response(SignalResponse::ContactList(res));
    }
    /// Fetches a chat by tel, if it exists
    pub fn chat_by_tel<'a>(&'a self, tel: String) -> Option<&'a Chat> {
        self.chat_list


@@ 367,85 354,6 @@ where
            .filter(|x| x.tel == tel)
            .next()
    }
    /*pub fn build_chat(&self, tel: impl Into<String>) -> Option<Chat> {
        let tel = tel.into();
        println!("{}", tel);
        if let Some(msgs) = self.sbackend.messages().get(&tel) {
            if let Some(contact) = self.sbackend.contacts().get(&tel) {
                if let Some(latest) = msgs.values().max_by_key(|a| a.msg.sent_at) {
                    Some(Chat {
                        ID: self.chat_list.chats.len() as i32,
                        name: contact.name.clone(),
                        tel,
                        last: latest.msg.message.clone(),
                        timestamp: latest.msg.sent_at,
                        is_group: false,
                        messages: msgs,
                    })
                } else {
                    Some(Chat {
                        ID: self.chat_list.chats.len() as i32,
                        name: contact.name.clone(),
                        tel: tel,
                        last: String::new(),
                        timestamp: 0,
                        is_group: false,
                        messages: msgs.len(),
                    })
                }
            } else {
                println!("c:{:?}", self.sbackend.contacts());
                None
            }
        } else {
            None
        }
    }
    pub fn update_chats(&mut self) {
        for (tel, msgs) in self.sbackend.messages().iter() {
            println!("{}", tel);
            if let Some(chat) = self.build_chat(tel) {
                self.chat_list.add_chat(chat);
            }
        }
        println!("{:?}", self.chat_list.chats);
    }
      pub fn process_scli_msg(&mut self, msg: SignalCLIMessage) {
          if !self.contacts.contains_key(&msg.source) {
              self.req_sender.send(SignalRequest::GetContacts).expect("Couldn't send GetContacts");
          }
          let mut message : Option<String>;
          if let Some(m) = msg.message {
           message = Some(m);
          } else if let Some(data) = msg.data_message {
              message = data.message;
          } else {
              return;
          }
          // TODO: Fix outgoing
          if let Some(v) = self.messages.get(&msg.source) {
              let last_id = v.values().map(|msg| {
                  msg.msg.ID
              }).max().unwrap();
              let made_msg = SignalMessage {
                  ID: last_id,
                  source: msg.source,
                  message: message.unwrap(),
                  outgoing: false,
                  sent_at: msg.timestamp,
              };
              self.add_message(made_msg, true);
          } else {
              let made_msg = SignalMessage {
                  ID: 0,
                  source: msg.source,
                  message: message.unwrap(),
                  outgoing: false,
                  sent_at: msg.timestamp,
              };
              self.add_message(made_msg, true);
          }
      }*/
    pub fn change_theme(&self, theme: Theme) {
        let screen = gdk::Screen::get_default().expect("Error initializing gtk css provider.");
        for prov in &self.providers {


@@ 488,7 396,7 @@ async fn main() -> Result<()> {
        let (req_sender, req_receiver) = unbounded_channel::<SignalRequest>();
        let clconf = config.clone();
        let window = ApplicationWindow::new(app);
        let mut state: SignalState<AxolotlSession> =
        let mut state: SignalState<scli::SCLISession> =
            SignalState::new(req_sender.clone(), l_res_sender.clone(), config.clone());
        StyleContext::add_provider_for_screen(
            &gdk::Screen::get_default().expect("Error initializing gtk css provider."),


@@ 512,9 420,11 @@ async fn main() -> Result<()> {
        req_sender
            .send(SignalRequest::GetChatList)
            .expect("Couldn't send GetChatList");
        req_sender
            .send(SignalRequest::GetContacts)
            .expect("Couldn't send GetContacts");
        if config.backend == Backend::Axolotl {
            req_sender
                .send(SignalRequest::GetContacts)
                .expect("Couldn't send GetContacts");
        }
        tokio::spawn(async move {
            match clconf.backend {
                Backend::Axolotl => {


@@ 522,41 432,11 @@ async fn main() -> Result<()> {
                }
                // Inspired in part by https://github.com/boxdot/gurk-rs
                Backend::SignalCLI => {
                    /*let mut command = tokio::process::Command::new("signal-cli");
                            command.arg("-u").arg().arg("daemon").arg("--json").stdout(std::process::Stdio::piped());//.kill_on_drop(true);
                            let mut child_process = command.spawn().expect("Couldn't spawn signal-cli daemon");
                            let stdout = child_process.stdout.take().expect("Couldn't get stdout");
                             let mut reader = BufReader::new(stdout).lines();
                    let _cmd_handle = tokio::spawn(async { child_process.await });
                    loop {
                        tokio::select! {
                            next = reader.next_line() => {
                                let stri = next.unwrap().unwrap();
                                println!("{}", stri);
                                if let Some(con) = serde_json::from_str::<SignalCLIContainer>(stri.as_str()).ok() {
                                    res_sender.send(SignalResponse::SignalCLIEnvelope(con.envelope)).expect("Couldn't send signalcli message");
                                } else {
                                    println!("{}", stri);
                                    println!("dnw");
                                }
                            },
                            req = req_receiver.recv() => {
                                if let Some(v) = req {
                                    match v {
                                        _ => {
                                            println!("Unsupported request for SignalCLI mode");
                                        }
                                    }
                                }
                            }
                        }
                    }
                    _cmd_handle.await;*/
                    scli::SCLISession::run(res_sender, req_receiver, clconf.scli.unwrap()).await;
                }
            }
        });
        std::thread::sleep_ms(1000);

        window.show_all();
        l_res_sender
            .send(SignalResponse::ShowView(View::Chats))

M src/scli.rs => src/scli.rs +212 -4
@@ 1,10 1,218 @@
use crate::Message;
use crate::*;
use config::*;
use signal::*;
use serde::{Deserialize, Deserializer, Serialize};
use std::io::BufRead;
use std::process::Stdio;
use std::fs;
use std::path::*;
pub struct SCLISession {
	pub contacts: HashMap<String, Contact>,
	pub messages: HashMap<String, BTreeMap<String, Message>>,
    pub contacts: HashMap<String, Contact>,
    pub messages: HashMap<String, BTreeMap<i64, Message>>,
    pub config: SignalCLIConfig,
    pub res_sender: glib::Sender<SignalResponse>,
}
impl SCLISession {
	pub fn messages<'a>(&'a self) -> &'a HashMap<String, BTreeMap<String, Message>> {
		&self.messages
	pub fn save(&self) {
		let datafile = DataFile::from_data(self.contacts.clone(), self.messages.clone());
		fs::write(Path::new(&self.config.data_dir).join("data.json"), serde_json::to_string(&datafile).unwrap().as_bytes()).expect("Couldn't write data");
	}
    pub fn fetch_contacts_scli(username: &str) -> Vec<Contact> {
        let output = Command::new("signal-cli")
            .arg("--username")
            .arg(username)
            .arg("listContacts")
            .output()
            .expect("Couldn't start listContacts");
        let res = BufRead::lines(output.stdout.as_slice())
            .into_iter()
            .filter_map(|s| SignalCLIContactInfo::from_str(s.unwrap()))
            .map(|ci| Contact {
                tel: ci.number,
                name: ci.name,
            })
            .collect();
        res
    }
    pub async fn run(
        res_sender: glib::Sender<SignalResponse>,
        mut req_receiver: UnboundedReceiver<SignalRequest>,
        config: SignalCLIConfig,
    ) {
        let mut cmd = tokio::process::Command::new("signal-cli");
        cmd.arg("-u")
            .arg(&config.username)
            .arg("daemon")
            .arg("--json")
            .stdout(Stdio::piped())
            .kill_on_drop(true);
        let mut child = cmd.spawn().expect("Couldn't run signal-cli");
        let stdout = child.stdout.take().unwrap();
        let mut reader = BufReader::new(stdout).lines();
        let cmd_handle = tokio::spawn(async { child.await });
        loop {
            tokio::select! {
                msg_line = reader.next_line() => {
                    println!("Incoming!");
                    let parsed : SignalCLIContainer= serde_json::from_str(&msg_line.unwrap().unwrap()).unwrap();
                    res_sender.send(SignalResponse::SignalCLIEnvelope(parsed.envelope)).expect("Couldn't send SignalCLIEnvelope");
                }
                inc = req_receiver.recv() => {
                    if let Some(v) = inc {
                        match v {
                            SignalRequest::GetContacts => {
                                println!("Getting contacts");
                                //let contacts = Self::fetch_contacts_scli(config.username.as_str());
                                //res_sender.send(SignalResponse::ContactList(contacts)).expect("Can't send ContactList");
                            }
                            _ => {

                            }
                        }
                    }
                }
            }
        }
    }
    pub fn add_message(&mut self, message: SignalMessage, new: bool) {
        if !self.messages.contains_key(&message.source) {
            self.messages
                .insert(message.source.clone(), BTreeMap::new());
        }
        let msg_p = self.messages.get_mut(&message.source).unwrap();
        if !msg_p.contains_key(&message.sent_at) {
            let msg = Message::new(message.clone());
            msg_p.insert(message.sent_at, msg);
            self.res_sender
                .send(SignalResponse::AddHist(
                    message.source.clone(),
                    message.sent_at,
                    new,
                ))
                .expect("Couldn't send AddHist");
        }
        self.save();
    }

    pub fn process_scli_msg(&mut self, msg: SignalCLIMessage) {
        /*if !self.contacts.contains_key(&msg.source) {
            self.req_sender.send(SignalRequest::GetContacts).expect("Couldn't send GetContacts");
        }*/
        let message: Option<String>;
        if let Some(m) = msg.message {
            println!("text");
            message = Some(m);
        } else if let Some(data) = msg.data_message {
            message = data.message;
        } else {
            println!("Return early");
            return;
        }
        // TODO: Fix outgoing
        if let Some(v) = self.messages.get(&msg.source) {
            let last_id = v.values().map(|msg| msg.msg.ID).max().unwrap();
            let made_msg = SignalMessage {
                ID: last_id,
                source: msg.source,
                message: message.unwrap(),
                outgoing: false,
                sent_at: msg.timestamp,
                attachment: None,
            };
            self.add_message(made_msg, true);
        } else {
            let made_msg = SignalMessage {
                ID: 0,
                source: msg.source,
                message: message.unwrap(),
                outgoing: false,
                sent_at: msg.timestamp,
                attachment: None,
            };
            self.add_message(made_msg, true);
        }
    }	
}
impl SignalBackend for SCLISession {
    fn new(res_sender: glib::Sender<SignalResponse>, config: Config) -> SCLISession {
    	let scli_config = config.scli.expect("No configured SCLI settings");
    	let mut messages = HashMap::new();
        let mut contacts = HashMap::new();
        let mut to_save = false;
    	if fs::metadata(&scli_config.data_dir).is_err() {
    		fs::create_dir_all(&scli_config.data_dir).expect("Couldn't create data directory");
    		let base_contacts = Self::fetch_contacts_scli(&scli_config.username);
    		for contact in base_contacts {
    			contacts.insert(contact.tel.clone(), contact);
    		}
    		to_save = true;
    	} else {
    		let data = DataFile::load_data(Path::new(&scli_config.data_dir).join("data.json").to_str().unwrap()).expect("Couldn't load data file");
    		for contact in data.contacts.values() {
    			contacts.insert(contact.tel.clone(), contact.clone());
    		}
    		messages = data.messages.into_iter().map(|(k, x)| {
    			(k, x.into_iter().map(|(k, msg)| {
    				(k, Message::new(msg))
    			}).collect())
    		}).collect();
    	}
        let session = SCLISession {
            res_sender,
            config: scli_config,
            messages,
            contacts,
        };
        if to_save {
        	session.save();
        }
        session
    }
    fn messages<'a>(&'a self) -> &'a HashMap<String, BTreeMap<i64, Message>> {
        &self.messages
    }
    fn contacts<'a>(&'a self) -> &'a HashMap<String, Contact> {
        &self.contacts
    }
    fn process_response(&mut self, response: SignalResponse) {
        match response {
            SignalResponse::SignalCLIEnvelope(msg) => {
                println!("Processing");
                self.process_scli_msg(msg);
            }
            SignalResponse::ContactList(contacts) => {
                for contact in contacts.into_iter() {
                    if let Some(contact_p) = self.contacts.get_mut(&contact.tel) {
                        *contact_p = contact;
                    } else {
                        self.contacts.insert(contact.tel.clone(), contact.clone());
                    }
                }
                self.save();
            }
            _ => {}
        }
    }
}
#[derive(Serialize, Deserialize)]
pub struct DataFile {
    pub contacts: HashMap<String, Contact>,
    pub messages: HashMap<String, BTreeMap<i64, SignalMessage>>
}
impl DataFile {
	pub fn from_data(contacts: HashMap<String, Contact>, messages: HashMap<String, BTreeMap<i64, Message>>) -> DataFile {
		DataFile {
			contacts,
			messages: messages.into_iter().map(|(k, b)| {
				(k, b.into_iter().map(|(k, v)| {
					(k, v.msg)
				}).collect())
			}).collect()
		}
	}
	pub fn load_data(path: &str) ->  std::result::Result<DataFile, std::boxed::Box<dyn std::error::Error>> {
		let stri = fs::read_to_string(&path)?;
		Ok(serde_json::from_str(&stri)?)
	}
}
\ No newline at end of file

M src/signal.rs => src/signal.rs +9 -5
@@ 1,14 1,14 @@
use gio::subclass::BoxedType;
use crate::Message;
use enum_variant_type::EnumVariantType;
use futures_util::sink::SinkExt;
use futures_util::stream::*;
use futures_util::{future, StreamExt};
use gio::subclass::BoxedType;
use glib_macros::*;
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize};
use serde_repr::*;
use std::convert::TryFrom;
use glib_macros::*;
use tokio_tungstenite::tungstenite::protocol::Message as TMessage;
use tokio_tungstenite::*;
lazy_static! {


@@ 203,9 203,13 @@ fn parse_attachment<'de, D>(d: D) -> Result<Option<Vec<Attachment>>, D::Error>
where
    D: Deserializer<'de>,
{
    let vec: String = Deserialize::deserialize(d)?;
    if let Some(vec) = serde_json::from_str::<Vec<Attachment>>(&vec).ok() {
        Ok(Some(vec))
    let vec: Option<String> = Deserialize::deserialize(d).ok();
    if let Some(vec) = vec {
        if let Some(vec) = serde_json::from_str::<Vec<Attachment>>(&vec).ok() {
            Ok(Some(vec))
        } else {
            Ok(None)
        }
    } else {
        Ok(None)
    }

M src/widgets/chat_list.rs => src/widgets/chat_list.rs +1 -1
@@ 1,7 1,7 @@
use std::time::SystemTime;
use crate::util::parse_time;
use crate::*;
use gdk_pixbuf::*;
use std::time::SystemTime;
/// The list of chats
pub struct ChatList {
    pub listbox: ListBox,

M src/widgets/message.rs => src/widgets/message.rs +1 -2
@@ 3,8 3,8 @@ use crate::*;
use core::fmt::Debug;
use regex::Regex;
use std::fmt;
use std::time::*;
use std::fs;
use std::time::*;
lazy_static! {
    static ref LINK_REG: Regex = Regex::new(
        r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"


@@ 90,7 90,6 @@ impl MessageUi {
                .as_millis()
                - msg.sent_at as u128,
        )));
        //let when_label = Label::new(Some(&msg.sent_at.to_string()));
        add_class(&msg_box, "msg");
        add_class(&when_label, "when-label");
        when_label.set_halign(Align::Start);

M src/widgets/mod.rs => src/widgets/mod.rs +3 -3
@@ 3,16 3,16 @@ mod chat_list;
mod create_chat;
mod create_contact;
mod edit_contact;
mod header;
mod message;
mod password;
mod header;
mod settings;
pub use chat_history::{ChatHistory, Element};
pub use chat_list::ChatList;
pub use create_chat::CreateChat;
pub use create_contact::CreateContact;
pub use edit_contact::EditContact;
pub use header::Header;
pub use message::MessageUi;
pub use password::PasswordDialog;
pub use header::Header;
pub use settings::Settings;
\ No newline at end of file
pub use settings::Settings;

M src/widgets/settings.rs => src/widgets/settings.rs +20 -20
@@ 1,25 1,25 @@
use crate::*;
pub struct Settings {
	pub config: Rc<RefCell<Config>>,
	pub theme_button: CheckButton,
	pub cbox: Box,
    pub config: Rc<RefCell<Config>>,
    pub theme_button: CheckButton,
    pub cbox: Box,
}
impl Settings {
	pub fn new(config: Config) -> Settings {
		let cbox = Box::new(Orientation::Vertical, 6);
		let theme_button = CheckButton::with_label("Signal Theme");
		if config.theme == Theme::Signal {
			theme_button.set_active(true);
		}
		cbox.add(&theme_button);
		Settings {
			config: Rc::new(RefCell::new(config)),
			theme_button,
			cbox
		}
	}
	pub fn connect(&self, state: &SignalState<impl SignalBackend>) {
 		self.theme_button.connect_clicked(clone!(@weak self.config as sconf, @weak state.config as stconf , @strong state.res_sender as res_sender => move |but| {
    pub fn new(config: Config) -> Settings {
        let cbox = Box::new(Orientation::Vertical, 6);
        let theme_button = CheckButton::with_label("Signal Theme");
        if config.theme == Theme::Signal {
            theme_button.set_active(true);
        }
        cbox.add(&theme_button);
        Settings {
            config: Rc::new(RefCell::new(config)),
            theme_button,
            cbox,
        }
    }
    pub fn connect(&self, state: &SignalState<impl SignalBackend>) {
        self.theme_button.connect_clicked(clone!(@weak self.config as sconf, @weak state.config as stconf , @strong state.res_sender as res_sender => move |but| {
 			let change_to = match but.get_active()  {
 				true => Theme::Signal,
 				false => Theme::Base,


@@ 36,5 36,5 @@ impl Settings {
 			res_sender.send(SignalResponse::ShowTheme(change_to)).expect("Couldn't send ShowTheme");
 			confy::store("signal-rs", old).expect("Couldn't store config");
 		}));
	}
}
\ No newline at end of file
    }
}