You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
216 lines
8.8 KiB
216 lines
8.8 KiB
//! WhatsApp message processing tools. |
|
|
|
use whatsappweb::{Jid, MediaType}; |
|
use chrono::prelude::*; |
|
use futures::sync::mpsc::UnboundedSender; |
|
use whatsappweb::message::{ChatMessageContent, QuotedChatMessage, MessageId, Peer, MessageStubType}; |
|
use regex::{Regex, Captures}; |
|
use huawei_modem::pdu::PduAddress; |
|
use std::sync::Arc; |
|
use unicode_segmentation::UnicodeSegmentation; |
|
|
|
use crate::comm::WhatsappCommand; |
|
use crate::store::Store; |
|
use crate::whatsapp_media::{MediaInfo, self}; |
|
use crate::util::Result; |
|
|
|
pub struct IncomingMessage { |
|
pub id: MessageId, |
|
pub peer: Option<Peer>, |
|
pub from: Jid, |
|
pub group: Option<i32>, |
|
pub content: ChatMessageContent, |
|
pub quoted: Option<QuotedChatMessage>, |
|
pub stub_type: Option<MessageStubType>, |
|
pub ts: NaiveDateTime |
|
} |
|
pub struct ProcessedIncomingMessage { |
|
pub from: Jid, |
|
pub text: String, |
|
pub group: Option<i32>, |
|
pub ts: NaiveDateTime |
|
} |
|
pub struct WaMessageProcessor { |
|
pub(crate) store: Store, |
|
pub(crate) media_path: String, |
|
pub(crate) dl_path: String, |
|
pub(crate) wa_tx: Arc<UnboundedSender<WhatsappCommand>> |
|
} |
|
|
|
impl WaMessageProcessor { |
|
fn process_incoming_media(&mut self, id: MessageId, peer: Option<Peer>, from: Jid, group: Option<i32>, ct: ChatMessageContent, ts: NaiveDateTime) -> Result<()> { |
|
|
|
let (ty, fi, name) = match ct { |
|
ChatMessageContent::Image { info, .. } => (MediaType::Image, info, None), |
|
ChatMessageContent::Video { info, .. } => (MediaType::Video, info, None), |
|
ChatMessageContent::Audio { info, .. } => (MediaType::Audio, info, None), |
|
ChatMessageContent::Document { info, filename } => (MediaType::Document, info, Some(filename)), |
|
_ => unreachable!() |
|
}; |
|
let mi = MediaInfo { |
|
ty, fi, name, peer, ts, |
|
mi: id, |
|
from, group, |
|
path: self.media_path.clone(), |
|
dl_path: self.dl_path.clone(), |
|
tx: self.wa_tx.clone() |
|
}; |
|
mi.start(); |
|
Ok(()) |
|
} |
|
fn process_wa_text_message<'a>(&mut self, msg: &'a str) -> String { |
|
lazy_static! { |
|
static ref BOLD_RE: Regex = Regex::new(r#"\*([^\*]+)\*"#).unwrap(); |
|
static ref ITALICS_RE: Regex = Regex::new(r#"_([^_]+)_"#).unwrap(); |
|
static ref MENTIONS_RE: Regex = Regex::new(r#"@(\d+)"#).unwrap(); |
|
} |
|
let emboldened = BOLD_RE.replace_all(msg, "\x02$1\x02"); |
|
let italicised = ITALICS_RE.replace_all(&emboldened, "\x1D$1\x1D"); |
|
let store = &mut self.store; |
|
let ret = MENTIONS_RE.replace_all(&italicised, |caps: &Captures| { |
|
let pdua: PduAddress = caps[0].replace("@", "+").parse().unwrap(); |
|
match store.get_recipient_by_addr_opt(&pdua) { |
|
Ok(Some(recip)) => recip.nick, |
|
Ok(None) => format!("<+{}>", &caps[1]), |
|
Err(e) => { |
|
warn!("Error searching for mention recipient: {}", e); |
|
format!("@{}", &caps[1]) |
|
} |
|
} |
|
}); |
|
ret.to_string() |
|
} |
|
fn jid_to_nick(&mut self, jid: &Jid) -> Result<Option<String>> { |
|
if let Some(num) = jid.phonenumber() { |
|
if let Ok(pdua) = num.parse() { |
|
return Ok(self.store.get_recipient_by_addr_opt(&pdua)? |
|
.map(|x| x.nick)); |
|
} |
|
} |
|
Ok(None) |
|
} |
|
fn stub_to_text(st: MessageStubType) -> Option<&'static str> { |
|
use self::MessageStubType::*; |
|
let ret = match st { |
|
CALL_MISSED_VOICE => "\x01ACTION tried to voice call you\x01", |
|
CALL_MISSED_VIDEO => "\x01ACTION tried to video call you\x01", |
|
CALL_MISSED_GROUP_VOICE => "\x01ACTION tried to group voice call\x01", |
|
CALL_MISSED_GROUP_VIDEO => "\x01ACTION tried to group video call\x01", |
|
// FIXME: this probably needs Special Handling (how do we get the number?!). |
|
INDIVIDUAL_CHANGE_NUMBER => "\x01ACTION changed their phone number (to something?)\x01", |
|
GROUP_PARTICIPANT_CHANGE_NUMBER => "\x01ACTION changed their phone number (to something?)\x01", |
|
// CIPHERTEXT stubs should never ever ever return any message text, |
|
// because they'll be replaced with the real message text once it arrives! |
|
CIPHERTEXT => return None, |
|
_ => return None |
|
}; |
|
Some(ret) |
|
} |
|
pub fn process_wa_incoming(&mut self, inc: IncomingMessage) -> Result<(Vec<ProcessedIncomingMessage>, bool)> { |
|
let IncomingMessage { id, peer, from, group, content, ts, quoted, stub_type } = inc; |
|
let mut ret = Vec::with_capacity(2); |
|
let mut is_media = false; |
|
let text = match content { |
|
ChatMessageContent::Text(s) => self.process_wa_text_message(&s), |
|
ChatMessageContent::Unimplemented(mut det) => { |
|
if det.trim() == "" { |
|
if let Some(st) = stub_type.and_then(|x| Self::stub_to_text(x)) { |
|
st.to_owned() |
|
} |
|
else { |
|
debug!("Discarding empty unimplemented message."); |
|
return Ok((ret, is_media)); |
|
} |
|
} |
|
else { |
|
if det.len() > 128 { |
|
det = det.graphemes(true) |
|
.take(128) |
|
.chain(std::iter::once("…")) |
|
.collect(); |
|
} |
|
format!("[\x02\x0304unimplemented\x0f] {}", det) |
|
} |
|
}, |
|
ChatMessageContent::LiveLocation { lat, long, speed, .. } => { |
|
// FIXME: use write!() maybe |
|
let spd = if let Some(s) = speed { |
|
format!("travelling at {:.02} m/s - https://google.com/maps?q={},{}", s, lat, long) |
|
} |
|
else { |
|
format!("broadcasting live location - https://google.com/maps?q={},{}", lat, long) |
|
}; |
|
format!("\x01ACTION is {}\x01", spd) |
|
}, |
|
ChatMessageContent::Location { lat, long, name, .. } => { |
|
let place = if let Some(n) = name { |
|
format!("at '{}'", n) |
|
} |
|
else { |
|
"somewhere".into() |
|
}; |
|
format!("\x01ACTION is {} - https://google.com/maps?q={},{}\x01", place, lat, long) |
|
}, |
|
ChatMessageContent::Redaction { mid } => { |
|
// TODO: make this more useful |
|
format!("\x01ACTION redacted message ID \x11{}\x11\x01", mid.0) |
|
}, |
|
ChatMessageContent::Contact { display_name, vcard } => { |
|
match whatsapp_media::store_contact(&self.media_path, &self.dl_path, vcard) { |
|
Ok(link) => { |
|
format!("\x01ACTION uploaded a contact for '{}' - {}\x01", display_name, link) |
|
}, |
|
Err(e) => { |
|
warn!("Failed to save contact card: {}", e); |
|
format!("\x01ACTION uploaded a contact for '{}' (couldn't download)\x01", display_name) |
|
} |
|
} |
|
}, |
|
mut x @ ChatMessageContent::Image { .. } | |
|
mut x @ ChatMessageContent::Video { .. } | |
|
mut x @ ChatMessageContent::Audio { .. } | |
|
mut x @ ChatMessageContent::Document { .. } => { |
|
let capt = x.take_caption(); |
|
self.process_incoming_media(id.clone(), peer.clone(), from.clone(), group, x, ts)?; |
|
is_media = true; |
|
if let Some(c) = capt { |
|
c |
|
} |
|
else { |
|
return Ok((ret, is_media)); |
|
} |
|
} |
|
}; |
|
if let Some(qm) = quoted { |
|
let nick = if group.is_some() { |
|
let nick = self.jid_to_nick(&qm.participant)? |
|
.unwrap_or(qm.participant.to_string()); |
|
format!("<{}> ", nick) |
|
} |
|
else { |
|
String::new() |
|
}; |
|
let mut message = qm.content.quoted_description(); |
|
if message.len() > 128 { |
|
message = message.graphemes(true) |
|
.take(128) |
|
.chain(std::iter::once("…")) |
|
.collect(); |
|
} |
|
let quote = format!("\x0315> \x1d{}{}", nick, message); |
|
ret.push(ProcessedIncomingMessage { |
|
from: from.clone(), |
|
text: quote, |
|
group, |
|
ts |
|
}); |
|
} |
|
ret.push(ProcessedIncomingMessage { |
|
from, |
|
text, |
|
group, |
|
ts |
|
}); |
|
Ok((ret, is_media)) |
|
} |
|
}
|
|
|