wx-bot-sdk 0.1.3

Standalone Weixin Bot SDK in Rust
Documentation
use std::{future::Future, pin::Pin, sync::Arc};

use crate::{
    api::{MessageItem, MessageItemType, WeixinApiOptions, WeixinMessage},
    media::download_media_from_item_to_file,
    util::temp_file_name,
};

use super::send::{
    WeixinInboundMediaOpts, WeixinMsgContext, is_media_item, send_message_weixin,
    set_context_token, weixin_message_to_msg_context,
};

pub type MessageHandler = Arc<
    dyn Fn(WeixinMsgContext) -> Pin<Box<dyn Future<Output = crate::Result<Option<String>>> + Send>>
        + Send
        + Sync,
>;

#[derive(Clone)]
pub struct ProcessMessageDeps {
    pub account_id: String,
    pub base_url: String,
    pub cdn_base_url: String,
    pub token: Option<String>,
    pub on_message: MessageHandler,
}

fn extract_text_body(items: Option<&[MessageItem]>) -> String {
    items
        .and_then(|items| {
            items.iter().find_map(|i| {
                (i.item_type == Some(MessageItemType::Text as i32))
                    .then(|| i.text_item.as_ref()?.text.clone())
                    .flatten()
            })
        })
        .unwrap_or_default()
}

fn has_media(item: &MessageItem) -> bool {
    let has = |m: Option<&crate::api::CdnMedia>| {
        m.map(|m| m.encrypt_query_param.is_some() || m.full_url.is_some())
            .unwrap_or(false)
    };
    match item.item_type {
        Some(x) if x == MessageItemType::Image as i32 => {
            has(item.image_item.as_ref().and_then(|i| i.media.as_ref()))
        }
        Some(x) if x == MessageItemType::Video as i32 => {
            has(item.video_item.as_ref().and_then(|i| i.media.as_ref()))
        }
        Some(x) if x == MessageItemType::File as i32 => {
            has(item.file_item.as_ref().and_then(|i| i.media.as_ref()))
        }
        Some(x) if x == MessageItemType::Voice as i32 => item
            .voice_item
            .as_ref()
            .and_then(|v| v.media.as_ref())
            .map(|m| {
                (m.encrypt_query_param.is_some() || m.full_url.is_some())
                    && item
                        .voice_item
                        .as_ref()
                        .and_then(|v| v.text.as_ref())
                        .is_none()
            })
            .unwrap_or(false),
        _ => false,
    }
}

pub async fn process_one_message(
    full: WeixinMessage,
    deps: &ProcessMessageDeps,
) -> crate::Result<()> {
    let text_body = extract_text_body(full.item_list.as_deref());
    crate::util::logger()
        .with_account(&deps.account_id)
        .info(format!(
            "inbound: from={:?} body=\"{}\" items={}",
            full.from_user_id,
            text_body.chars().take(50).collect::<String>(),
            full.item_list.as_ref().map(|v| v.len()).unwrap_or(0)
        ));

    let mut media_opts = WeixinInboundMediaOpts::default();
    let media_item = full.item_list.as_deref().and_then(|items| {
        items
            .iter()
            .find(|i| i.item_type == Some(MessageItemType::Image as i32) && has_media(i))
            .or_else(|| {
                items
                    .iter()
                    .find(|i| i.item_type == Some(MessageItemType::Video as i32) && has_media(i))
            })
            .or_else(|| {
                items
                    .iter()
                    .find(|i| i.item_type == Some(MessageItemType::File as i32) && has_media(i))
            })
            .or_else(|| {
                items
                    .iter()
                    .find(|i| i.item_type == Some(MessageItemType::Voice as i32) && has_media(i))
            })
            .or_else(|| {
                items.iter().find_map(|i| {
                    (i.item_type == Some(MessageItemType::Text as i32))
                        .then(|| i.ref_msg.as_ref()?.message_item.as_deref())
                        .flatten()
                        .filter(|m| is_media_item(m))
                })
            })
    });
    if let Some(item) = media_item {
        let dir = std::env::temp_dir().join("weixin-bot-media");
        tokio::fs::create_dir_all(&dir).await?;
        media_opts = download_media_from_item_to_file(item, &deps.cdn_base_url, |content_type| {
            let ext = if content_type.contains("image") {
                ".jpg"
            } else if content_type.contains("video") {
                ".mp4"
            } else if content_type.contains("audio") {
                ".silk"
            } else {
                ".bin"
            };
            dir.join(temp_file_name("media", ext))
        })
        .await
        .unwrap_or_default();
    }

    let ctx = weixin_message_to_msg_context(&full, &deps.account_id, Some(media_opts));
    if let (Some(token), Some(from)) = (&full.context_token, &full.from_user_id) {
        set_context_token(&deps.account_id, from, token);
    }

    if let Some(reply) = (deps.on_message)(ctx.clone()).await? {
        let opts = WeixinApiOptions {
            base_url: deps.base_url.clone(),
            token: deps.token.clone(),
            timeout_ms: None,
            long_poll_timeout_ms: None,
        };
        send_message_weixin(&ctx.from, &reply, &opts, ctx.context_token.as_deref()).await?;
    }
    Ok(())
}