amagi 0.1.2

Rust SDK, CLI, and Web API service skeleton for multi-platform social web adapters.
Documentation
use crate::error::AppError;
use crate::platforms::internal::random::PseudoRandom;
use crate::platforms::internal::{md5::md5_hex, rc4::rc4_encrypt};
use crate::platforms::xiaohongshu::OrderedJson;

use super::codec::encode_custom_base64;

const B1_SECRET_KEY: &[u8] = b"xhswebmplfbt";
const URL_SAFE_BYTES: &[u8] = b"!*'()~_-";

pub(super) fn build_b1_value(random: &mut PseudoRandom, now_ms: u64) -> Result<String, AppError> {
    let payload = build_b1_payload(random, now_ms)?;
    let payload_json = payload.to_json_string()?;
    let encrypted = rc4_encrypt(B1_SECRET_KEY, payload_json.as_bytes());
    let encoded = custom_url_encode_latin1(&encrypted);
    let byte_array = percent_encoded_bytes(&encoded)?;
    let json = OrderedJson::Array(
        byte_array
            .into_iter()
            .map(|value| OrderedJson::uint(value.into()))
            .collect(),
    )
    .to_json_string()?;

    Ok(encode_custom_base64(json.as_bytes()))
}

fn build_b1_payload(random: &mut PseudoRandom, now_ms: u64) -> Result<OrderedJson, AppError> {
    let mut entropy = [0u8; 32];
    random.fill_bytes(&mut entropy);
    let x52 = md5_hex(&entropy);
    let x36 = (random.next_mod(20) + 1).to_string();

    Ok(OrderedJson::object(vec![
        ("x33", OrderedJson::string("0")),
        ("x34", OrderedJson::string("0")),
        ("x35", OrderedJson::string("0")),
        ("x36", OrderedJson::string(x36)),
        (
            "x37",
            OrderedJson::string("0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|1|0|0|0|0|0"),
        ),
        (
            "x38",
            OrderedJson::string(
                "0|0|1|0|1|0|0|0|0|0|1|0|1|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0",
            ),
        ),
        ("x39", OrderedJson::uint(0)),
        ("x42", OrderedJson::string("3.4.4")),
        ("x43", OrderedJson::string("742cc32c")),
        ("x44", OrderedJson::string(now_ms.to_string())),
        (
            "x45",
            OrderedJson::string("__SEC_CAV__1-1-1-1-1|__SEC_WSA__|"),
        ),
        ("x46", OrderedJson::string("false")),
        ("x48", OrderedJson::string("")),
        ("x49", OrderedJson::string("{list:[],type:}")),
        ("x50", OrderedJson::string("")),
        ("x51", OrderedJson::string("")),
        ("x52", OrderedJson::string(x52)),
        ("x82", OrderedJson::string("_0x17a2|_0x1954")),
    ]))
}

fn custom_url_encode_latin1(bytes: &[u8]) -> String {
    let mut output = String::new();

    for &byte in bytes {
        if byte.is_ascii_alphanumeric() || URL_SAFE_BYTES.contains(&byte) {
            output.push(byte as char);
        } else {
            output.push('%');
            output.push_str(&format!("{byte:02X}"));
        }
    }

    output
}

fn percent_encoded_bytes(value: &str) -> Result<Vec<u8>, AppError> {
    let mut bytes = Vec::new();

    for chunk in value.split('%').skip(1) {
        if chunk.len() < 2 {
            return Err(AppError::InvalidRequestConfig(format!(
                "invalid xiaohongshu b1 percent chunk `{chunk}`"
            )));
        }

        let byte = u8::from_str_radix(&chunk[..2], 16).map_err(|error| {
            AppError::InvalidRequestConfig(format!(
                "invalid xiaohongshu b1 percent byte `{}`: {error}",
                &chunk[..2]
            ))
        })?;
        bytes.push(byte);
        bytes.extend_from_slice(&chunk.as_bytes()[2..]);
    }

    Ok(bytes)
}