diff --git a/migrations/2019-09-14-115634_nicksrc/down.sql b/migrations/2019-09-14-115634_nicksrc/down.sql new file mode 100644 index 0000000..57c8484 --- /dev/null +++ b/migrations/2019-09-14-115634_nicksrc/down.sql @@ -0,0 +1 @@ +ALTER TABLE recipients DROP COLUMN nicksrc; diff --git a/migrations/2019-09-14-115634_nicksrc/up.sql b/migrations/2019-09-14-115634_nicksrc/up.sql new file mode 100644 index 0000000..76837cb --- /dev/null +++ b/migrations/2019-09-14-115634_nicksrc/up.sql @@ -0,0 +1 @@ +ALTER TABLE recipients ADD COLUMN nicksrc INT NOT NULL DEFAULT -1; diff --git a/src/comm.rs b/src/comm.rs index 8a8206e..831148e 100644 --- a/src/comm.rs +++ b/src/comm.rs @@ -54,7 +54,7 @@ pub enum ContactManagerCommand { ProcessMessages, ProcessGroups, UpdateAway(Option), - ChangeNick(String), + ChangeNick(String, i32), SetWhatsapp(bool) } #[derive(Clone)] diff --git a/src/contact.rs b/src/contact.rs index 7b460c6..2fc34a7 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -163,8 +163,8 @@ impl ContactManager { self.presence = msg; self.update_away()?; }, - ChangeNick(nick) => { - self.change_nick(nick)?; + ChangeNick(nick, src) => { + self.change_nick(nick, src)?; }, SetWhatsapp(wam) => { self.wa_mode = wam; @@ -173,9 +173,9 @@ impl ContactManager { } Ok(()) } - fn change_nick(&mut self, nick: String) -> Result<()> { - info!("Contact {} changing nick to {}", self.nick, nick); - self.store.update_recipient_nick(&self.addr, &nick)?; + fn change_nick(&mut self, nick: String, src: i32) -> Result<()> { + debug!("Contact {} changing nick to {}", self.nick, nick); + self.store.update_recipient_nick(&self.addr, &nick, src)?; self.irc.0.send(Command::NICK(nick))?; Ok(()) } @@ -271,8 +271,7 @@ impl ContactManager { pub fn new(recip: Recipient, p: InitParameters) -> impl Future { let store = p.store; let wa_mode = recip.whatsapp; - let addr = match util::un_normalize_address(&recip.phone_number) - .ok_or(format_err!("invalid num {} in db", recip.phone_number)) { + let addr = match recip.get_addr() { Ok(r) => r, Err(e) => return Either::B(futures::future::err(e.into())) }; diff --git a/src/contact_common.rs b/src/contact_common.rs index 1ef6bc0..e7e1c8b 100644 --- a/src/contact_common.rs +++ b/src/contact_common.rs @@ -4,7 +4,7 @@ use huawei_modem::pdu::PduAddress; use crate::models::{Recipient, Message}; use crate::store::Store; use crate::comm::ContactManagerCommand; -use crate::util::{self, Result}; +use crate::util::Result; use futures::sync::mpsc::UnboundedSender; use crate::comm::{WhatsappCommand, ModemCommand, ControlBotCommand}; @@ -19,8 +19,7 @@ pub trait ContactManagerManager { fn forward_cmd(&mut self, _: &PduAddress, _: ContactManagerCommand) -> Result<()>; fn resolve_nick(&self, _: &str) -> Option; fn setup_recipient(&mut self, recip: Recipient) -> Result<()> { - let addr = util::un_normalize_address(&recip.phone_number) - .ok_or(format_err!("invalid num {} in db", recip.phone_number))?; + let addr = recip.get_addr()?; debug!("Setting up recipient for {} (nick {})", addr, recip.nick); if self.has_contact(&addr) { debug!("Not doing anything; contact already exists"); diff --git a/src/contact_factory.rs b/src/contact_factory.rs index 341d600..cfbbd56 100644 --- a/src/contact_factory.rs +++ b/src/contact_factory.rs @@ -9,7 +9,7 @@ use std::collections::{HashMap, HashSet}; use tokio_core::reactor::Handle; use huawei_modem::pdu::PduAddress; use crate::contact::ContactManager; -use crate::util::{self, Result}; +use crate::util::Result; use crate::models::Recipient; use tokio_timer::Interval; use failure::Error; @@ -201,8 +201,7 @@ impl ContactFactory { fn process_messages(&mut self) -> Result<()> { for msg in self.store.get_all_messages()? { if self.messages_processed.insert(msg.id) { - let addr = util::un_normalize_address(&msg.phone_number) - .ok_or(format_err!("invalid address {} in db", msg.phone_number))?; + let addr = msg.get_addr()?; if self.contacts_starting.get(&addr).is_some() { continue; } diff --git a/src/control.rs b/src/control.rs index 43f2d38..a75a218 100644 --- a/src/control.rs +++ b/src/control.rs @@ -61,14 +61,17 @@ impl Future for ControlBot { } } impl ControlCommon for ControlBot { - fn wa_tx(&mut self) -> &mut UnboundedSender { - &mut self.wa_tx + fn cf_send(&mut self, c: ContactFactoryCommand) { + self.cf_tx.unbounded_send(c) + .unwrap() } - fn cf_tx(&mut self) -> &mut UnboundedSender { - &mut self.cf_tx + fn wa_send(&mut self, c: WhatsappCommand) { + self.wa_tx.unbounded_send(c) + .unwrap() } - fn m_tx(&mut self) -> &mut UnboundedSender { - &mut self.m_tx + fn m_send(&mut self, c: ModemCommand) { + self.m_tx.unbounded_send(c) + .unwrap() } fn control_response(&mut self, msg: &str) -> Result<()> { self.irc.0.send_notice(&self.admin, msg)?; diff --git a/src/control_common.rs b/src/control_common.rs index 838756d..722fa07 100644 --- a/src/control_common.rs +++ b/src/control_common.rs @@ -1,17 +1,17 @@ //! Common behaviours for the control bot. -use futures::sync::mpsc::UnboundedSender; use crate::comm::{WhatsappCommand, ContactFactoryCommand, ContactManagerCommand, ModemCommand}; use crate::util::Result; +use crate::models::Recipient; use crate::admin::{InspCommand, AdminCommand, GhostCommand, GroupCommand, ContactCommand}; use crate::admin::ModemCommand as AdminModemCommand; use crate::admin::WhatsappCommand as AdminWhatsappCommand; use crate::models::Message; pub trait ControlCommon { - fn wa_tx(&mut self) -> &mut UnboundedSender; - fn cf_tx(&mut self) -> &mut UnboundedSender; - fn m_tx(&mut self) -> &mut UnboundedSender; + fn wa_send(&mut self, c: WhatsappCommand); + fn cf_send(&mut self, c: ContactFactoryCommand); + fn m_send(&mut self, c: ModemCommand); /// Process the InspIRCd-specific command specified. /// /// Returns `true` if the command was processed, or `false` if it wasn't (i.e. we aren't @@ -46,23 +46,20 @@ pub trait ControlCommon { match gc { // This bit looks silly, because it is (see above) ChangeNick(n) => { - c = Some(ContactManagerCommand::ChangeNick(n)); + c = Some(ContactManagerCommand::ChangeNick(n, Recipient::NICKSRC_USER)); }, SetWhatsapp(n) => { c = Some(ContactManagerCommand::SetWhatsapp(n)); }, PresenceSubscribe => { - self.cf_tx().unbounded_send(ContactFactoryCommand::SubscribePresenceByNick(nick.clone())) - .unwrap(); + self.cf_send(ContactFactoryCommand::SubscribePresenceByNick(nick.clone())); }, Remove => { - self.cf_tx().unbounded_send(ContactFactoryCommand::DropContactByNick(nick.clone())) - .unwrap(); + self.cf_send(ContactFactoryCommand::DropContactByNick(nick.clone())); } } if let Some(c) = c { - self.cf_tx().unbounded_send(ContactFactoryCommand::ForwardCommandByNick(nick, c)) - .unwrap(); + self.cf_send(ContactFactoryCommand::ForwardCommandByNick(nick, c)); } self.control_response("Ghost command executed.")?; }, @@ -75,8 +72,7 @@ pub trait ControlCommon { Reinit => ModemCommand::ForceReinit, TempPath(s) => ModemCommand::UpdatePath(s), }; - self.m_tx().unbounded_send(cts) - .unwrap(); + self.m_send(cts); }, AdminCommand::Whatsapp(wac) => { use self::AdminWhatsappCommand::*; @@ -88,8 +84,7 @@ pub trait ControlCommon { UpdateAll => WhatsappCommand::GroupUpdateAll, PrintAcks => WhatsappCommand::PrintAcks }; - self.wa_tx().unbounded_send(cts) - .unwrap(); + self.wa_send(cts); }, AdminCommand::Group(gc) => { use self::GroupCommand::*; @@ -98,8 +93,7 @@ pub trait ControlCommon { BridgeWhatsapp { jid, chan } => WhatsappCommand::GroupAssociate(jid, chan), Unbridge(ch) => WhatsappCommand::GroupRemove(ch) }; - self.wa_tx().unbounded_send(cts) - .unwrap(); + self.wa_send(cts); }, AdminCommand::Contact(cc) => { use self::ContactCommand::*; @@ -107,8 +101,7 @@ pub trait ControlCommon { NewSms(a) => (a, Message::SOURCE_SMS), NewWhatsapp(a) => (a, Message::SOURCE_WA) }; - self.cf_tx().unbounded_send(ContactFactoryCommand::QueryContact(addr, src)) - .unwrap(); + self.cf_send(ContactFactoryCommand::QueryContact(addr, src)); }, AdminCommand::Insp(ic) => { if !self.process_insp(ic)? { diff --git a/src/insp_s2s.rs b/src/insp_s2s.rs index 999da59..5e34677 100644 --- a/src/insp_s2s.rs +++ b/src/insp_s2s.rs @@ -13,7 +13,7 @@ use huawei_modem::pdu::{PduAddress, DeliverPdu}; use std::collections::{HashSet, HashMap}; use failure::Error; use crate::models::Recipient; -use crate::util::{self, Result}; +use crate::util::Result; use crate::contact_common::ContactManagerManager; use crate::sender_common::Sender; use crate::control_common::ControlCommon; @@ -97,7 +97,14 @@ impl ContactManagerManager for InspLink { fn setup_contact_for(&mut self, recip: Recipient, addr: PduAddress) -> Result<()> { trace!("setting up contact for recip #{}: {}", recip.id, addr); let host = self.host_for_wa(recip.whatsapp); - let user = InspUser::new_from_recipient(addr.clone(), recip.nick, &host); + let nick = match self.check_nick_for_collisions(&recip.nick) { + Some(n) => { + self.store.update_recipient_nick(&addr, &n, Recipient::NICKSRC_COLLISION)?; + n + }, + None => recip.nick + }; + let user = InspUser::new_from_recipient(addr.clone(), nick, &host); let uuid = self.new_user(user)?; self.contacts.insert(addr.clone(), InspContact { uuid: uuid.clone(), @@ -125,7 +132,7 @@ impl ContactManagerManager for InspLink { } fn store(&mut self) -> &mut Store { &mut self.store - } + } fn resolve_nick(&self, nick: &str) -> Option { for (uuid, user) in self.users.iter() { if user.nick == nick { @@ -137,19 +144,36 @@ impl ContactManagerManager for InspLink { None } fn forward_cmd(&mut self, a: &PduAddress, cmd: ContactManagerCommand) -> Result<()> { - if let Some(ct) = self.contacts.get_mut(&a) { + if self.contacts.get(a).is_some() { match cmd { ContactManagerCommand::UpdateAway(text) => { + let ct = self.contacts.get(a).unwrap(); self.outbox.push(Message::new(Some(&ct.uuid), "AWAY", vec![], text.as_ref().map(|x| x as &str))?); }, - ContactManagerCommand::ChangeNick(st) => { + ContactManagerCommand::ChangeNick(st, src) => { + { + // Abort if we're trying to change nick to something + // it already is, because otherwise it'll collide. + let ct = self.contacts.get(a).unwrap(); + let u = self.users.get(&ct.uuid).unwrap(); + if u.nick == st { + warn!("Tried to uselessly change nick for {}!", st); + return Ok(()) + } + } + let nick = match self.check_nick_for_collisions(&st) { + Some(n) => n, + None => st.into() + }; + let ct = self.contacts.get(a).unwrap(); let u = self.users.get_mut(&ct.uuid).unwrap(); - u.nick = st.clone(); + u.nick = nick.clone(); let ts = chrono::Utc::now().timestamp().to_string(); - self.outbox.push(Message::new(Some(&ct.uuid), "NICK", vec![&st, &ts], None)?); - self.store.update_recipient_nick(&a, &st)?; + self.outbox.push(Message::new(Some(&ct.uuid), "NICK", vec![&nick, &ts], None)?); + self.store.update_recipient_nick(&a, &nick, src)?; }, ContactManagerCommand::SetWhatsapp(wam) => { + let mut ct = self.contacts.get_mut(a).unwrap(); ct.wa_mode = wam; self.set_wa_state(&a, wam)?; }, @@ -163,9 +187,18 @@ impl ContactManagerManager for InspLink { } } impl ControlCommon for InspLink { - fn cf_tx(&mut self) -> &mut UnboundedSender { &mut self.cf_tx } - fn wa_tx(&mut self) -> &mut UnboundedSender { &mut self.wa_tx } - fn m_tx(&mut self) -> &mut UnboundedSender { &mut self.m_tx } + fn cf_send(&mut self, c: ContactFactoryCommand) { + self.cf_tx.unbounded_send(c) + .unwrap() + } + fn wa_send(&mut self, c: WhatsappCommand) { + self.wa_tx.unbounded_send(c) + .unwrap() + } + fn m_send(&mut self, c: ModemCommand) { + self.m_tx.unbounded_send(c) + .unwrap() + } fn control_response(&mut self, msg: &str) -> Result<()> { if let Some(admu) = self.admin_uuid() { let line = Message::new(Some(&self.control_uuid), "NOTICE", vec![&admu], Some(msg))?; @@ -308,6 +341,31 @@ impl InspLink { } Ok(()) } + fn nick_exists(&self, nick: &str) -> bool { + for (_, user) in self.users.iter() { + if user.nick == nick { + return true; + } + } + false + } + fn check_nick_for_collisions(&self, nick: &str) -> Option { + if self.nick_exists(nick) { + debug!("Nick {} collides with pre-existing nick; finding a new one", nick); + let mut idx = 0; + let mut newnick = format!("{}{}", nick, idx); + while self.nick_exists(&newnick) { + debug!("New nick {} also collides!", newnick); + idx += 1; + newnick = format!("{}{}", nick, idx); + } + info!("Nick {} collides; using non-colliding nick {} instead", nick, newnick); + return Some(newnick); + } + else { + None + } + } fn remove_user(&mut self, uuid: &str, recreate: bool) -> Result<()> { debug!("Removing user {} with recreate {}", uuid, recreate); let addr = if let Some(pdua) = self.contacts_uuid_pdua.get(uuid) { @@ -482,6 +540,7 @@ impl InspLink { "ENDBURST" => { if self.remote_sid == prefix { debug!("Received end of netburst"); + self.do_quasiburst()?; self.state = LinkState::Linked; self.on_linked()?; } @@ -549,8 +608,7 @@ impl InspLink { for grp in self.store.get_all_groups()? { for part in grp.participants { if let Some(recip) = self.store.get_recipient_by_id_opt(part)? { - let num = util::un_normalize_address(&recip.phone_number) - .ok_or(format_err!("invalid address {} in db", recip.phone_number))?; + let num = recip.get_addr()?; if let Some(ct) = self.contacts.get(&num) { let mode = if grp.admins.contains(&part) { "+o" @@ -680,8 +738,7 @@ impl InspLink { }; for msg in self.store.get_all_messages()? { debug!("Processing message #{}", msg.id); - let addr = util::un_normalize_address(&msg.phone_number) - .ok_or(format_err!("invalid address {} in db", msg.phone_number))?; + let addr = msg.get_addr()?; if !self.has_contact(&addr) { if !self.request_contact(addr.clone(), msg.source)? { continue; @@ -738,10 +795,13 @@ impl InspLink { self.users.insert(uuid.clone(), cb); let line = self.make_uid_line(&uuid)?; self.send(line); + self.send_sid_line("ENDBURST", vec![], None)?; + Ok(()) + } + fn do_quasiburst(&mut self) -> Result<()> { for recip in self.store.get_all_recipients()? { self.setup_recipient(recip)?; } - self.send_sid_line("ENDBURST", vec![], None)?; Ok(()) } } diff --git a/src/insp_user.rs b/src/insp_user.rs index 528a9c3..9eecfeb 100644 --- a/src/insp_user.rs +++ b/src/insp_user.rs @@ -15,6 +15,7 @@ pub struct InspUser { // modes in +abc [args] format pub modes: String, pub gecos: String, + pub ours: bool } impl InspUser { pub fn new_from_uid_line(args: Vec, suffix: Option) -> Result<(String, Self)> { @@ -35,7 +36,8 @@ impl InspUser { ip: args.next().unwrap(), signon_time: args.next().unwrap().parse()?, modes: args.collect::>().join(" "), - gecos: suffix.unwrap() + gecos: suffix.unwrap(), + ours: false }; Ok((uuid, ret)) } @@ -51,7 +53,8 @@ impl InspUser { ip: "0.0.0.0".into(), signon_time: ts, modes: "+i".into(), - gecos: "sms-irc control bot".into() + gecos: "sms-irc control bot".into(), + ours: true } } pub fn new_from_recipient(a: PduAddress, nick: String, hostname: &str) -> Self { @@ -67,7 +70,8 @@ impl InspUser { ip: "0.0.0.0".into(), signon_time: ts, modes: "+i".into(), - gecos: format!("{}", a) + gecos: format!("{}", a), + ours: true } } } diff --git a/src/irc_s2c.rs b/src/irc_s2c.rs index a0ce978..92bad19 100644 --- a/src/irc_s2c.rs +++ b/src/irc_s2c.rs @@ -12,15 +12,17 @@ use futures::{Future, Async, Poll, Stream, Sink, self}; use failure::{Error, format_err}; use std::net::{SocketAddr, ToSocketAddrs}; use std::collections::VecDeque; +use std::collections::HashMap; use huawei_modem::pdu::DeliverPdu; -use crate::util::{Result, self}; +use crate::util::Result; use crate::sender_common::Sender; use crate::irc_s2c_registration::{PendingIrcConnectionWrapper, RegistrationInformation}; use crate::config::IrcServerConfig; use crate::comm::InitParameters; use crate::models::Group; use crate::comm::*; +use crate::control_common::ControlCommon; use crate::store::Store; pub static SERVER_NAME: &str = "sms-irc."; @@ -41,9 +43,11 @@ pub struct IrcConnection { reginfo: RegistrationInformation, outbox: Vec, store: Store, - joined_groups: Vec, + /// map from channel name to group info + joined_groups: HashMap, wa_outbox: VecDeque, m_outbox: VecDeque, + cf_outbox: VecDeque, new: bool } @@ -184,9 +188,10 @@ impl IrcConnection { Self { sock, addr, reginfo, store, outbox: vec![], - joined_groups: vec![], + joined_groups: HashMap::new(), wa_outbox: VecDeque::new(), m_outbox: VecDeque::new(), + cf_outbox: VecDeque::new(), new: true } } @@ -201,7 +206,7 @@ impl IrcConnection { self.reply_s2c("PRIVMSG", vec![], Some(&thing as &str))?; }, CommandResponse(thing) => { - self.reply_s2c("NOTICE", vec![], Some(&thing as &str))?; + self.outbox.push(Message::new(Some("root"), "PRIVMSG", vec!["&smsirc"], Some(&thing))?); }, ProcessGroups => {} } @@ -245,8 +250,7 @@ impl IrcConnection { for msg in self.store.get_all_messages()? { debug!("Processing message #{}", msg.id); - let addr = util::un_normalize_address(&msg.phone_number) - .ok_or(format_err!("invalid address {} in db", msg.phone_number))?; + let addr = msg.get_addr()?; let recip = match self.store.get_recipient_by_addr_opt(&addr)? { Some(r) => r, None => { @@ -289,8 +293,7 @@ impl IrcConnection { self.reply_s2c("353", vec!["@", &grp.channel], Some(&nicks as &str))?; } self.reply_s2c("366", vec![&grp.channel], Some("End of /NAMES list."))?; - self.reply_s2c("324", vec![&grp.channel, "+nt"], None)?; - self.joined_groups.push(grp); + self.joined_groups.insert(grp.channel.clone(), grp); Ok(()) } fn setup_control_channel(&mut self) -> Result<()> { @@ -328,22 +331,32 @@ impl IrcConnection { self.reply_from_user("NICK", vec![&new], None)?; self.reginfo.nick = new; }, - Command::JOIN(chan, _, _) => { - self.reply_s2c("405", vec![&chan], Some("You may not manually /JOIN channels in this alpha version."))?; + Command::JOIN(_, _, _) => { + // Just ignore /JOIN requests at present, since we autojoin. }, Command::PART(chan, _) => { // This is ERR_NOTONCHANNEL, which isn't amazing. - self.reply_s2c("442", vec![&chan], Some("You may not manually /PART channels in this alpha version."))?; + self.reply_s2c("442", vec![&chan], Some("You may not part."))?; + }, + Command::ChannelMODE(target, modes) => { + if modes.len() > 0 { + self.reply_s2c("482", vec![&target], Some("You may not alter channel modes."))?; + } + else { + self.reply_s2c("324", vec![&target, "+nt"], None)?; + } }, Command::PRIVMSG(target, msg) => { - if target.starts_with("#") { + if target == "&smsirc" { + self.process_admin_command(msg)?; + } + else if target.starts_with("#") { // FIXME: check the channel actually exists self.wa_outbox.push_back(WhatsappCommand::SendGroupMessage(target, msg)); } else { if let Some(recip) = self.store.get_recipient_by_nick_opt(&target)? { - let addr = util::un_normalize_address(&recip.phone_number) - .ok_or(format_err!("unnormalizable addr"))?; + let addr = recip.get_addr()?; if recip.whatsapp { self.wa_outbox.push_back(WhatsappCommand::SendDirectMessage(addr, msg)); } @@ -368,6 +381,21 @@ impl IrcConnection { } } +impl ControlCommon for IrcConnection { + fn cf_send(&mut self, c: ContactFactoryCommand) { + self.cf_outbox.push_back(c); + } + fn wa_send(&mut self, c: WhatsappCommand) { + self.wa_outbox.push_back(c); + } + fn m_send(&mut self, c: ModemCommand) { + self.m_outbox.push_back(c); + } + fn control_response(&mut self, msg: &str) -> Result<()> { + self.outbox.push(Message::new(Some("root"), "PRIVMSG", vec!["&smsirc"], Some(msg))?); + Ok(()) + } +} impl Sender for IrcConnection { fn report_error(&mut self, from_nick: &str, err: String) -> Result<()> { self.reply_from_nick(from_nick, "NOTICE", vec![&self.reginfo.nick.clone()], Some(&err as &str))?; diff --git a/src/models.rs b/src/models.rs index df6cbd7..569d7d9 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,6 +1,8 @@ use crate::schema::{recipients, messages, groups, wa_persistence, wa_msgids}; use serde_json::Value; use chrono::NaiveDateTime; +use huawei_modem::pdu::PduAddress; +use crate::util::{self, Result}; #[derive(Queryable)] pub struct Recipient { @@ -10,6 +12,26 @@ pub struct Recipient { pub whatsapp: bool, pub avatar_url: Option, pub notify: Option, + pub nicksrc: i32, +} +impl Recipient { + /// Nick source: migrated from previous sms-irc install + pub const NICKSRC_MIGRATED: i32 = -1; + /// Nick source: autogenerated from phone number + pub const NICKSRC_AUTO: i32 = 0; + /// Nick source: renamed by user + pub const NICKSRC_USER: i32 = 1; + /// Nick source: from WhatsApp contact name + pub const NICKSRC_WA_CONTACT: i32 = 2; + /// Nick source: from WhatsApp notify + pub const NICKSRC_WA_NOTIFY: i32 = 3; + /// Nick source: from a nick collision + pub const NICKSRC_COLLISION: i32 = 4; + pub fn get_addr(&self) -> Result { + let addr = util::un_normalize_address(&self.phone_number) + .ok_or(format_err!("invalid address {} in db", self.phone_number))?; + Ok(addr) + } } #[derive(Insertable)] #[table_name="recipients"] @@ -18,7 +40,8 @@ pub struct NewRecipient<'a> { pub nick: &'a str, pub whatsapp: bool, pub avatar_url: Option<&'a str>, - pub notify: Option<&'a str> + pub notify: Option<&'a str>, + pub nicksrc: i32 } #[derive(Queryable, Debug)] pub struct Message { @@ -34,6 +57,12 @@ pub struct Message { impl Message { pub const SOURCE_SMS: i32 = 0; pub const SOURCE_WA: i32 = 1; + + pub fn get_addr(&self) -> Result { + let addr = util::un_normalize_address(&self.phone_number) + .ok_or(format_err!("invalid address {} in db", self.phone_number))?; + Ok(addr) + } } #[derive(Queryable, Debug)] pub struct Group { diff --git a/src/schema.rs b/src/schema.rs index 6eb1a63..f4f9176 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -30,6 +30,7 @@ table! { whatsapp -> Bool, avatar_url -> Nullable, notify -> Nullable, + nicksrc -> Int4, } } diff --git a/src/store.rs b/src/store.rs index 9007992..b8693af 100644 --- a/src/store.rs +++ b/src/store.rs @@ -12,6 +12,7 @@ use whatsappweb::session::PersistentSession; use whatsappweb::Jid; use crate::util::{self, Result}; use chrono::NaiveDateTime; +use regex::Regex; use crate::models::*; embed_migrations!(); @@ -26,9 +27,31 @@ impl Store { let pool = Pool::builder() .build(manager)?; embedded_migrations::run(&*pool.get()?)?; - Ok(Self { + let mut ret = Self { inner: Arc::new(pool) - }) + }; + let sourceless = ret.get_recipients_with_nicksrc(Recipient::NICKSRC_MIGRATED)?; + if sourceless.len() > 0 { + warn!("Adding nickname sources for migrated recipients"); + // Use heuristics to match nicknames which look like they've been + // automatically generated + lazy_static! { + static ref DEFAULT_RE: Regex = Regex::new(r#"I\d+"#).unwrap(); + } + for r in sourceless { + let addr = r.get_addr()?; + let newsrc = if DEFAULT_RE.is_match(&r.nick) { + Recipient::NICKSRC_AUTO + } + else { + // We can't assume much, so set to NICKSRC_USER so + // nothing overwrites it + Recipient::NICKSRC_USER + }; + ret.update_recipient_nick(&addr, &r.nick, newsrc)?; + } + } + Ok(ret) } pub fn store_sms_message(&mut self, addr: &PduAddress, pdu: &[u8], csms_data: Option) -> Result { use crate::schema::messages; @@ -153,7 +176,8 @@ impl Store { nick, whatsapp: false, avatar_url: None, - notify: None + notify: None, + nicksrc: Recipient::NICKSRC_AUTO }; let conn = self.inner.get()?; @@ -162,7 +186,7 @@ impl Store { .get_result(&*conn)?; Ok(res) } - pub fn store_wa_recipient(&mut self, addr: &PduAddress, nick: &str, notify: Option<&str>) -> Result { + pub fn store_wa_recipient(&mut self, addr: &PduAddress, nick: &str, notify: Option<&str>, nicksrc: i32) -> Result { use crate::schema::recipients; let num = util::normalize_address(addr); @@ -171,7 +195,8 @@ impl Store { nick, whatsapp: true, avatar_url: None, - notify: notify + notify: notify, + nicksrc }; let conn = self.inner.get()?; @@ -191,14 +216,14 @@ impl Store { .execute(&*conn)?; Ok(()) } - pub fn update_recipient_nick(&mut self, addr: &PduAddress, n: &str) -> Result<()> { + pub fn update_recipient_nick(&mut self, addr: &PduAddress, n: &str, src: i32) -> Result<()> { use crate::schema::recipients::dsl::*; let conn = self.inner.get()?; let num = util::normalize_address(addr); ::diesel::update(recipients) .filter(phone_number.eq(num)) - .set(nick.eq(n)) + .set((nick.eq(n), nicksrc.eq(src))) .execute(&*conn)?; Ok(()) } @@ -249,6 +274,15 @@ impl Store { .load(&*conn)?; Ok(res) } + pub fn get_recipients_with_nicksrc(&mut self, ns: i32) -> Result> { + use crate::schema::recipients::dsl::*; + let conn = self.inner.get()?; + + let res = recipients + .filter(nicksrc.eq(ns)) + .load(&*conn)?; + Ok(res) + } pub fn get_all_messages(&mut self) -> Result> { use crate::schema::messages::dsl::*; let conn = self.inner.get()?; diff --git a/src/whatsapp.rs b/src/whatsapp.rs index baf69c6..ac2db15 100644 --- a/src/whatsapp.rs +++ b/src/whatsapp.rs @@ -449,17 +449,24 @@ impl WhatsappManager { } Ok(()) } - fn get_contact_notify_for_jid(&mut self, jid: &Jid) -> Option<&str> { - let mut notify: Option<&str> = None; + fn get_nick_for_jid(&mut self, jid: &Jid) -> Result<(String, i32)> { if let Some(ct) = self.contacts.get(jid) { if let Some(ref name) = ct.name { - notify = Some(name); + let nick = util::string_to_irc_nick(&name); + return Ok((nick, Recipient::NICKSRC_WA_CONTACT)); } else if let Some(ref name) = ct.notify { - notify = Some(name); + let nick = util::string_to_irc_nick(&name); + return Ok((nick, Recipient::NICKSRC_WA_NOTIFY)); } } - notify + let addr = match util::jid_to_address(jid) { + Some(a) => a, + None => { + return Err(format_err!("couldn't translate jid {} to address", jid)); + } + }; + Ok((util::make_nick_for_address(&addr), Recipient::NICKSRC_AUTO)) } fn get_wa_recipient(&mut self, jid: &Jid) -> Result { let addr = match util::jid_to_address(jid) { @@ -472,13 +479,10 @@ impl WhatsappManager { Ok(recip) } else { - let notify = self.get_contact_notify_for_jid(jid).map(|x| x.to_string()); - let nick = match notify { - Some(ref n) => util::string_to_irc_nick(n), - None => util::make_nick_for_address(&addr) - }; - info!("Creating new WA recipient for {} (nick {})", addr, nick); - let ret = self.store.store_wa_recipient(&addr, &nick, notify.as_ref().map(|x| x as &str))?; + let (nick, nicksrc) = self.get_nick_for_jid(jid)?; + info!("Creating new WA recipient for {} (nick {}, src {})", addr, nick, nicksrc); + let notify = self.contacts.get(jid).and_then(|x| x.notify.as_ref().map(|x| x as &str)); + let ret = self.store.store_wa_recipient(&addr, &nick, notify, nicksrc)?; self.cf_tx.unbounded_send(ContactFactoryCommand::SetupContact(addr.clone())) .unwrap(); Ok(ret) @@ -650,25 +654,29 @@ impl WhatsappManager { fn on_contact_change(&mut self, ct: WaContact) -> Result<()> { let jid = ct.jid.clone(); if let Some(addr) = util::jid_to_address(&jid) { - let old_notify = self.get_contact_notify_for_jid(&jid).map(|x| x.to_string()); self.contacts.insert(ct.jid.clone(), ct); let recip = self.get_wa_recipient(&jid)?; - let notify = self.get_contact_notify_for_jid(&jid).map(|x| x.to_string()); - if old_notify != notify { - debug!("Notify changed for recipient {}: it's now {:?}", recip.nick, notify); - self.store.update_recipient_notify(&addr, notify.as_ref().map(|x| x as &str))?; - if self.autoupdate_nicks && old_notify.is_none() { - if let Some(n) = notify { - let nick = util::string_to_irc_nick(&n); - info!("Automatically updating nick for {} to {}", addr, nick); - self.store.update_recipient_nick(&addr, &nick)?; - let cmd = ContactFactoryCommand::ForwardCommand( - addr, - crate::comm::ContactManagerCommand::ChangeNick(nick) - ); - self.cf_tx.unbounded_send(cmd) - .unwrap(); - } + let (new_nick, newsrc) = self.get_nick_for_jid(&jid)?; + let notify = self.contacts.get(&jid).and_then(|x| x.notify.as_ref().map(|x| x as &str)); + if notify.is_some() { + self.store.update_recipient_notify(&addr, notify)?; + } + if recip.nick != new_nick { + debug!("New nick '{}' (src {}) for recipient {} (from '{}', src {})", new_nick, newsrc, addr, recip.nick, recip.nicksrc); + let should_update = match (recip.nicksrc, newsrc) { + (Recipient::NICKSRC_MIGRATED, _) => true, + (Recipient::NICKSRC_AUTO, _) => true, + (Recipient::NICKSRC_WA_NOTIFY, Recipient::NICKSRC_WA_CONTACT) => true, + _ => false + }; + if should_update && self.autoupdate_nicks { + info!("Automatically updating nick for {} to {}", addr, new_nick); + let cmd = ContactFactoryCommand::ForwardCommand( + addr, + crate::comm::ContactManagerCommand::ChangeNick(new_nick, newsrc) + ); + self.cf_tx.unbounded_send(cmd) + .unwrap(); } } }