Sign in Sign up
Branches
master
158 lines (149 loc) · 8.0 KB Raw
//! Send an animated (Lottie) sticker, or a sticker pack, to your own number with a
//! saved session (stop the bridge first: one session, one connection).
//!   sendsticker <session> <file.was> [premium 0|1]
//!   sendsticker <session> --pack <dir>     (dir: manifest.tsv, pack.zip, thumb.png)
//! The .was / pack zip goes up byte for byte; see media::lottie_sticker_message and
//! media::sticker_pack_message for the message layouts.

use std::io::Read;
use std::net::TcpStream;
use std::time::Duration;

use inkwhats::wa::client_payload::{self, AppVersion, UserAgent};
use inkwhats::wa::conn::Connection;
use inkwhats::wa::node::{Content, Node};
use inkwhats::wa::noise::KeyPair;
use inkwhats::wa::session::Session;
use inkwhats::wa::signal::{encrypt_first_message_random, PreKeyBundle};
use inkwhats::wa::{media, requests};

const HOST: &str = "g.whatsapp.net:443";
const VER: (u32, u32, u32, u32) = (2, 26, 35, 71);

fn rand32() -> [u8; 32] {
    let mut b = [0u8; 32];
    std::fs::File::open("/dev/urandom").unwrap().read_exact(&mut b).unwrap();
    b
}
fn cbytes(n: &Node, tag: &str) -> Option<Vec<u8>> {
    match &n.child(tag)?.content { Content::Bytes(b) => Some(b.clone()), _ => None }
}
fn be(b: &[u8]) -> u32 { b.iter().fold(0u32, |a, &x| (a << 8) | x as u32) }
fn strip05(v: Vec<u8>) -> Option<[u8; 32]> {
    if v.len() == 33 && v[0] == 5 { v[1..].try_into().ok() } else { v.try_into().ok() }
}
fn parse_bundle(user: &Node) -> Option<(String, PreKeyBundle)> {
    let jid = user.get("jid")?.to_string();
    let skey = user.child("skey")?;
    let key = user.child("key");
    Some((jid, PreKeyBundle {
        registration_id: be(&cbytes(user, "registration").unwrap_or_default()),
        identity_key: strip05(cbytes(user, "identity")?)?,
        signed_prekey_id: be(&cbytes(skey, "id")?),
        signed_prekey: strip05(cbytes(skey, "value")?)?,
        signed_prekey_sig: cbytes(skey, "signature")?.try_into().ok()?,
        prekey_id: key.as_ref().and_then(|k| cbytes(k, "id")).map(|b| be(&b)),
        prekey: key.as_ref().and_then(|k| cbytes(k, "value")).and_then(strip05),
    }))
}

enum What {
    Sticker { was: Vec<u8>, premium: u32 },
    Pack { info: media::PackInfo, zip: Vec<u8>, thumb: Vec<u8> },
}

