wx-bot-sdk 0.1.3

Standalone Weixin Bot SDK in Rust
Documentation
use std::path::{Path, PathBuf};

use rand::RngCore;
use tokio::fs;

use crate::{
    api::{GetUploadUrlReq, UploadMediaType, WeixinApiOptions, get_upload_url},
    media::get_extension_from_content_type_or_url,
    util::temp_file_name,
};

use super::{aes_ecb::aes_ecb_padded_size, cdn_upload::upload_buffer_to_cdn};

#[derive(Clone, Debug)]
pub struct UploadedFileInfo {
    pub filekey: String,
    pub download_encrypted_query_param: String,
    pub aeskey: String,
    pub file_size: usize,
    pub file_size_ciphertext: usize,
}

async fn upload_media_to_cdn(
    file_path: impl AsRef<Path>,
    to_user_id: &str,
    opts: &WeixinApiOptions,
    cdn_base_url: &str,
    media_type: UploadMediaType,
    label: &str,
) -> crate::Result<UploadedFileInfo> {
    let plaintext = fs::read(file_path.as_ref()).await?;
    let rawsize = plaintext.len();
    let rawfilemd5 = format!("{:x}", md5::compute(&plaintext));
    let filesize = aes_ecb_padded_size(rawsize);
    let mut key_bytes = [0_u8; 16];
    rand::thread_rng().fill_bytes(&mut key_bytes);
    let mut filekey_bytes = [0_u8; 16];
    rand::thread_rng().fill_bytes(&mut filekey_bytes);
    let filekey = hex::encode(filekey_bytes);
    let aeskey = hex::encode(key_bytes);

    let resp = get_upload_url(
        GetUploadUrlReq {
            filekey: Some(filekey.clone()),
            media_type: Some(media_type as i32),
            to_user_id: Some(to_user_id.to_string()),
            rawsize: Some(rawsize),
            rawfilemd5: Some(rawfilemd5),
            filesize: Some(filesize),
            no_need_thumb: Some(true),
            aeskey: Some(aeskey.clone()),
            ..Default::default()
        },
        opts,
    )
    .await?;

    let uploaded = upload_buffer_to_cdn(
        &plaintext,
        resp.upload_full_url.as_deref(),
        resp.upload_param.as_deref(),
        &filekey,
        cdn_base_url,
        &key_bytes,
        label,
    )
    .await?;

    Ok(UploadedFileInfo {
        filekey,
        download_encrypted_query_param: uploaded.download_param,
        aeskey,
        file_size: rawsize,
        file_size_ciphertext: filesize,
    })
}

pub async fn download_remote_image_to_temp(
    url: &str,
    dest_dir: impl AsRef<Path>,
) -> crate::Result<PathBuf> {
    let res = reqwest::get(url).await?;
    let status = res.status();
    let content_type = res
        .headers()
        .get("content-type")
        .and_then(|v| v.to_str().ok())
        .map(ToOwned::to_owned);
    let buf = res.bytes().await?;
    if !status.is_success() {
        return Err(format!("remote media download failed: {status} url={url}").into());
    }
    fs::create_dir_all(dest_dir.as_ref()).await?;
    let ext = get_extension_from_content_type_or_url(content_type.as_deref(), Some(url));
    let path = dest_dir
        .as_ref()
        .join(temp_file_name("weixin-remote", &ext));
    fs::write(&path, buf).await?;
    Ok(path)
}

pub async fn upload_file_to_weixin(
    file_path: impl AsRef<Path>,
    to_user_id: &str,
    opts: &WeixinApiOptions,
    cdn_base_url: &str,
) -> crate::Result<UploadedFileInfo> {
    upload_media_to_cdn(
        file_path,
        to_user_id,
        opts,
        cdn_base_url,
        UploadMediaType::Image,
        "uploadFileToWeixin",
    )
    .await
}

pub async fn upload_video_to_weixin(
    file_path: impl AsRef<Path>,
    to_user_id: &str,
    opts: &WeixinApiOptions,
    cdn_base_url: &str,
) -> crate::Result<UploadedFileInfo> {
    upload_media_to_cdn(
        file_path,
        to_user_id,
        opts,
        cdn_base_url,
        UploadMediaType::Video,
        "uploadVideoToWeixin",
    )
    .await
}

pub async fn upload_file_attachment_to_weixin(
    file_path: impl AsRef<Path>,
    _file_name: &str,
    to_user_id: &str,
    opts: &WeixinApiOptions,
    cdn_base_url: &str,
) -> crate::Result<UploadedFileInfo> {
    upload_media_to_cdn(
        file_path,
        to_user_id,
        opts,
        cdn_base_url,
        UploadMediaType::File,
        "uploadFileAttachmentToWeixin",
    )
    .await
}