/// Uploads whatever `what` needs and returns the finished Message plus its enc mediatype.
fn build(what: &What, host: &str, auth: &str) -> Result<(Vec<u8>, &'static str), String> {
    match what {
        What::Sticker { was, premium } => {
            let enc = media::encrypt_media(was, media::LABEL_IMAGE, rand32());
            let (url, path) = media::upload(host, auth, "sticker", &enc)?;
            Ok((media::lottie_sticker_message(&url, &path, &enc, *premium), "sticker"))
        }
        What::Pack { info, zip, thumb } => {
            let key = rand32();
            let zip_enc = media::encrypt_media(zip, media::LABEL_STICKER_PACK, key);
            let thumb_enc = media::encrypt_media(thumb, media::LABEL_STICKER_PACK_THUMB, key);
            let (_, zip_path) = media::upload(host, auth, "sticker-pack", &zip_enc)?;
            let (_, thumb_path) = media::upload(host, auth, "thumbnail-sticker-pack", &thumb_enc)?;
            Ok((media::sticker_pack_message(info, &zip_enc, &zip_path, &thumb_enc, &thumb_path), "sticker_pack"))
        }
    }
}

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let path = args.get(1).expect("session path");
    let what = match (args.get(2).map(String::as_str), args.get(3)) {
        (Some("--pack"), Some(dir)) => What::Pack {
            info: media::PackInfo::from_manifest(&std::fs::read_to_string(format!("{dir}/manifest.tsv")).expect("manifest.tsv"))
                .expect("manifest"),
            zip: std::fs::read(format!("{dir}/pack.zip")).expect("pack.zip"),
            thumb: std::fs::read(format!("{dir}/thumb.png")).expect("thumb.png"),
        },
        (Some(file), premium) => What::Sticker {
            was: std::fs::read(file).expect("sticker file"),
            premium: premium.and_then(|p| p.parse().ok()).unwrap_or(1),
        },
        _ => panic!("usage: sendsticker <session> <file.was> [premium] | --pack <dir>"),
    };
    let sess = Session::load(path).expect("session");
    let username = sess.username.expect("username");
    let device = sess.device.expect("device");
    let own_jid = format!("{username}@s.whatsapp.net");

    let ua = UserAgent::default_android(
        AppVersion { primary: VER.0, secondary: VER.1, tertiary: VER.2, quaternary: VER.3 },
        (0..16).map(|_| format!("{:02x}", rand32()[0])).collect(),
    );
    let payload = client_payload::login(username, device, &ua, "");
    let stream = TcpStream::connect(HOST).expect("tcp");
    stream.set_read_timeout(Some(Duration::from_secs(25))).unwrap();
    stream.set_write_timeout(Some(Duration::from_secs(20))).unwrap();
    let mut conn = Connection::connect(stream, sess.keys.noise.clone(), KeyPair::from_secret(rand32()), &payload).expect("handshake");
    eprintln!("logged in {username}:{device}");

    let mut message: Option<(Vec<u8>, &'static str)> = None; // padded DSM + mediatype
    let mut step = 0;
    for _ in 0..500 {
        let node = match conn.recv_node() { Ok(n) => n, Err(e) => { eprintln!("recv: {e}"); break; } };
        if node.tag == "receipt" || node.tag == "message" { continue; }
        if node.tag == "success" && step == 0 {
            step = 1;
            let _ = conn.send_node(&requests::media_conn(&conn.next_id()));
            continue;
        }
        if node.tag == "iq" {
            if let Some(mc) = node.child("media_conn") {
                let auth = mc.get("auth").unwrap_or("").to_string();
                let host = mc.nodes().iter().find(|n| n.tag == "host").and_then(|h| h.get("hostname")).unwrap_or("mmg.whatsapp.net").to_string();
                match build(&what, &host, &auth) {
                    Ok((content, mediatype)) => {
                        // Own devices (self-send) get it wrapped in a DeviceSentMessage.
                        let m = media::device_sent_message(&own_jid, &content);
                        message = Some((media::pad_message(m, 3), mediatype));
                        let _ = conn.send_node(&requests::get_prekeys(&conn.next_id(), &[own_jid.clone()]));
                        eprintln!("uploaded; requested prekeys ...");
                    }
                    Err(e) => { eprintln!("{e}"); return; }
                }
                continue;
            }
            if let Some(list) = node.child("list") {
                let Some((msg, mediatype)) = &message else { continue };
                let mut encs = Vec::new();
                for user in list.nodes().iter().filter(|n| n.tag == "user") {
                    if let Some((jid, bundle)) = parse_bundle(user) {
                        if jid.starts_with(&format!("{username}:{device}")) { continue; }
                        if let Ok(pk) = encrypt_first_message_random(&sess.keys.identity, sess.keys.registration_id, &bundle, msg) {
                            encs.push(Node::new("to").attr("jid", &jid).children(vec![
                                Node::new("enc").attr("v", "2").attr("type", "pkmsg").attr("mediatype", *mediatype).bytes(pk)]));
                        }
                    }
                }
                if encs.is_empty() { eprintln!("no targets"); return; }
                let id = format!("INKW{}", rand32()[..8].iter().map(|b| format!("{b:02X}")).collect::<String>());
                let mut kids = vec![Node::new("participants").children(encs)];
                if let Some(adv) = &sess.adv_identity { kids.push(Node::new("device-identity").bytes(adv.clone())); }
                let m = Node::new("message").attr("to", &own_jid).attr("id", &id).attr("type", "media").children(kids);
                conn.send_node(&m).expect("send");
                eprintln!("sent {mediatype} id={id}; waiting for ack ...");
                continue;
            }
            eprintln!("<< iq {:?}", node.attrs);
        }
        if node.tag == "ack" { eprintln!("ack: {:?}", node.attrs); return; }
    }
    eprintln!("(done)");
}